在外人看来,数据科学似乎是一门庞大而模糊的学科。当今的数据科学专家并没有上大学以获得数据科学学位(尽管现在许多大学都提供这些课程)。
第一代专业数据科学家来自数学,统计学,计算机科学和物理学等学科。
数据科学的“科学”部分是提出问题,生成假设,检查证据并制定解释证据的模型。
这些是任何人都可以学习的技能,并且比以往任何时候都有更多的资源来学习。
最好的资源之一是Kaggle 。他们的数据科学竞赛为所有人提供了一个挑战真实项目的平台。围绕这些挑战而形成的社区也是向他人学习的好地方。
当我从物理学家转变为数据科学家时,Kaggle是我自学新技能(尤其是使用scikit-learn
之类的机器学习库)时所使用的资源之一。
本文来自《数据黑客》,登录官网可阅读更多精彩资讯和文章。
在本文中,我将使用经典挑战“Titanic”来解释如何为任何数据科学问题找到一个成功的解决方案。
这项挑战的目的是建立一个模型,该模型可以根据乘客信息来预测是否存活。数据集包括乘客的姓名,年龄,性别,船舱等级和家庭信息,以及他们是否在灾难中幸免。
Kaggle提供训练数据和测试数据。训练数据具有生存的“基本事实”标签(是/否),但是测试数据不包含基本事实标签。Kaggle保留这些标签,并使用它们对您的模型进行评分。测试数据预测取决于您自己的预测,预测的准确性用于确定您在排行榜上的位置。
在开始数据科学项目之前,我建议您这样设置工作环境:
建议使用conda
来管理Python环境。我首选的数据科学库是numpy
, pandas
,matplotlib
, seaborn
和scikit-learn
。根据问题的性质,其他库(例如scipy
)可能是相关的。深度学习挑战要安装Tensorflow
或PyTorch
。
最后是加载数据。建议将数据从Kaggle下载到自己的计算机上,并存储到名为data
的子文件夹中。
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
pd.set_option('display.max_rows', 200)
import seaborn as sns
# Apply the default theme
sns.set()
# Assuming you've downloaded the data to your own machine and
# it resides in a subfolder called "data".
train_data = pd.read_csv('./data/train.csv')
test_data = pd.read_csv('./data/test.csv')
千万不要跳过这一步。
无论何时使用新数据,了解数据所包含的内容,变量的含义,所使用的单位,数据类型以及数据分布都非常重要。
这将帮助您更深入地理解数据,更容易提出假设并找到正确的解决方案。
训练集的前几行如下所示,“生存(Survived)”列是要预测的目标变量。
Kaggle很好地解释了数据,并用表格解释了每个变量:
其中大多数是不言自明的,但sibsp
和parch
需要更多解释:
sibsp :数据集以这种方式定义家庭关系
兄弟姐妹=兄弟,姐妹,继兄弟,继父
配偶=丈夫,妻子(情妇和未婚夫被忽略)
parch:数据集通过这种方式定义家庭关系…
父母=母亲,父亲
孩子=女儿,儿子,继女,继子
一些孩子只带一个保姆旅行,因此他们的parch = 0
Seaborn擅长可视化数据分布,在本节中,我将以几种不同的方式检查数据。
我想知道妇女和儿童生存率更高的假设是否成立,因此我创建了以下图表,该图表显示了基于性别和年龄的乘客生存状况。
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
# Apply the default theme
sns.set()
# Load the training data
train_data = pd.read_csv('./data/train.csv')
sns.catplot(data=train_data, kind="swarm", x="Sex", y="Age", hue="Survived")
我们的假设是成立的,但是值得注意的是,有相当多的孩子没有幸存下来,而且有相当多的各个年龄段的男性确实幸存了下来,他们的幸存可能与什么事情相关?
也许舱位等级可以预测生存。让我们再看一下数据,这次按船舱等级分类。
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
# Apply the default theme
sns.set()
# Load the training data
train_data = pd.read_csv('./data/train.csv')
sns.catplot(data=train_data, kind="swarm", x="Sex", y="Age",
col="Pclass", hue="Survived")
上图根据年龄,性别和船舱等级(第1,第2,第3)列出生存情况。
在幸存的成年男子中,一等舱乘客的幸存比例更高。二等和三等旅客中也有成年男性幸存者,但相对而言,每组中的旅客人数并不多。
在没有幸存的妇女中,大多数是三等舱旅客。
这告诉我们性别,年龄和船舱等级都可能影响生存,但是每个组中都有离群值。目前尚不清楚这是随机的还是由于更细微的因素造成的。
最后,让我们快速看一下这些乘客的登船地点,也许可以告诉我们一些事情。
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
# Apply the default theme
sns.set()
# Load the training data
train_data = pd.read_csv('./data/train.csv')
sns.catplot(data=train_data, kind="count", x="Survived", col="Embarked")
上图显示了在不同登船口岸的乘客的生存情况。
字母S,C和Q分别代表南安普敦,瑟堡和皇后镇。大部分乘客在南安普敦上船,在瑟堡上船的乘客似乎有更高的生存机会,但港口和生存之间似乎没有很强的相关性。
有了这些洞察,我们开始制定有关数据的假设,并在接下来的环节进行测试。如果不可视化数据,我们就不会有这些理解。
即便在最理想的情况下,数据也很少是“干净的”,这意味着数据集可能存在缺失值或错误。有时候数据将以需要的格式进行转换,过滤或转换记录单位,然后才能进行下一步的工作。
如何清洗数据取决于具体的项目:
仔细查看Titanic数据,我们会发现一些问题:
X = train_data
y = train_data['Survived']
X.drop(columns = ['Survived'], inplace=True)
for col in X.columns:
if X[col].isna().any():
print('Column "{}" is missing data.'.format(col))
代码输出:
Column "Age" is missing data.
Column "Cabin" is missing data.
Column "Embarked" is missing data.
这里我使用pandas
数据框的.isna()
方法检查缺失值。缺少数据或空值会产生“NA”代码,如果缺失数值型数据,则会生成“NaN”,表示“不是数字(Not a Number)”。
Titanic数据中包含许多缺失值,有时不知道一个人的年龄,他们所住的客舱(如果有)或登船口是什么。
我们的选择是:
每种方法都有其优点。在第一种情况下,我们不对数据做任何假设,而只是选择摆脱表中所有不完整的行。好处是不会往模型中注入太多主观假设,代价是训练数据可能不足。
在训练机器学习模型时,更多的数据总是更好。如果您有大量干净的数据,则最好丢弃所有不完整的样本。但是,如果表中的每一行都是宝贵的,那么最好找到合理的值来填补漏洞。
Titanic数据集不是很大,训练集只有不到1000名乘客,而且我们可能需要进一步细分训练数据以验证模型,从而导致训练集数据不足。
机器学习模型需要数值输入,但是很多Titanic数据是分类变量,我们需要以某种方式将分类变量转换为数值变量。
Sex
列只有两个值,即female
和male
,我们可以将它们重新映射为0
和1
。
train_data.Sex = train_data.Sex.map({‘female’: 0, ‘male’: 1})
下面将使用“独热编码(one-hot encoding)”技术来处理具有多于两个类别的分类变量,例如登船地点(有3种类别)。
通过一些合理的假设,我们实际上可以很好地填补数据中的缺失值。
年龄
我们可以采用好几种策略,例如简单地将所有乘客的平均年龄估算为缺失值。
但是我们可以做得更好。
我的策略是使用每个船舱等级的平均乘客年龄。
for pclass, grp in X.groupby('Pclass'):
print('Class:', pclass, '-- Median Age:', grp.Age.median())
结果如下:
Class: 1 -- Median Age: 37.0
Class: 2 -- Median Age: 29.0
Class: 3 -- Median Age: 24.0
头等舱的乘客往往年龄更大,这并不令人惊讶,进入二等舱和三等舱,年龄会有所下降。
对于缺失年龄的观测值,我用该观测值所属的船舱等级的年龄中位数来填补。
def impute_age(row):
if row['Pclass'] == 1:
age = 37.0
elif row['Pclass'] == 2:
age = 29.0
elif row['Pclass'] == 3:
age = 24.0
return age
missing_ages = X.Age.isna()
X.loc[missing_ages, 'Age'] = X[missing_ages].apply(lambda row: impute_age(row), axis=1)
登船港口
该变量的缺失值较少,最常见的登船点是南汉普顿,因此在其他条件相同的情况下,最有可能乘客会登上那里,所有级别的乘客都是如此。
missing_embarked = X.Embarked.isna()
X.loc[missing_embarked, 'Embarked'] = 'S'
从船舱到甲板
我们表中的许多行都包含一个舱号,最初尚不清楚如何利用此信息,但我们可以根据船舱号来确定船甲板,例如,“C22”在Deck C上。
客舱大多位于B到F甲板上,有关轮船布局的一些信息可以在这里找到,该页面还指示可以在哪里找到一等舱,二等舱和三等舱。
对于已知船舱号的乘客,我用它来推断甲板。
对于没有船舱号的乘客,我根据他们的票价来推断他们最可能乘坐的甲板。
我在数据框中创建了一个名为“Deck”的新列,并在其中写入了所有推断出的甲板信息。现在可以删除“Cabin”列。
def infer_deck(row):
if type(row['Cabin']) == str:
deck = str(row['Cabin'])[0]
else:
deck = 'Unknown'
return deck
X['Deck'] = X.apply(lambda row: infer_deck(row), axis=1)
for pc, grp in X.groupby('Pclass'):
print('\n Class:', pc)
print(grp['Deck'].value_counts())
# For each class, impute missing deck by deck layout inferred from
# https://www.dummies.com/education/history/titanic-facts-the-layout-of-the-ship/
# Pclass 1: 'C'
# Pclass 2: 'E'
# Pcasss 3: 'F'
def infer_deck_v2(row):
if row['Pclass'] == 1:
deck = 'C'
elif row['Pclass'] == 2:
deck = 'E'
else:
deck = 'F'
return deck
unknown_decks = X['Deck'] == 'Unknown'
X.loc[unknown_decks, 'Deck'] = X[unknown_decks].apply(lambda row: infer_deck_v2(row), axis=1)
X.drop(['Cabin'],axis=1, inplace=True)
输出为:
Class: 1
C 59
B 47
Unknown 40
D 29
E 25
A 15
T 1
Name: Deck, dtype: int64
Class: 2
Unknown 168
F 8
D 4
E 4
Name: Deck, dtype: int64
Class: 3
Unknown 479
F 5
G 4
E 3
Name: Deck, dtype: int64
对于所有带有未知甲板的乘客,我根据他们的乘客等级将他们分配到一个甲板。
我花了大量时间研究可以从ticket
列中收集哪些信息。
您会注意到某些票证带有前缀,例如“ SC/PARIS”,后跟数字。前缀和数字都可以告诉我们一些信息,我的猜测是前缀指示票务供应商,有时候可以通过票号推断出一起旅行的人群。
我对前缀数据做了很多深层清理和推断,最后我还是删除了它,因为它似乎并没有预测价值。如果找到了使用该变量来改进模型的方法,请发表评论。
Kaggle论坛上对此主题进行了很好的讨论。
现在我们已经清理了数据,可以尝试一些简单的测试。让我们分离一些测试数据,这些数据可以用来检验我们的假设。对于这些分离的测试数据,我们知道真实的目标变量标签,因此可以测量预测的准确性。
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
XX = X[['Age','Sex']]
std_scaler = StandardScaler()
XX = std_scaler.fit_transform(XX)
X_train, X_test, y_train, y_test = train_test_split(XX, y, test_size=0.25,
random_state=42)
我们知道Titanic的幸存者逃离了救生艇,而这些(我们假设)将优先容纳妇女和儿童。仅凭这两个变量,是否能够准确预测生存?
使用逻辑回归进行测试:
clf = LogisticRegression()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(accuracy_score(y_pred, y_test))
预测精度为:
0.7847533632286996
78%的准确性非常好!正如预期的那样,这两个变量具有很高的预测性。
接下来,我们可以假设,由于头等舱旅客的状态或他们的机舱靠近上层甲板,他们更有可能成为幸存者,所以让我们看看仅靠头等舱是否可以作为一个很好的预测指标。然后,我们将看看将其与年龄和性别相结合是否可以改善结果。
clf = LogisticRegression()
# Need to split Pclass into 3 separate binary columns
X = pd.concat([X, pd.get_dummies(X['Pclass'], prefix='Pclass')],
axis=1)
X.drop(['Pclass'],axis=1, inplace=True)
# Keep only the class columns
XX = X[['Pclass_1', 'Pclass_2', 'Pclass_3']]
std_scaler = StandardScaler()
XX = std_scaler.fit_transform(XX)
X_train, X_test, y_train, y_test = train_test_split(XX, y, test_size=0.25,
random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print('Using Pclass as the sole predictor, our accuracy:')
print(accuracy_score(y_pred, y_test))
XX = X[['Age', 'Sex', 'Pclass_1', 'Pclass_2', 'Pclass_3']]
std_scaler = StandardScaler()
XX = std_scaler.fit_transform(XX)
X_train, X_test, y_train, y_test = train_test_split(XX, y, test_size=0.25,
random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print('\nUsing Pclass, age, and sex as predictors, our accuracy:')
print(accuracy_score(y_pred, y_test))
对Pclass
变量进行独热编码,我将在下面说明独热编码的重要性。测试结果为:
Using Pclass as the sole predictor, our accuracy:
0.6995515695067265
Using Pclass, age, and sex as predictors, our accuracy:
0.7937219730941704
使用船舱等级作为唯一的预测指标,我们的分类器的准确性几乎达到70%。结合年龄和性别,结果略有改善:分别为79%和78%。这种差异不是很大,可能是噪音。
这些实验告诉我们,生存很大程度上取决于年龄,性别和社会经济地位。仅这三个因素就可能使我们对生存情况有一个较好的预测。
但是要想将准确率提升几个百分点,需要进行一些特征工程(feature engineering)。
出色的特征工程通常会将专家与新手数据科学家区分开来。任何人都可以使用现成的软件库,用几行Python代码训练机器学习模型,并使用它进行预测。但是,数据科学不仅仅涉及模型选择。您需要为该模型提供高质量的预测特征。
特征工程通常意味着创建新特征,以帮助您的机器学习模型做出更好的预测。有一些工具可以使这一过程自动化,但是最好先深入考虑数据以及可能导致目标结果的其他因素。
在Titanic数据集中,我们有一些关于家庭出行的信息。“sibsp”和“parch”向我们介绍了乘客的兄弟姐妹,配偶,父母和子女的数量。我们可以创建一个新变量“ Family Size”,它是“ sibsp”和“ parch”的总和。
X['Family Size'] = X['SibSp'] + X['Parch']
许多Kagglers还会创建一个名为“not_alone”的变量,它是一个二进制标识符,描述乘客是否自己旅行。
该数据集包含许多分类数据,例如登船口岸有3个标签:瑟堡,皇后镇和南安普敦。ML模型需要数值数据,因此我们必须将分类变量映射为数值变量:
{'Cherbourg': 1, 'Queenstown': 2, 'Southampton': 3}
考虑一下这对机器学习模型的影响。南安普敦的重要性比瑟堡高3倍吗?不,那是荒谬的,每个港口都同样重要。
取而代之的是,我们对分类变量实施“独热编码(one-hot encoding)”,这将创建三个新列,每个港口对应一列,使用数字0或1表示乘客是否登上了特定的港口。
我们可以对其他分类变量(例如deck)执行相同的操作。在数据集中,性别也是一个分类变量,但是由于标签是“女性”或“男性”,因此只使用0/1来表示,无需创建新列。
独热编码的一个主要缺点是它会创建许多新列,每列均视为独立特征,更多特征并不总是一件好事。我们希望观测值的数量大大超过特征的数量,这可以防止过度拟合。
一些Kagglers发现,针对年龄或票价范围创建单独的分组很有帮助。当试图登上救生艇时,船员可能不是在询问年龄,而是在考虑“婴儿”,“孩子”,“年轻”,“老人”等年龄类别。您可以创建类似的分类变量,看看这是否对您的模型有所帮助。我将保留年龄和票价变量不变。
上述步骤实际上是最难的部分,约占工作量的80%至90%。
接下来的几个步骤通常更轻松,更有趣。我们可以尝试不同的机器学习模型,以查看它们的性能如何,并选择有前途的模型进行进一步的优化。
由于我们只是在尝试预测二元变量“survival”,因此任何二元分类器都将起作用,scikit-learn
提供了很多选择。
一些最受欢迎的分类器是:
最后一个XGBoost不是scikit-learn
一部分,因此您必须单独安装它。
让我们获取训练数据,选择一个分类器,然后使用k折交叉验证进行测试。
交叉验证是一种技术,在保留剩余数据的同时对模型进行训练,可以忽略一小部分数据。然后针对遗漏的数据测试模型的准确性。将这个过程重复k次,每次都会随机抽取剩余数据的一部分。
在上述分类器中,逻辑回归和决策树最容易理解。随机森林是由许多决策树构成的整体模型。AdaBoost和XGBoost属于更高级的模型,其中XGBoost在Kagglers中非常受欢迎。
我不会在本文中介绍每个分类器的原理,这些信息很容易在网上找到。
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import cross_validate
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier
import xgboost as xgb
X.drop(columns=['PassengerId', 'Name'], inplace=True)
std_scaler = StandardScaler()
X = std_scaler.fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25,
random_state=42)
def assess_model(y_test, y_pred):
scores = cross_validate(clf, X, y, cv=5,
scoring=('accuracy'),
return_train_score=True)
train_avg = np.mean(scores['train_score'])
train_std = np.std(scores['train_score'])
test_avg = np.mean(scores['test_score'])
test_std =np.std(scores['test_score'])
print('Average Train Accuracy: {:5.3f} ±{:4.2f}'.format(train_avg, train_std))
print('Average Test Accuracy: {:5.3f} ±{:4.2f}'.format(test_avg, test_std))
return
clf = LogisticRegression(class_weight='balanced', max_iter=1000, solver='lbfgs')
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
assess_model(y_test, y_pred)
clf = DecisionTreeClassifier(random_state=0)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
assess_model(y_test, y_pred)
clf = RandomForestClassifier(random_state=0)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
assess_model(y_test, y_pred)
clf = AdaBoostClassifier(random_state=0)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
assess_model(y_test, y_pred)
clf = xgb.XGBClassifier()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
assess_model(y_test, y_pred)
assess_model
函数使用5折交叉验证测试每个分类器的准确性,结果如下:
# Logistic regression
Average Train Accuracy: 0.802 ±0.01
Average Test Accuracy: 0.780 ±0.02
# Decision tree
Average Train Accuracy: 0.974 ±0.00
Average Test Accuracy: 0.781 ±0.02
# Random forest
Average Train Accuracy: 0.974 ±0.00
Average Test Accuracy: 0.791 ±0.03
# AdaBoost
Average Train Accuracy: 0.832 ±0.00
Average Test Accuracy: 0.804 ±0.02
# XGBoost
Average Train Accuracy: 0.964 ±0.00
Average Test Accuracy: 0.820 ±0.03
要注意一点,每个分类器都使用了默认超参数。
有时分类器可以在训练集上获得约97%的准确性,这看起来很棒,但很可能是过拟合的结果。测试集的预测精度才是我们关心的指标。
在这些分类器中,XGBoost具有最高的准确性。
大多数机器学习模型具有可调整的参数,这些参数通常会影响模型的准确性。对于每个问题,这些参数的最佳性能值都不同。
在ML中,这些参数通常称为“超参数”,调整超参数与其说是一门科学倒不如是一门艺术。
有一些工具可以帮助您进行调整。 TPOT就是一个例子。为了简单起见,我们将执行一个简单的网格搜索,并手动测试整个范围的合理超参数值,以查看哪个参数可以给我们带来最佳结果。
from sklearn.model_selection import GridSearchCV
param_grid = {'bootstrap': [True],
'max_depth': [2, 6, None],
'max_features': ['auto', 'log2'],
'min_samples_leaf': [1, 2, 3, 5],
'min_samples_split': [2, 4, 6],
'n_estimators': [100, 350]
}
forest_clf = RandomForestClassifier()
forest_grid_search = GridSearchCV(forest_clf, param_grid, cv=5,
scoring="accuracy",
return_train_score=True,
verbose=True,
n_jobs=-1)
forest_grid_search.fit(X_train, y_train)
这段代码要花一些时间才能运行,因为它会尝试所有不同的超参数组合。
print(forest_grid_search.best_params_)
print(forest_grid_search.best_estimator_)
print(forest_grid_search.best_score_)
结果如下:
{'bootstrap': True,
'max_depth': 6,
'max_features': 'auto',
'min_samples_leaf': 2,
'min_samples_split': 4,
'n_estimators': 100}
RandomForestClassifier(max_depth=6, min_samples_leaf=2, min_samples_split=4)
0.830804623499046
在Kaggle Titanic挑战中,如果不作弊,很难获得超过83%的准确率。我提到了其他几种提高预测精度的潜在方法,例如对年龄或票价分类,以及根据阶级和性别来估算缺失的年龄值。您可以尝试这些方法,看看它们是否可以提高准确性。
解决数据科学问题是一个迭代的过程,即从头开始,了解您的数据,进行清洗,然后反复测试不同的模型并添加更多特征,直到获得良好的性能为止。最终您还可以优化最佳模型,进一步提升预测精度。
来源:Medium
作者:Mikhail Klassen
翻译校对:数据黑客
原文标题:How to Structure Your Data Science Workflow
数据黑客:专注金融大数据,聚合全网最好的资讯和教程,提供开源数据接口。
我们聚合全网最优秀的资讯和教程:
我们提供开源数据接口: