构建数据科学工作流

2021-01-20 19:15·人工智能遇见磐创

打开网易新闻 查看更多图片

1.入门

从外部看,数据科学似乎是一门庞大而模糊的学科。今天的数据科学专家并不是为了获得数据科学学位而上大学的(尽管现在许多大学都提供这些课程)。

第一代专业数据科学家来自数学、统计学、计算机科学和物理学科。

数据科学的“科学”部分是提出问题、生成假设、检验,以及形成解释证据的模型的工作。

这些技能是任何人都可以学习的,而且现在比以往任何时候都有更多的资源可以开始学习。

最好的资源之一是Kaggle。他们的数据科学竞赛对任何人来说都是一个机会,让他们能够去练习项目。围绕这些挑战形成的社区也是向他人学习的好地方。

案例研究

在本文中,我将使用经典挑战“泰坦尼克号”来解释如何处理数据科学问题并找到一个成功的解决方案:https://www.kaggle.com/c/titanic/overview 。

这项挑战的目的是建立一个模型,根据乘客名单中已知的乘客信息,预测乘客的存活率。这是基于历史数据,我们知道很多乘客的姓名、年龄、性别、船舱等级和家庭信息,以及他们是否在灾难中幸存下来。

Kaggle提供训练数据和测试数据。训练数据有生存的“真实”标签(是/否),但测试数据不包括基本真实标签。Kaggle保留这些标签,并使用它们为你的提交打分。测试数据预测取决于你的预测,预测的准确性将用于确定你在排行榜上的位置。

如何在泰坦尼克号挑战赛中获得满分

关于泰坦尼克号挑战赛的旁注:如果你看看排行榜,你会看到很多完美的分数。这自然会让你怀疑,“他们是怎么做到的?”。

答案令人沮丧——他们作弊了。如果你在网上搜索一段时间,你会发现可以在互联网上找到带有基本事实标签的完整测试数据。那些有完美分数的人只需提交真实的标签,而不是机器学习模型的预测……然后得到一个完美的分数。

但他们没有通过真正的挑战——挑战存在于掌握一门技术,而不是偷取高分。

工作环境

在开始数据科学项目之前,我建议设置你的工作环境,以便:

  1. 新建项目文件夹,其中包含用于存储数据的子文件夹;
  2. 独立的虚拟环境,安装了标准的数据科学库。

对于虚拟环境,我建议使用conda来管理Python环境。我最喜欢的数据科学库是numpy、pandas、matplotlib、seaborn和scikit-learn。根据问题的性质,其他库(如scipy)可能是相关的。深度学习的挑战包括安装Tensorflow或PyTorch。

最后我们加载数据。假设你已经将挑战的数据从Kaggle下载到你自己的机器上,并将其放入名为data的子文件夹中,并且你正在Jupyter Notebook中编写代码。

或者,你可以直接在Kaggle平台上创建一个数据科学Notebook。

importnumpyasnpimportmatplotlib.pyplotaspltimportpandasaspdpd.set_option('display.max_rows',200)importseabornassns#应用默认主题sns.set()#假设你已经将数据下载到自己的机器上#它位于名为“data”的子文件夹中train_data=pd.read_csv('./data/train.csv')test_data=pd.read_csv('./data/test.csv')

2.探索性数据分析

不要跳过这第一步。

无论何时使用新数据,了解数据包含的内容、变量的含义、使用的单位和数据类型以及分布的外观都很重要。

这将有助于你对数据建立直觉,使其更容易产生假设,这也有望使解决方案更容易找到。

打开网易新闻 查看更多图片

训练数据的前几行如下所示。“Survived”列表示生存,它是我们试图预测的目标变量。

challenge网站很好地解释了数据,并用表格解释了每个变量:

其中大多数都是不言而喻的,但是sibsp和parch需要更多的信息:

sibsp:数据集以这种方式定义家庭关系…

Sibling=兄弟姐妹

Spouse=丈夫、妻子

parch:数据集以这种方式定义家庭关系…

Parent=母亲,父亲

Child=女儿,儿子,继女,继子

有些孩子只和保姆一起旅行,因此对他们来说parch=0。

按年龄或性别分列的存活率

seaborn库擅长可视化,因此在本节中,我将以几种不同的方式检查数据。

我很想知道我们关于妇女和儿童存活率的假设是否成立,所以我设计了以下的图。

importnumpyasnpimportmatplotlib.pyplotaspltimportpandasaspdimportseabornassns#应用默认主题sns.set()#加载训练数据train_data=pd.read_csv('./data/train.csv')sns.catplot(data=train_data,kind="swarm",x="Sex",y="Age",hue="Survived")

用seaborn创建的图显示了基于性别和年龄的乘客存活率。

我们的假设成立得相当好,但值得注意的是,相当多的儿童没有存活下来,相当多不同年龄的男性确实存活了下来。他们的生存可能与其他什么有关吗?

船舱等级

也许船舱等级是生存的预测。让我们再看一次数据,但这次是按船舱等级划分的。

importnumpyasnpimportmatplotlib.pyplotaspltimportpandasaspdimportseabornassns#应用默认主题sns.set()#加载训练数据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)的乘客存活率。

我们在这里看到了一些内容。在幸存的成年男性中,一等舱乘客存活率较高;在二等舱和三等舱乘客中也有成年男性生还者,但相对于每组乘客总人数而言没有一等舱那么多。

在没有幸存下来的女性中,大多数是三等舱乘客。

这告诉我们,性别、年龄和船舱等级都可能是生存率的预测因素,但在每一组中都有异常值。目前还不清楚这是随机的,还是由于更微妙的因素。

登船

最后,让我们快速看看这些乘客在哪里登船。

importnumpyasnpimportmatplotlib.pyplotaspltimportpandasaspdimportseabornassns#应用默认主题sns.set()#加载训练数据train_data=pd.read_csv('./data/train.csv')sns.catplot(data=train_data,kind="count",x="Survived",col="Embarked")

字母S、C和Q代表南安普顿、瑟堡和皇后镇。大多数乘客在南安普顿上船。在瑟堡上船的乘客从他们的同龄人中看来生存的机会稍好一些,但港口和生存之间似乎没有很强的相关性。

有了这些见解,我们已经开始形成关于数据的假设,我们将在稍后进行测试。如果没有可视化的数据,我们就不会有同样的直觉。

3.清理数据

即使在最好的情况下,数据也很少是“干净的”,这意味着数据中可能存在缺失值或错误。其他时候,数据将以需要转换、过滤或其他方式处理的单位记录,然后才能进行任何进一步的工作。

数据清理没有单一的方法。这取决于你的数据:

  • 图像可能需要重新缩放、旋转、颜色校正、平滑、锐化;
  • 音频可能需要过滤、重新录制、去噪或标准化;
  • 自然语言数据(文本)可能需要更正大小写,删除停用词,并删除标点符号。

如果我们仔细研究泰坦尼克号的数据,会发现一些问题:

X=train_datay=train_data['Survived']X.drop(columns=['Survived'],inplace=True)forcolinX.columns:ifX[col].isna().any():print('Column"{}"ismissingdata.'.format(col))

此代码生成输出:

Column"Age"ismissingdata.Column"Cabin"ismissingdata.Column"Embarked"ismissingdata.

处理NAN

这里我使用pandas来用.isna()方法检查空值。缺少数据或空值将生成“NA”。如果需要数字数据,它会产生一个“NaN”,意思是“不是一个数字”。

泰坦尼克号的数据中有很多缺失的值。有时我们不知道一个人的年龄,或者他们住的是哪个舱室(如果有的话),或者他们的出发港是什么。

这给我们留下了几个选择:

  • 删除所有缺少值的行
  • 找到合理的数值来填补

每种方法都有其优点。在第一种情况下,我们对数据不做任何假设,只选择删除表中任何不完整的行。这样做的好处是,我们不会用我们的假设来偏袒未来的模型,而是以减少训练示例为代价。

在训练机器学习模型时,数据越多越好。如果你有很多干净的数据,扔掉任何不完整的样本可能是好的。但是如果表中的每一行都是宝贵的,那么最好找到值来填补。

泰坦尼克号的数据集不是很大。我们的训练设备里只有不到1000名乘客。我们可能需要进一步细分训练数据来验证模型,这样我们的训练例子就更少了。

重新映射类别数据

机器学习模型需要数值数据,但很多泰坦尼克号的数据是离散数据。我们需要把这些数据转换成数字。

“Sex”列只有两个值,女性和男性。我们可以把它们重新映射到0和1。

train_data.Sex=train_data.Sex.map({‘female’:0,‘male’:1})

下面将使用一种称为“one-hot”的技术来处理具有两种以上可能性的分类数据,例如出发港(有3个可能的值)。

填补缺失值

通过一些合理的假设,我们实际上可以很好地填补数据。

年龄

我们可以在这里使用几种策略,例如简单地用所有乘客的平均年龄填充缺失值。

但我们可以做得更好。

我的策略是观察每一个船舱等级的人的平均年龄。

forpclass,grpinX.groupby('Pclass'):print('Class:',pclass,'--MedianAge:',grp.Age.median())

结果是:

Class:1--MedianAge:37.0Class:2--MedianAge:29.0Class:3--MedianAge:24.0

当你进入二等舱和三等舱时,我发现头等舱的乘客往往年龄偏大,而且年龄有下降的趋势,对此我并不感到惊讶。

对于所有缺失的年龄值,我根据船舱等级给他们分配了中值。

defimpute_age(row):ifrow['Pclass']==1:age=37.0elifrow['Pclass']==2:age=29.0elifrow['Pclass']==3:age=24.0returnagemissing_ages=X.Age.isna()X.loc[missing_ages,'Age']=X[missing_ages].apply(lambdarow:impute_age(row),axis=1)

你可以通过观察男女的平均年龄,然后根据这两个变量填写缺失的数据,从而进一步改进这项技术。

登船港

这里没有多少缺失的值。最常见的登船港是南汉普顿,所以在其他条件相同的情况下,乘客很可能在那里登船。所有级别的乘客都是这样。

missing_embarked=X.Embarked.isna()X.loc[missing_embarked,'Embarked']='S'

从船舱到甲板

我们表中的许多行都包含一个舱位号。最初还不清楚如何利用这些信息,但我们可以根据船舱号来确定甲板。例如,“C22”在甲板C上。

客舱大多在B到F甲板上。关于船舶布局的一些信息可以在这里找到。同一页还显示了一等舱、二等舱和三等舱的位置。

对于已知舱位号的乘客,我用它来推断甲板。

对于没有舱位号的乘客,我用他们的船舱等级来推断他们最有可能占用的舱位。

我在我的数据框中创建了一个名为“Deck”的新列,并在其中写入所有推断的Deck信息。“客舱(Cabin)”栏现在可以删除了。

definfer_deck(row):iftype(row['Cabin'])==str:deck=str(row['Cabin'])[0]else:deck='Unknown'returndeckX['Deck']=X.apply(lambdarow:infer_deck(row),axis=1)forpc,grpinX.groupby('Pclass'):print('\nClass:',pc)print(grp['Deck'].value_counts())#对于每个类,根据从中推断出的甲板布局来计算缺失的甲板#https://www.dummies.com/education/history/titanic-facts-the-layout-of-the-ship/#Pclass1:'C'#Pclass2:'E'#Pcasss3:'F'definfer_deck_v2(row):ifrow['Pclass']==1:deck='C'elifrow['Pclass']==2:deck='E'else:deck='F'returndeckunknown_decks=X['Deck']=='Unknown'X.loc[unknown_decks,'Deck']=X[unknown_decks].apply(lambdarow:infer_deck_v2(row),axis=1)X.drop(['Cabin'],axis=1,inplace=True)

输出为:

Class:1C59B47Unknown40D29E25A15T1Name:Deck,dtype:int64Class:2Unknown168F8D4E4Name:Deck,dtype:int64Class:3Unknown479F5G4E3Name:Deck,dtype:int64

我在这里的策略是查看甲板布局,看看大部分一等舱、二等舱和三等舱的位置。它似乎分别是C、E和F甲板,尽管我可能错了。

对于所有未知舱位的乘客,我根据乘客船舱等级将他们分配到一个舱位。

解释车票号码

我花了大量时间研究可以从罚单列中的值中收集到哪些信息。

你会注意到有些票有一个前缀,比如“S.C./PARIS”,后面跟着一个数字。前缀和数字都能告诉我们一些东西。我猜前缀是指售票员。从车票号码本身我们有时可以推断出一群人一起旅行。

我对前缀数据做了一系列深入的清理和消除歧义,但最后,我还是放弃了它,因为它似乎没有带来任何结果。

在Kaggle论坛上有一个关于这个话题的很好的讨论: https://www.kaggle.com/c/titanic/discussion/11127 。

4.假设检验

既然我们已经清理了数据,我们可以尝试一些简单的测试。我们可以分离出测试集。对于这个分离出来的测试数据,我们知道基本的真实标签,所以可以测量我们预测的准确性。

fromsklearn.preprocessingimportStandardScalerfromsklearn.model_selectionimporttrain_test_splitfromsklearn.linear_modelimportLogisticRegressionfromsklearn.metricsimportaccuracy_scoreXX=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)

年龄和性别

我们知道泰坦尼克号的幸存者乘救生艇逃离,而这些救生艇(我们假设)将优先装满妇女和儿童。我们能从这两个变量中准确预测生存率吗?

我们将使用logistic回归进行测试:

clf=LogisticRegression()clf.fit(X_train,y_train)y_pred=clf.predict(X_test)print(accuracy_score(y_pred,y_test))

准确度:

78%的准确率相当不错!显然,这两个变量是高度重要的,正如预期的那样。

船舱等级

接下来,我们可以假设头等舱的乘客,因为他们的身份或者他们的舱室靠近上层甲板,可能更可能是幸存者,所以让我们来看看是否只有舱位是一个很好的预测因素。然后我们再看看结合年龄和性别是否能改善之前的结果。

clf=LogisticRegression()#需要将Pclass拆分为3个单独的二元列X=pd.concat([X,pd.get_dummies(X['Pclass'],prefix='Pclass')],axis=1)X.drop(['Pclass'],axis=1,inplace=True)#只保留该些列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('UsingPclassasthesolepredictor,ouraccuracy:')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('\nUsingPclass,age,andsexaspredictors,ouraccuracy:')print(accuracy_score(y_pred,y_test))

我不得不对Pclass变量进行one-hot编码。我解释一下下面的one-hot编码是什么,为什么它很重要。从这些测试中,我得到的结果是:

UsingPclassasthesolepredictor,ouraccuracy:0.6995515695067265UsingPclass,age,andsexaspredictors,ouraccuracy:0.7937219730941704

因此,使用船舱等级作为唯一的预测因子,我们的logistic分类器的准确率接近70%。结合年龄和性别,我们在之前的结果上略有改善:79%比78%。这种差别不大,可能是噪音。

前几项实验告诉我们的是,生存在很大程度上取决于年龄、性别和社会经济地位。单凭这三个因素,我们就有可能对生存率有一个相当好的预测。

但要想弥补最后几个百分点的准确性,还需要一些特征工程。

打开网易新闻 查看更多图片

5.特征工程

真正优秀的特征工程往往是专家与新手数据科学家的区别。任何人都可以使用现成的软件库,用几行Python训练机器学习模型,并使用它进行预测。但数据科学不仅仅是模型选择。你需要为该模型提供高质量的预测特征。

设计新特征

特征工程通常意味着创建新的特征来帮助机器学习模型做出更好的预测。有一些工具可以使这一过程自动化,但最好首先深入思考数据,以及可能导致目标结果的其他因素。

在我们的泰坦尼克号例子中,我们有一些信息家庭一起旅行。“sibsp”和“parch”列告诉我们乘客有多少兄弟姐妹、配偶、父母和孩子。我们可以创建一个名为“Family Size”的新变量,它是“sibsp”和“parch”的总和。

X['FamilySize']=X['SibSp']+X['Parch']

许多kaggler还会创建一个名为“not_only”的变量,它只是一个二元标识符,用来描述乘客是否独自旅行。

one-hot编码

这个特定的数据集包含大量的分类数据。考虑一下出发港。有3个可能的值:瑟堡、皇后镇和南安普顿。我们需要输入一个数值模型,以便操作:

{'Cherbourg':1,'Queenstown':2,'Southampton':3}

但是想想这对机器学习模型来说是怎样的。南安普敦的价值是瑟堡的3倍吗?不,那太荒谬了。每个港口都同等重要。

相反,我们对这些分类数据执行“one-hot编码”,这将创建三个新列,每个港口一个,我们将使用数字0或1来指示乘客是否在特定港口登船。

我们也可以对其他分类变量做同样的处理,比如deck。性别在我们的数据集中是一个分类变量,但是由于我们的数据集中只有“女性”和“男性”,所以我们只使用0/1来表示。不需要创建新列。

one-hot编码的一个主要缺点是它可以创建许多新列。每列都被视为一个单独的特征。很多特征并不总是一件好事。数据中的示例数量应该大大超过特征的数量。这有助于防止过拟合。

装箱

一些kaggler发现为年龄或票价范围创建单独的“箱子”是有帮助的。考虑到,当救生艇装满水时,船员们可能不是问年龄,而是考虑诸如“婴儿”、“儿童”、“年轻人”、“老年人”等年龄类别。你可以创建类似的容器,看看这是否有助于你的模型。我将保留年龄和票价变量不变。