机器学习口袋参考-一-

66 阅读47分钟

机器学习口袋参考(一)

原文:annas-archive.org/md5/dd771df8c19ec4613e3638bc1f862b92

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

机器学习和数据科学目前非常流行,是发展迅速的目标。我大部分职业生涯都在使用 Python 和数据,并且希望有一本实体书可以提供我在工业界和工作坊教学中使用的常见方法的参考,以解决结构化机器学习问题。

这本书是我认为最好的资源和示例的集合,用于攻击一个有结构化数据的预测建模任务。有许多库执行所需的部分任务,我已经尝试将它们纳入其中,因为我在咨询或行业工作中应用了这些技术。

许多人可能会抱怨缺乏深度学习技术。这些可能需要另外一本书来详细介绍。我也更喜欢简单的技术,而且行业内的其他人似乎也同意。对于非结构化数据(视频、音频、图像)使用深度学习,对于结构化数据使用强大的工具如 XGBoost。

我希望这本书能为您解决紧迫的问题提供有用的参考。

期望什么

本书提供了解决常见结构化数据问题的深入示例。它介绍了各种库和模型、它们的权衡、如何调整它们以及如何解释它们。

代码片段的大小应该适合你在自己的项目中使用和调整。

本书适用对象

如果你刚开始学习机器学习,或者已经从事机器学习多年,这本书应该作为一个有价值的参考。它假设你有一些 Python 的知识,并且完全不涉及语法。相反,它展示了如何使用各种库来解决现实世界中的问题。

这并不取代深入的课程,但应该作为应用机器学习课程可能涵盖的内容的参考。(注:作者将其用作他所教授的数据分析和机器学习课程的参考。)

本书中使用的约定

本书采用以下排版约定:

斜体

表示新术语、网址、电子邮件地址、文件名和文件扩展名。

等宽字体

用于程序清单,以及在段落中引用程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

小贴士

这个元素表示一个提示或建议。

注意

这个元素表示一般的注意事项。

警告

这个元素表示警告或注意事项。

使用代码示例

附加材料(代码示例、练习等)可在https://github.com/mattharrison/ml_pocket_reference获得。

本书的目的是帮助您完成工作。一般情况下,如果本书提供示例代码,您可以在您的程序和文档中使用它。除非您要复制大量代码,否则无需征得我们的许可。例如,编写使用本书多个代码片段的程序不需要许可。出售或分发包含 O’Reilly 书籍示例的 CD-ROM 需要许可。引用本书并引用示例代码回答问题不需要许可。将本书大量示例代码整合到产品文档中需要许可。

我们感谢您的致意,但并不要求署名。署名通常包括标题、作者、出版商和 ISBN。例如:“Machine Learning Pocket Reference by Matt Harrison (O’Reilly). Copyright 2019 Matt Harrison, 978-1-492-04754-4.”

如果您认为您对代码示例的使用超出了合理使用范围或以上给出的权限,请随时联系我们:permissions@oreilly.com

O’Reilly 在线学习

近 40 年来,O’Reilly Media 提供技术和商业培训、知识和见解,帮助企业取得成功。

我们独特的专家和创新者网络通过书籍、文章、会议以及我们的在线学习平台分享他们的知识和专长。O’Reilly 的在线学习平台为您提供按需访问的实时培训课程、深度学习路径、交互式编码环境,以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频资源。更多信息,请访问http://oreilly.com

如何联系我们

请向出版商提出关于本书的评论和问题:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为本书设有一个网页,列出勘误、示例和任何额外信息。您可以访问此页面:http://www.oreilly.com/catalog/9781492047544

要评论或询问关于本书的技术问题,请发送电子邮件至bookquestions@oreilly.com

有关我们的书籍、课程、会议和新闻的更多信息,请访问我们的网站http://www.oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://www.youtube.com/oreillymedia

致谢

感谢我的妻子和家人的支持。我对 Python 社区提供的优秀语言和工具集深表感激。与 Nicole Tache 的合作愉快,她提供了极好的反馈意见。我的技术审阅者 Mikio Braun、Natalino Busa 和 Justin Francis 使我保持了诚实。谢谢!

第一章:介绍

这不太像是一本教学手册,而更像是关于机器学习的笔记、表格和示例。它是作者在培训期间作为额外资源创建的,旨在作为实体笔记本分发。参与者(喜欢纸质材料的物理特性)可以添加自己的笔记和想法,并获得经过筛选的示例的宝贵参考。

我们将介绍如何使用结构化数据进行分类。其他常见的机器学习应用包括预测连续值(回归)、创建聚类或试图降低维度等。本书不讨论深度学习技术。虽然这些技术在非结构化数据上表现良好,但大多数人推荐本书中的技术来处理结构化数据。

我们假设您具有 Python 的知识和熟悉度。学习如何使用pandas 库来处理数据非常有用。我们有许多使用 pandas 的示例,它是处理结构化数据的优秀工具。然而,如果您不熟悉 numpy,一些索引操作可能会令人困惑。完整覆盖 pandas 可能需要一本专著来讨论。

使用的库

本书使用了许多库。这既可能是好事,也可能是坏事。其中一些库可能难以安装或与其他库的版本冲突。不要觉得您需要安装所有这些库。使用“即时安装”并仅在需要时安装您想要使用的库。

>>> import autosklearn, catboost,
category_encoders, dtreeviz, eli5, fancyimpute,
fastai, featuretools, glmnet_py, graphviz,
hdbscan, imblearn, janitor, lime, matplotlib,
missingno, mlxtend, numpy, pandas, pdpbox, phate,
pydotplus, rfpimp, scikitplot, scipy, seaborn,
shap, sklearn, statsmodels, tpot, treeinterpreter,
umap, xgbfir, xgboost, yellowbrick

>>> for lib in [
...     autosklearn,
...     catboost,
...     category_encoders,
...     dtreeviz,
...     eli5,
...     fancyimpute,
...     fastai,
...     featuretools,
...     glmnet_py,
...     graphviz,
...     hdbscan,
...     imblearn,
...     lime,
...     janitor,
...     matplotlib,
...     missingno,
...     mlxtend,
...     numpy,
...     pandas,
...     pandas_profiling,
...     pdpbox,
...     phate,
...     pydotplus,
...     rfpimp,
...     scikitplot,
...     scipy,
...     seaborn,
...     shap,
...     sklearn,
...     statsmodels,
...     tpot,
...     treeinterpreter,
...     umap,
...     xgbfir,
...     xgboost,
...     yellowbrick,
... ]:
...     try:
...         print(lib.__name__, lib.__version__)
...     except:
...         print("Missing", lib.__name__)
catboost 0.11.1
category_encoders 2.0.0
Missing dtreeviz
eli5 0.8.2
fancyimpute 0.4.2
fastai 1.0.28
featuretools 0.4.0
Missing glmnet_py
graphviz 0.10.1
hdbscan 0.8.22
imblearn 0.4.3
janitor 0.16.6
Missing lime
matplotlib 2.2.3
missingno 0.4.1
mlxtend 0.14.0
numpy 1.15.2
pandas 0.23.4
Missing pandas_profiling
pdpbox 0.2.0
phate 0.4.2
Missing pydotplus
rfpimp
scikitplot 0.3.7
scipy 1.1.0
seaborn 0.9.0
shap 0.25.2
sklearn 0.21.1
statsmodels 0.9.0
tpot 0.9.5
treeinterpreter 0.1.0
umap 0.3.8
xgboost 0.81
yellowbrick 0.9
注意

大多数这些库可以使用pipconda轻松安装。对于fastai,我需要使用pip install --no-deps fastaiumap库可以使用pip install umap-learn安装。janitor库可以使用pip install pyjanitor安装。autosklearn库可以使用pip install auto-sklearn安装。

我通常使用 Jupyter 进行分析。您也可以使用其他笔记本工具。请注意,一些工具如 Google Colab 已预安装了许多库(尽管它们可能是过时版本)。

在 Python 中安装库有两个主要选项。一个是使用pip(Python 包管理工具的缩写),这是一个随 Python 一起安装的工具。另一个选项是使用Anaconda。我们将两者都介绍。

使用 Pip 进行安装

在使用pip之前,我们将创建一个沙盒环境,将我们的库安装到其中。这称为名为env的虚拟环境:

$ python -m venv env
注意

在 Macintosh 和 Linux 上,使用python;在 Windows 上,使用python3。如果 Windows 在命令提示符中未能识别它,请重新安装或修复您的安装,并确保选中“将 Python 添加到我的 PATH”复选框。

然后,您激活环境,这样当您安装库时,它们将进入沙盒环境而不是全局的 Python 安装中。由于许多这些库会发生变化并进行更新,最好在每个项目基础上锁定版本,这样您就知道您的代码将可以运行。

这是如何在 Linux 和 Macintosh 上激活虚拟环境:

$ source env/bin/activate

注意到提示符已更新,表示我们正在使用虚拟环境:

  (env) $ which python
  env/bin/python

在 Windows 上,您需要通过运行此命令激活环境:

C:> env\Scripts\activate.bat

再次注意到,提示符已更新,表示我们正在使用虚拟环境:

  (env) C:> where python
  env\Scripts\python.exe

在所有平台上,您可以使用pip安装包。要安装 pandas,键入:

(env) $ pip install pandas

一些包名称与库名称不同。您可以使用以下命令搜索包:

(env) $ pip search libraryname

安装了包之后,你可以使用pip创建一个包含所有包版本的文件:

(env) $ pip freeze > requirements.txt

使用此requirements.txt文件,您可以轻松地将包安装到新的虚拟环境中:

(other_env) $ pip install -r requirements.txt

使用 Conda 安装

conda工具与 Anaconda 捆绑,允许我们创建环境并安装包。

要创建一个名为env的环境,请运行:

$ conda create --name env python=3.6

要激活此环境,请运行:

$ conda activate env

这将在 Unix 和 Windows 系统上更新提示符。现在你可以使用以下命令搜索包:

(env) $ conda search libraryname

要安装像 pandas 这样的包,请运行:

(env) $ conda install pandas

要创建包含包需求的文件,请运行:

(env) $ conda env export > environment.yml

要在新环境中安装这些要求,请运行:

(other_env) $ conda create -f environment.yml
警告

本书提到的一些库无法从 Anaconda 的仓库安装。不要担心。事实证明,你可以在 conda 环境中使用pip(无需创建新的虚拟环境),并使用pip安装这些库。

第二章:机器学习流程概述

数据挖掘的跨行业标准过程(CRISP-DM)是一种进行数据挖掘的过程。它包括几个步骤,可供持续改进参考。它们包括:

  • 业务理解

  • 数据理解

  • 数据准备

  • 建模

  • 评估

  • 部署

图 2-1 展示了我创建预测模型的工作流程,该模型扩展了 CRISP-DM 方法论。下一章节中的详细步骤将覆盖这些基本步骤。

机器学习的通用工作流程。

图 2-1. 机器学习的通用工作流程。

第三章:分类演练:泰坦尼克数据集

本章将通过使用泰坦尼克数据集来解决一个常见的分类问题。后面的章节将深入探讨并扩展在分析过程中执行的常见步骤。

项目布局建议

进行探索性数据分析的一个很好的工具是Jupyter。Jupyter 是一个支持 Python 和其他语言的开源笔记本环境。它允许您创建代码或 Markdown 内容的单元格

我倾向于使用 Jupyter 有两种模式。一种是用于探索性数据分析和快速尝试。另一种是更多地以可交付的方式使用,我使用 Markdown 单元格格式化报告,并插入代码单元格以说明重要的观点或发现。如果你不小心,你的笔记本可能需要一些重构和应用软件工程实践(删除全局变量,使用函数和类等)。

cookiecutter 数据科学包建议了一个布局,用于创建一个分析,以便轻松复制和共享代码。

导入

这个例子主要基于pandasscikit-learnYellowbrick。pandas 库为我们提供了方便的数据整理工具。scikit-learn 库具有出色的预测建模能力,而 Yellowbrick 是一个用于评估模型的可视化库。

>>> import matplotlib.pyplot as plt
>>> import pandas as pd
>>> from sklearn import (
...     ensemble,
...     preprocessing,
...     tree,
... )
>>> from sklearn.metrics import (
...     auc,
...     confusion_matrix,
...     roc_auc_score,
...     roc_curve,
... )
>>> from sklearn.model_selection import (
...     train_test_split,
...     StratifiedKFold,
... )
>>> from yellowbrick.classifier import (
...     ConfusionMatrix,
...     ROCAUC,
... )
>>> from yellowbrick.model_selection import (
...     LearningCurve,
... )
警告

您可能会发现在线文档和示例包括星号导入,例如:

from pandas import *

避免使用星号导入。显式表达会使您的代码更易于理解。

提出问题

在这个例子中,我们想要创建一个预测模型来回答一个问题。它将根据个人和旅行特征对个体是否在泰坦尼克号船难中幸存进行分类。这是一个玩具示例,但它作为一个教学工具,展示了建模的许多步骤。我们的模型应该能够获取乘客信息并预测该乘客在泰坦尼克号上是否会幸存。

这是一个分类问题,因为我们正在预测幸存的标签;他们是否幸存下来。

数据术语

我们通常用一个数据矩阵来训练模型。(我更喜欢使用 pandas DataFrames,因为它很方便有列标签,但 numpy 数组也可以使用。)

对于监督学习,如回归或分类,我们的目的是有一个将特征转换为标签的函数。如果我们将其写成代数公式,它会像这样:

y = f(X)

X 是一个矩阵。每一行代表数据的一个样本或关于一个个体的信息。X 中的每一列都是一个特征。我们函数的输出 y 是一个包含标签(用于分类)或值(用于回归)的向量(见图 3-1)。

结构化数据布局。

图 3-1。结构化数据布局。

这是标准命名过程,用于命名数据和输出。如果您阅读学术论文或查看库的文档,它们会遵循这种约定。在 Python 中,我们使用变量名X来保存样本数据,即使变量的大写是违反标准命名约定(PEP 8)的。不用担心,每个人都这样做,如果您将变量命名为x,可能会让人觉得有点奇怪。变量y存储标签或目标。

表 3-1 显示了一个基本数据集,包括两个样本和每个样本的三个特征。

表 3-1. 样本(行)和特征(列)

pclassagesibsp
1290
121

收集数据

我们将加载一个 Excel 文件(确保已安装 pandas 和 xlrd¹),其中包含泰坦尼克号的特征。它有许多列,包括一个包含个体生存情况的 survived 列:

>>> url = (
...     "http://biostat.mc.vanderbilt.edu/"
...     "wiki/pub/Main/DataSets/titanic3.xls"
... )
>>> df = pd.read_excel(url)
>>> orig_df = df

数据集包括以下列:

  • pclass - 乘客等级(1 = 1 等,2 = 2 等,3 = 3 等)

  • 生存 - 生存(0 = 否,1 = 是)

  • 姓名 - 姓名

  • 性别 - 性别

  • 年龄 - 年龄

  • sibsp - 船上兄弟姐妹/配偶的数量

  • parch - 船上父母/子女的数量

  • 票 - 票号

  • 票价 - 乘客票价

  • 舱室 - 舱室

  • 登船 - 登船点(C = 瑟堡,Q = 皇后镇,S = 南安普顿)

  • 船 - 救生艇

  • 尸体 - 尸体识别号码

  • 家庭/目的地 - 家庭/目的地

Pandas 可以读取此电子表格,并将其转换为 DataFrame。我们需要抽查数据,并确保可以进行分析。

清洗数据

一旦我们拥有数据,我们需要确保它以我们可以用来创建模型的格式存在。大多数 scikit-learn 模型要求我们的特征是数字(整数或浮点数)。此外,如果模型传递了缺失值(pandas 或 numpy 中的NaN),许多模型将失败。某些模型如果数据经过标准化(具有平均值为 0 和标准偏差为 1)处理,性能会更好。我们将使用 pandas 或 scikit-learn 解决这些问题。此外,泰坦尼克号数据集具有泄漏特征。

泄漏特征是包含有关未来或目标信息的变量。拥有关于目标的数据并不是坏事,并且在模型创建时我们经常会有这些数据。然而,如果这些变量在我们对新样本进行预测时不可用,我们应该将它们从模型中删除,因为它们会泄漏未来的数据。

清洗数据可能需要一些时间。最好有专业主题专家(SME),可以提供处理异常值或缺失数据的指导。

>>> df.dtypes
pclass         int64
survived       int64
name          object
sex           object
age          float64
sibsp          int64
parch          int64
ticket        object
fare         float64
cabin         object
embarked      object
boat          object
body         float64
home.dest     object
dtype: object

我们通常看到int64float64datetime64[ns]object。这些是 pandas 用于存储数据列的类型。int64float64是数值类型。datetime64[ns]存储日期和时间数据。object通常意味着它存储字符串数据,虽然可能是字符串和其他类型的组合。

在从 CSV 文件读取时,pandas 将尝试将数据强制转换为适当的类型,但会退回到 object。从电子表格、数据库或其他系统读取数据可能会提供 DataFrame 中更好的类型。无论如何,浏览数据并确保类型合理都是值得的。

整数类型通常没问题。浮点类型可能有一些缺失值。日期和字符串类型将需要转换或用于特征工程数字类型。低基数的字符串类型称为分类列,从中创建虚拟列可能是值得的(pd.get_dummies 函数负责此操作)。

注意

在 pandas 0.23 版本之前,如果类型为 int64,我们可以确保没有缺失值。如果类型为 float64,值可能是所有浮点数,也可能是类似整数的数字,带有缺失值。pandas 库将具有缺失数字的整数值转换为浮点数,因为这种类型支持缺失值。object 通常意味着字符串类型(或字符串和数字混合)。

从 pandas 0.24 开始,有一个新的 Int64 类型(请注意大小写)。这不是默认的整数类型,但您可以强制转换为此类型并支持缺失数字。

pandas-profiling 库包括一个配置报告。您可以在笔记本中生成此报告。它将总结列的类型,并允许您查看分位数统计、描述统计、直方图、常见值和极端值的详细信息(见图 3-2 和 3-3):

>>> import pandas_profiling
>>> pandas_profiling.ProfileReport(df)

Pandas-profiling 概要。

图 3-2. Pandas-profiling 概要。

Pandas-profiling 变量详细信息。

图 3-3. Pandas-profiling 变量详细信息。

使用 DataFrame 的 .shape 属性来检查行数和列数:

>>> df.shape
(1309, 14)

使用 .describe 方法获取摘要统计信息,并查看非空数据的计数。此方法的默认行为是仅报告数值列。这里的输出被截断,仅显示前两列:

>>> df.describe().iloc[:, :2]
 pclass     survived
count  1309.000000  1309.000000
mean      2.294882     0.381971
std       0.837836     0.486055
min       1.000000     0.000000
25%       2.000000     0.000000
50%       3.000000     0.000000
75%       3.000000     1.000000
max       3.000000     1.000000

计数统计仅包括不是 NaN 的值,因此用于检查列是否缺少数据是有用的。还可以通过查看最小值和最大值来检查是否存在异常值。总结统计是一种方法。绘制直方图或箱线图是稍后将要看到的视觉表示。

我们将需要处理缺失数据。使用 .isnull 方法查找具有缺失值的列或行。在 DataFrame 上调用 .isnull 返回一个新的 DataFrame,其中每个单元格包含 TrueFalse 值。在 Python 中,这些值分别计算为 10,这使我们可以对它们进行求和或计算缺失百分比(通过计算均值)。

代码指示每列中缺失数据的计数:

>>> df.isnull().sum()
pclass          0
survived        0
name            0
sex             0
age           263
sibsp           0
parch           0
ticket          0
fare            1
cabin        1014
embarked        2
boat          823
body         1188
home.dest     564
dtype: int64
提示

.mean替换.sum以获得 null 值的百分比。默认情况下,调用这些方法将沿着 axis 0(沿着索引)应用操作。如果你想获得每个样本的缺失特征的计数,你可以沿 axis 1(沿着列)应用此方法:

>>> df.isnull().sum(axis=1).loc[:10]
0    1
1    1
2    2
3    1
4    2
5    1
6    1
7    2
8    1
9    2
dtype: int64

一家中小企业可以帮助确定如何处理缺失数据。年龄列可能是有用的,所以保留它并插值值可能会为模型提供一些信号。大多数值缺失的列(舱位、船和尸体)往往没有提供价值,可以删除。

body 列(身体识别号)对于许多行来说是缺失的。无论如何,我们都应该删除这一列,因为它泄漏了数据。这一列表示乘客没有生存;我们的模型可能会利用这一点来作弊。我们会把它拿出来。(如果我们创建一个模型来预测乘客是否会死亡,知道他们有一个身体识别号的先验信息会让我们知道他们已经死了。我们希望我们的模型不知道这些信息,而是根据其他列进行预测。)同样,船列泄漏了相反的信息(乘客幸存了)。

让我们来看看一些缺失数据的行。我们可以创建一个布尔数组(一个包含TrueFalse以指示行是否有缺失数据的系列),并使用它来检查缺失数据的行:

>>> mask = df.isnull().any(axis=1)

>>> mask.head()  # rows
0    True
1    True
2    True
3    True
4    True
dtype: bool

>>> df[mask].body.head()
0      NaN
1      NaN
2      NaN
3    135.0
4      NaN
Name: body, dtype: float64

我们稍后将为年龄列填充(或推导出)缺失值。

类型为object的列往往是分类的(但它们也可能是高基数字符串数据,或者是列类型的混合)。对于我们认为是分类的object列,可以使用.value_counts方法来检查值的计数:

>>> df.sex.value_counts(dropna=False)
male      843
female    466
Name: sex, dtype: int64

请记住,pandas 通常会忽略 null 或 NaN 值。如果你想包括这些值,请使用dropna=False来显示 NaN 的计数:

>>> df.embarked.value_counts(dropna=False)
S      914
C      270
Q      123
NaN      2
Name: embarked, dtype: int64

对于处理缺失的登船值,我们有几个选项。使用 S 可能看起来是合乎逻辑的,因为那是最常见的值。我们可以深入研究数据,尝试确定是否有其他更好的选项。我们也可以删除这两个值。或者,因为这是分类的,我们可以忽略它们,并使用 pandas 来创建虚拟列,如果这两个样本只是每个选项都有 0 个条目的话。对于这个特征,我们将选择后者。

创建特征

我们可以删除没有方差或没有信号的列。在这个数据集中没有这样的特征,但是如果有一个名为“is human”的列,其中每个样本都有 1,那么这一列将不提供任何信息。

或者,除非我们正在使用自然语言处理或从文本列中提取数据,其中每个值都不同,否则模型将无法利用此列。姓名列就是一个例子。有些人已经从姓名中提取了标题 t,并将其视为分类。

我们还想删除泄露信息的列。船和 body 列都泄漏了乘客是否幸存的信息。

pandas 的.drop方法可以删除行或列:

>>> name = df.name
>>> name.head(3)
0      Allen, Miss. Elisabeth Walton
1     Allison, Master. Hudson Trevor
2       Allison, Miss. Helen Loraine
Name: name, dtype: object

>>> df = df.drop(
...     columns=[
...         "name",
...         "ticket",
...         "home.dest",
...         "boat",
...         "body",
...         "cabin",
...     ]
... )

我们需要从字符串列创建虚拟列。这将为性别和登船港口创建新的列。Pandas 提供了一个方便的get_dummies函数来实现:

>>> df = pd.get_dummies(df)

>>> df.columns
Index(['pclass', 'survived', 'age', 'sibsp',
 'parch', 'fare', 'sex_female', 'sex_male',
 'embarked_C', 'embarked_Q', 'embarked_S'],
 dtype='object')

此时,性别 _male 和性别 _female 列是完全逆相关的。通常我们会删除任何具有完美或非常高正负相关性的列。多重共线性可能会影响某些模型中特征重要性和系数的解释。以下是删除性别 _male 列的代码:

>>> df = df.drop(columns="sex_male")

或者,我们可以在get_dummies调用中添加一个drop_first=True参数:

>>> df = pd.get_dummies(df, drop_first=True)

>>> df.columns
Index(['pclass', 'survived', 'age', 'sibsp',
 'parch', 'fare', 'sex_male',
 'embarked_Q', 'embarked_S'],
 dtype='object')

创建一个带有特征的 DataFrame(X)和一个带有标签的系列(y)。我们也可以使用 numpy 数组,但那样我们就没有列名了:

>>> y = df.survived
>>> X = df.drop(columns="survived")
提示

我们可以使用pyjanitor 库来替换最后两行:

>>> import janitor as jn
>>> X, y = jn.get_features_targets(
...     df, target_columns="survived"
... )

示例数据

我们总是希望在不同的数据集上进行训练和测试。否则,你不会真正知道你的模型在未见过的数据上的泛化能力。我们将使用 scikit-learn 将 30%的数据分离出来进行测试(使用random_state=42以消除比较不同模型时的随机性影响):

>>> X_train, X_test, y_train, y_test = model_selection.train_test_split(
...     X, y, test_size=0.3, random_state=42
... )

插补数据

年龄列存在缺失值。我们需要从数值中插补年龄。我们只想在训练集上进行插补,然后使用该插补器填充测试集的数据。否则我们会泄漏数据(通过向模型提供未来信息来作弊)。

现在我们有了测试和训练数据,我们可以在训练集上填补缺失值,并使用训练好的插补器填补测试数据集。fancyimpute 库实现了许多算法。不幸的是,这些算法大多数不是以归纳的方式实现的。这意味着你不能先调用.fit再调用.transform,这也意味着你不能基于模型训练的方式来对新数据进行插补。

IterativeImputer类(原本在 fancyimpute 中,但已迁移到 scikit-learn)支持归纳模式。要使用它,我们需要添加一个特殊的实验性导入(从 scikit-learn 版本 0.21.2 开始):

>>> from sklearn.experimental import (
...     enable_iterative_imputer,
... )
>>> from sklearn import impute
>>> num_cols = [
...     "pclass",
...     "age",
...     "sibsp",
...     "parch",
...     "fare",
...     "sex_female",
... ]

>>> imputer = impute.IterativeImputer()
>>> imputed = imputer.fit_transform(
...     X_train[num_cols]
... )
>>> X_train.loc[:, num_cols] = imputed
>>> imputed = imputer.transform(X_test[num_cols])
>>> X_test.loc[:, num_cols] = imputed

如果我们想使用中位数进行插补,我们可以使用 pandas 来实现:

>>> meds = X_train.median()
>>> X_train = X_train.fillna(meds)
>>> X_test = X_test.fillna(meds)

标准化数据

对数据进行归一化或预处理后,许多模型的性能会有所提高。特别是那些依赖距离度量来确定相似性的模型。(请注意,树模型会单独处理每个特征,因此不需要此要求。)

我们将对数据进行标准化预处理。标准化是将数据转换为均值为零、标准差为一的形式。这样模型不会认为具有较大数值范围的变量比具有较小数值范围的变量更重要。我将结果(numpy 数组)放回到 pandas DataFrame 中,以便更轻松地进行操作(并保留列名)。

我通常也不会对虚拟列进行标准化,所以我会忽略它们:

>>> cols = "pclass,age,sibsp,fare".split(",")
>>> sca = preprocessing.StandardScaler()
>>> X_train = sca.fit_transform(X_train)
>>> X_train = pd.DataFrame(X_train, columns=cols)
>>> X_test = sca.transform(X_test)
>>> X_test = pd.DataFrame(X_test, columns=cols)

重构

在这一点上,我喜欢重构我的代码。我通常制作两个函数。一个用于一般清理,另一个用于将其分成训练和测试集,并执行那些需要在这些集上以不同方式进行的突变:

>>> def tweak_titanic(df):
...     df = df.drop(
...         columns=[
...             "name",
...             "ticket",
...             "home.dest",
...             "boat",
...             "body",
...             "cabin",
...         ]
...     ).pipe(pd.get_dummies, drop_first=True)
...     return df

>>> def get_train_test_X_y(
...     df, y_col, size=0.3, std_cols=None
... ):
...     y = df[y_col]
...     X = df.drop(columns=y_col)
...     X_train, X_test, y_train, y_test = model_selection.train_test_split(
...         X, y, test_size=size, random_state=42
...     )
...     cols = X.columns
...     num_cols = [
...         "pclass",
...         "age",
...         "sibsp",
...         "parch",
...         "fare",
...     ]
...     fi = impute.IterativeImputer()
...     X_train.loc[
...         :, num_cols
...     ] = fi.fit_transform(X_train[num_cols])
...     X_test.loc[:, num_cols] = fi.transform(
...         X_test[num_cols]
...     )
...
...     if std_cols:
...         std = preprocessing.StandardScaler()
...         X_train.loc[
...             :, std_cols
...         ] = std.fit_transform(
...             X_train[std_cols]
...         )
...         X_test.loc[
...             :, std_cols
...         ] = std.transform(X_test[std_cols])
...
...     return X_train, X_test, y_train, y_test

>>> ti_df = tweak_titanic(orig_df)
>>> std_cols = "pclass,age,sibsp,fare".split(",")
>>> X_train, X_test, y_train, y_test = get_train_test_X_y(
...     ti_df, "survived", std_cols=std_cols
... )

基线模型

创建一个做某些非常简单的基线模型可以让我们有比较的依据。请注意,使用默认的.score结果给出的是准确率,这可能会误导。一个问题,其中正例是 10,000 中的 1,通过始终预测负例很容易得到超过 99%的准确率。

>>> from sklearn.dummy import DummyClassifier
>>> bm = DummyClassifier()
>>> bm.fit(X_train, y_train)
>>> bm.score(X_test, y_test)  # accuracy
0.5292620865139949

>>> from sklearn import metrics
>>> metrics.precision_score(
...     y_test, bm.predict(X_test)
... )
0.4027777777777778

不同的家族

这段代码尝试了多种算法家族。"没有免费午餐"定理指出,没有一个算法能在所有数据上表现良好。然而,对于某些有限的数据集,可能有一个算法能够在该集上表现良好。(这些天结构化学习的流行选择是一种树提升算法,如 XGBoost。)

在这里,我们使用几个不同的家族,并使用 k 折交叉验证比较 AUC 得分和标准差。一个平均分数稍微低一些但标准差较小的算法可能是一个更好的选择。

因为我们使用了 k 折交叉验证,我们将向模型提供所有的Xy

>>> X = pd.concat([X_train, X_test])
>>> y = pd.concat([y_train, y_test])
>>> from sklearn import model_selection
>>> from sklearn.dummy import DummyClassifier
>>> from sklearn.linear_model import (
...     LogisticRegression,
... )
>>> from sklearn.tree import DecisionTreeClassifier
>>> from sklearn.neighbors import (
...     KNeighborsClassifier,
... )
>>> from sklearn.naive_bayes import GaussianNB
>>> from sklearn.svm import SVC
>>> from sklearn.ensemble import (
...     RandomForestClassifier,
... )
>>> import xgboost

>>> for model in [
...     DummyClassifier,
...     LogisticRegression,
...     DecisionTreeClassifier,
...     KNeighborsClassifier,
...     GaussianNB,
...     SVC,
...     RandomForestClassifier,
...     xgboost.XGBClassifier,
... ]:
...     cls = model()
...     kfold = model_selection.KFold(
...         n_splits=10, random_state=42
...     )
...     s = model_selection.cross_val_score(
...         cls, X, y, scoring="roc_auc", cv=kfold
...     )
...     print(
...         f"{model.__name__:22}  AUC: "
...         f"{s.mean():.3f} STD: {s.std():.2f}"
...     )
DummyClassifier         AUC: 0.511  STD: 0.04
LogisticRegression      AUC: 0.843  STD: 0.03
DecisionTreeClassifier  AUC: 0.761  STD: 0.03
KNeighborsClassifier    AUC: 0.829  STD: 0.05
GaussianNB              AUC: 0.818  STD: 0.04
SVC                     AUC: 0.838  STD: 0.05
RandomForestClassifier  AUC: 0.829  STD: 0.04
XGBClassifier           AUC: 0.864  STD: 0.04

堆叠

如果你在走 Kaggle 的路线(或者想要以解释性为代价的最大性能),堆叠 是一个选项。堆叠分类器使用其他模型的输出来预测目标或标签。我们将使用之前模型的输出并将它们结合起来,看看堆叠分类器是否可以做得更好:

>>> from mlxtend.classifier import (
...     StackingClassifier,
... )
>>> clfs = [
...     x()
...     for x in [
...         LogisticRegression,
...         DecisionTreeClassifier,
...         KNeighborsClassifier,
...         GaussianNB,
...         SVC,
...         RandomForestClassifier,
...     ]
... ]
>>> stack = StackingClassifier(
...     classifiers=clfs,
...     meta_classifier=LogisticRegression(),
... )
>>> kfold = model_selection.KFold(
...     n_splits=10, random_state=42
... )
>>> s = model_selection.cross_val_score(
...     stack, X, y, scoring="roc_auc", cv=kfold
... )
>>> print(
...     f"{stack.__class__.__name__}  "
...     f"AUC: {s.mean():.3f}  STD: {s.std():.2f}"
... )
StackingClassifier  AUC: 0.804  STD: 0.06

在这种情况下,看起来性能稍微下降了一点,以及标准差。

创建模型

我将使用随机森林分类器来创建一个模型。这是一个灵活的模型,往往能够给出不错的开箱即用结果。记得用我们之前拆分的训练和测试数据(调用.fit)来训练它:

>>> rf = ensemble.RandomForestClassifier(
...     n_estimators=100, random_state=42
... )
>>> rf.fit(X_train, y_train)
RandomForestClassifier(bootstrap=True,
 class_weight=None, criterion='gini',
 max_depth=None, max_features='auto',
 max_leaf_nodes=None,
 min_impurity_decrease=0.0,
 min_impurity_split=None,
 min_samples_leaf=1, min_samples_split=2,
 min_weight_fraction_leaf=0.0, n_estimators=10,
 n_jobs=1, oob_score=False, random_state=42,
 verbose=0, warm_start=False)

评估模型

现在我们有了一个模型,我们可以使用测试数据来查看模型对它之前未见过的数据的泛化能力。分类器的.score方法返回预测准确率的平均值。我们希望确保用测试数据调用.score方法(假定它在训练数据上表现更好):

>>> rf.score(X_test, y_test)
0.7964376590330788

我们还可以查看其他指标,比如精确度:

>>> metrics.precision_score(
...     y_test, rf.predict(X_test)
... )
0.8013698630136986

基于树的模型的一个好处是可以检查特征重要性。特征重要性告诉您一个特征对模型的贡献有多大。请注意,去除一个特征并不意味着分数会相应下降,因为其他特征可能是共线的(在这种情况下,我们可以删除性别 _male 或性别 _female 列,因为它们具有完美的负相关性):

>>> for col, val in sorted(
...     zip(
...         X_train.columns,
...         rf.feature_importances_,
...     ),
...     key=lambda x: x[1],
...     reverse=True,
... )[:5]:
...     print(f"{col:10}{val:10.3f}")
age            0.277
fare           0.265
sex_female     0.240
pclass         0.092
sibsp          0.048

特征重要性是通过查看错误增加来计算的。如果去除一个特征增加了模型的错误,那么这个特征就更重要。

我非常喜欢 SHAP 库,用于探索模型认为重要的特征,并解释预测。该库适用于黑盒模型,我们稍后会展示它。

优化模型

模型有控制它们行为的超参数。通过改变这些参数的值,我们改变它们的性能。Sklearn 有一个网格搜索类来评估具有不同参数组合的模型,并返回最佳结果。我们可以使用这些参数来实例化模型类:

>>> rf4 = ensemble.RandomForestClassifier()
>>> params = {
...     "max_features": [0.4, "auto"],
...     "n_estimators": [15, 200],
...     "min_samples_leaf": [1, 0.1],
...     "random_state": [42],
... }
>>> cv = model_selection.GridSearchCV(
...     rf4, params, n_jobs=-1
... ).fit(X_train, y_train)
>>> print(cv.best_params_)
{'max_features': 'auto', 'min_samples_leaf': 0.1,
 'n_estimators': 200, 'random_state': 42}

>>> rf5 = ensemble.RandomForestClassifier(
...     **{
...         "max_features": "auto",
...         "min_samples_leaf": 0.1,
...         "n_estimators": 200,
...         "random_state": 42,
...     }
... )
>>> rf5.fit(X_train, y_train)
>>> rf5.score(X_test, y_test)
0.7888040712468194

我们可以传递scoring参数给GridSearchCV以优化不同的指标。详见第十二章了解指标及其含义的列表。

混淆矩阵

混淆矩阵允许我们查看正确的分类,以及假阳性和假阴性。也许我们希望优化假阳性或假阴性,不同的模型或参数可以改变这一点。我们可以使用 sklearn 获取文本版本,或使用 Yellowbrick 进行绘图(参见图 3-4):

>>> from sklearn.metrics import confusion_matrix
>>> y_pred = rf5.predict(X_test)
>>> confusion_matrix(y_test, y_pred)
array([[196,  28],
 [ 55, 114]])

>>> mapping = {0: "died", 1: "survived"}
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> cm_viz = ConfusionMatrix(
...     rf5,
...     classes=["died", "survived"],
...     label_encoder=mapping,
... )
>>> cm_viz.score(X_test, y_test)
>>> cm_viz.poof()
>>> fig.savefig(
...     "images/mlpr_0304.png",
...     dpi=300,
...     bbox_inches="tight",
... )

Yellowbrick 混淆矩阵。这是一个有用的评估工具,显示了底部的预测类别和侧面的真实类别。一个好的分类器应该在对角线上有所有的值,并且其他单元格中为零。

图 3-4. Yellowbrick 混淆矩阵。这是一个有用的评估工具,显示了底部的预测类别和侧面的真实类别。一个好的分类器应该在对角线上有所有的值,并且其他单元格中为零。

ROC 曲线

接收操作特征曲线(ROC)图是评估分类器常用的工具。通过测量曲线下面积(AUC),我们可以得到一个比较不同分类器的度量。它绘制了真正率与假阳性率。我们可以使用 sklearn 来计算 AUC:

>>> y_pred = rf5.predict(X_test)
>>> roc_auc_score(y_test, y_pred)
0.7747781065088757

或者使用 Yellowbrick 来可视化绘图:

>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> roc_viz = ROCAUC(rf5)
>>> roc_viz.score(X_test, y_test)
0.8279691030696217
>>> roc_viz.poof()
>>> fig.savefig("images/mlpr_0305.png")

ROC 曲线。显示真正率与假阳性率。一般来说,曲线越凸出越好。通过测量 AUC 可以得到一个评估数字。接近 1 表示更好。低于 0.5 是一个较差的模型。

图 3-5. ROC 曲线。显示真正率与假阳性率。一般来说,曲线越凸出越好。通过测量 AUC 可以得到一个评估数字。接近 1 表示更好。低于 0.5 是一个较差的模型。

学习曲线

学习曲线用于告诉我们是否有足够的训练数据。它使用数据的增加部分来训练模型,并测量得分(参见图 3-6)。如果交叉验证得分继续上升,那么我们可能需要投资获取更多数据。以下是一个 Yellowbrick 示例:

>>> import numpy as np
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> cv = StratifiedKFold(12)
>>> sizes = np.linspace(0.3, 1.0, 10)
>>> lc_viz = LearningCurve(
...     rf5,
...     cv=cv,
...     train_sizes=sizes,
...     scoring="f1_weighted",
...     n_jobs=4,
...     ax=ax,
... )
>>> lc_viz.fit(X, y)
>>> lc_viz.poof()
>>> fig.savefig("images/mlpr_0306.png")

这个学习曲线显示随着我们增加更多的训练样本,我们的交叉验证(测试)得分似乎在提高。

图 3-6. 这个学习曲线显示,随着我们添加更多的训练样本,我们的交叉验证(测试)分数似乎在改善。

部署模型

使用 Python 的pickle模块,我们可以持久化模型并加载它们。一旦我们有了一个模型,我们调用.predict方法来获得分类或回归结果:

>>> import pickle
>>> pic = pickle.dumps(rf5)
>>> rf6 = pickle.loads(pic)
>>> y_pred = rf6.predict(X_test)
>>> roc_auc_score(y_test, y_pred)
0.7747781065088757

使用Flask部署预测的 Web 服务非常普遍。现在有其他商业和开源产品推出,支持部署。其中包括ClipperPipeline,和Google 的云机器学习引擎

¹ 即使我们不直接调用这个库,当我们加载一个 Excel 文件时,pandas 在后台利用它。

第四章:缺失数据

我们需要处理缺失数据。前一章节展示了一个例子。本章将更深入地探讨。如果数据缺失,大多数算法将无法工作。值得注意的例外是最近的增强库:XGBoost、CatBoost 和 LightGBM。

和许多机器学习中的其他事物一样,如何处理缺失数据没有硬性答案。此外,缺失数据可能代表不同的情况。想象一下,人口普查数据返回,一个年龄特征被报告为缺失。这是因为样本不愿透露他们的年龄?他们不知道他们的年龄?问问题的人甚至忘记询问年龄?缺失的年龄是否有模式?它是否与另一个特征相关?它是否完全是随机的?

处理缺失数据的方法有多种:

  • 删除任何带有缺失数据的行。

  • 删除任何带有缺失数据的列。

  • 填补缺失值

  • 创建一个指示列来表示数据缺失

检查缺失数据

让我们回到泰坦尼克号的数据。因为 Python 将 TrueFalse 分别视为 10,我们可以在 pandas 中利用这一技巧来获取缺失数据的百分比:

>>> df.isnull().mean() * 100
pclass        0.000000
survived      0.000000
name          0.000000
sex           0.000000
age          20.091673
sibsp         0.000000
parch         0.000000
ticket        0.000000
fare          0.076394
cabin        77.463713
embarked      0.152788
boat         62.872422
body         90.756303
home.dest    43.086325
dtype: float64

要可视化缺失数据的模式,可以使用 missingno 库。该库对于查看连续的缺失数据区域非常有用,这表明缺失数据不是随机的(见 图 4-1)。matrix 函数在右侧包括一个火花线。这里的模式也表明非随机的缺失数据。您可能需要限制样本数量以便看到这些模式:

>>> import missingno as msno
>>> ax = msno.matrix(orig_df.sample(500))
>>> ax.get_figure().savefig("images/mlpr_0401.png")

数据缺失的位置。作者看不出明显的模式。

图 4-1. 数据缺失的位置。作者看不出明显的模式。

我们可以使用 pandas 创建一个缺失数据计数的条形图(见 图 4-2):

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> (1 - df.isnull().mean()).abs().plot.bar(ax=ax)
>>> fig.savefig("images/mlpr_0402.png", dpi=300)

使用 pandas 的非缺失数据百分比。船和身体有漏洞,所以我们应该忽略它们。有些年龄缺失很有趣。

图 4-2. 使用 pandas 的非缺失数据百分比。船和身体有漏洞,所以我们应该忽略它们。有些年龄缺失很有趣。

或者使用 missingno 库创建相同的图(见 图 4-3):

>>> ax = msno.bar(orig_df.sample(500))
>>> ax.get_figure().savefig("images/mlpr_0403.png")

使用 missingno 的非缺失数据百分比。

图 4-3. 使用 missingno 的非缺失数据百分比。

我们可以创建一个热力图,显示数据缺失的相关性(见 图 4-4)。在这种情况下,看起来数据缺失的位置并没有相关性:

>>> ax = msno.heatmap(df, figsize=(6, 6))
>>> ax.get_figure().savefig("/tmp/mlpr_0404.png")

缺失数据与 missingno 的相关性。

图 4-4. 缺失数据与 missingno 的相关性。

我们可以创建一个树状图,显示数据缺失的聚类情况(见 图 4-5)。处于同一水平的叶子预测彼此的存在(空或填充)。垂直臂用于指示不同聚类的差异程度。短臂意味着分支相似:

>>> ax = msno.dendrogram(df)
>>> ax.get_figure().savefig("images/mlpr_0405.png")

缺失数据的树状图与 missingno。我们可以看到右上角没有缺失数据的列。

图 4-5. 缺失数据的树状图与 missingno。我们可以看到右上角没有缺失数据的列。

删除缺失数据

pandas 库可以使用.dropna方法删除所有带有缺失数据的行:

>>> df1 = df.dropna()

要删除列,我们可以注意哪些列缺失,并使用.drop方法。可以传入一个列名列表或单个列名:

>>> df1 = df.drop(columns="cabin")

或者,我们可以使用.dropna方法,并设置axis=1(沿着列轴删除):

>>> df1 = df.dropna(axis=1)

谨慎处理删除数据。我通常把这看作是最后的选择。

填补数据

一旦你有一个预测数据的工具,你可以用它来预测缺失数据。定义缺失值的值的一般任务称为填充

如果你在填补数据,你需要建立一个流水线,并且在模型创建和预测时使用相同的填补逻辑。scikit-learn 中的 SimpleImputer 类将处理平均值、中位数和最常见的特征值。

默认行为是计算平均值:

>>> from sklearn.impute import SimpleImputer
>>> num_cols = df.select_dtypes(
...     include="number"
... ).columns
>>> im = SimpleImputer()  # mean
>>> imputed = im.fit_transform(df[num_cols])

提供strategy='median'strategy='most_frequent'以将替换值更改为中位数或最常见值。如果你希望用常数值填充,比如-1,可以使用strategy='constant'fill_value=-1结合使用。

小贴士

在 pandas 中,你可以使用.fillna方法来填补缺失值。确保不要泄漏数据。如果你使用平均值进行填充,请确保在模型创建和预测时使用相同的平均值。

最频繁和常量策略可以用于数字或字符串数据。平均值和中位数需要数字数据。

fancyimpute 库实现了许多算法并遵循 scikit-learn 接口。遗憾的是,大多数算法是传导的,这意味着你不能在拟合算法后单独调用.transform方法。IterativeImputer归纳的(已从 fancyimpute 迁移到 scikit-learn)并支持在拟合后进行转换。

添加指示列

数据本身的缺失可能为模型提供一些信号。pandas 库可以添加一个新列来指示缺失值:

>>> def add_indicator(col):
...     def wrapper(df):
...         return df[col].isna().astype(int)
...
...     return wrapper

>>> df1 = df.assign(
...     cabin_missing=add_indicator("cabin")
... )

第五章:清理数据

我们可以使用通用工具如 pandas 和专业工具如 pyjanitor 来帮助清理数据。

列名

在使用 pandas 时,使用 Python 友好的列名可以进行属性访问。pyjanitor 的 clean_names 函数将返回一个列名为小写并用下划线替换空格的 DataFrame:

>>> import janitor as jn
>>> Xbad = pd.DataFrame(
...     {
...         "A": [1, None, 3],
...         "  sales numbers ": [20.0, 30.0, None],
...     }
... )
>>> jn.clean_names(Xbad)
 a  _sales_numbers_
0  1.0             20.0
1  NaN             30.0
2  3.0              NaN
提示

我建议使用索引赋值、.assign 方法、.loc.iloc 赋值来更新列。我还建议不要使用属性赋值来更新 pandas 中的列。由于可能会覆盖同名列的现有方法,属性赋值不能保证可靠地工作。

pyjanitor 库很方便,但不能去除列周围的空白。我们可以使用 pandas 更精细地控制列的重命名:

>>> def clean_col(name):
...     return (
...         name.strip().lower().replace(" ", "_")
...     )

>>> Xbad.rename(columns=clean_col)
 a  sales_numbers
0  1.0           20.0
1  NaN           30.0
2  3.0            NaN

替换缺失值

pyjanitor 中的 coalesce 函数接受一个 DataFrame 和一个要考虑的列列表。这类似于 Excel 和 SQL 数据库中的功能。它返回每行的第一个非空值:

>>> jn.coalesce(
...     Xbad,
...     columns=["A", "  sales numbers "],
...     new_column_name="val",
... )
 val
0   1.0
1  30.0
2   3.0

如果我们想要用特定值填充缺失值,可以使用 DataFrame 的 .fillna 方法:

>>> Xbad.fillna(10)
 A    sales numbers
0   1.0              20.0
1  10.0              30.0
2   3.0              10.0

或者 pyjanitor 的 fill_empty 函数:

>>> jn.fill_empty(
...     Xbad,
...     columns=["A", "  sales numbers "],
...     value=10,
... )
 A    sales numbers
0   1.0              20.0
1  10.0              30.0
2   3.0              10.0

经常情况下,我们会使用更精细的方法在 pandas、scikit-learn 或 fancyimpute 中执行每列的空值替换。

在创建模型之前,可以使用 pandas 来进行健全性检查,确保处理了所有的缺失值。以下代码在 DataFrame 中返回一个布尔值,用于检查是否有任何缺失的单元格:

>>> df.isna().any().any()
True

第六章:探索

有人说,将一个专家培训成数据科学家比反过来要容易得多。我不完全同意这一观点,但数据确实有细微差别,专家可以帮助分析这些差异。通过理解业务和数据,他们能够创建更好的模型并对业务产生更大的影响。

在创建模型之前,我将进行一些探索性数据分析。这不仅让我对数据有所了解,还是与控制数据的业务单元会面并讨论问题的好借口。

数据大小

再次提醒,我们在这里使用泰坦尼克号数据集。pandas 的 .shape 属性会返回行数和列数的元组:

>>> X.shape
(1309, 13)

我们可以看到这个数据集有 1,309 行和 13 列。

汇总统计

我们可以使用 pandas 获取数据的汇总统计信息。.describe 方法还将给出非 NaN 值的计数。让我们查看第一列和最后一列的结果:

>>> X.describe().iloc[:, [0, -1]]
 pclass   embarked_S
count  1309.000000  1309.000000
mean     -0.012831     0.698243
std       0.995822     0.459196
min      -1.551881     0.000000
25%      -0.363317     0.000000
50%       0.825248     1.000000
75%       0.825248     1.000000
max       0.825248     1.000000

计数行告诉我们这两列都是填充的。没有缺失值。我们还有均值、标准差、最小值、最大值和四分位数值。

注意

pandas DataFrame 有一个 iloc 属性,可以根据索引位置进行索引操作。它允许我们通过标量、列表或切片传递行位置,然后我们可以添加逗号并传递列位置作为标量、列表或切片。

这里我们提取第二行和第五行以及最后三列:

>>> X.iloc[[1, 4], -3:]
 sex_male  embarked_Q  embarked_S
677       1.0           0           1
864       0.0           0           1

还有一个 .loc 属性,我们可以根据名称而不是位置输出行和列。这是 DataFrame 的相同部分:

>>> X.loc[[677, 864], "sex_male":]
 sex_male  embarked_Q  embarked_S
677       1.0           0           1
864       0.0           0           1

直方图

直方图是可视化数值数据的强大工具。您可以看到有多少个模式,并查看分布(见 图 6-1)。pandas 库有一个 .plot 方法来显示直方图:

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> X.fare.plot(kind="hist", ax=ax)
>>> fig.savefig("images/mlpr_0601.png", dpi=300)

Pandas 直方图。

图 6-1. Pandas 直方图。

使用 seaborn 库,我们可以绘制一个连续值的直方图,并与目标变量进行对比(见 图 6-2):

fig, ax = plt.subplots(figsize=(12, 8))
mask = y_train == 1
ax = sns.distplot(X_train[mask].fare, label='survived')
ax = sns.distplot(X_train[~mask].fare, label='died')
ax.set_xlim(-1.5, 1.5)
ax.legend()
fig.savefig('images/mlpr_0602.png', dpi=300, bbox_inches='tight')

Seaborn 直方图。

图 6-2. Seaborn 直方图。

散点图

散点图显示了两个数值列之间的关系(见 图 6-3)。同样,使用 pandas 很容易。如果数据重叠,调整 alpha 参数:

>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> X.plot.scatter(
...     x="age", y="fare", ax=ax, alpha=0.3
... )
>>> fig.savefig("images/mlpr_0603.png", dpi=300)

Pandas 散点图。

图 6-3. Pandas 散点图。

这两个特征之间似乎没有太多的相关性。我们可以使用 .corr 方法在两个(pandas)列之间进行皮尔逊相关性分析来量化相关性:

>>> X.age.corr(X.fare)
0.17818151568062093

联合绘图

Yellowbrick 有一个更复杂的散点图,包括边缘的直方图以及一个称为 联合绘图 的回归线(见 图 6-4):

>>> from yellowbrick.features import (
...     JointPlotVisualizer,
... )
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> jpv = JointPlotVisualizer(
...     feature="age", target="fare"
... )
>>> jpv.fit(X["age"], X["fare"])
>>> jpv.poof()
>>> fig.savefig("images/mlpr_0604.png", dpi=300)

Yellowbrick 联合绘图。

图 6-4. Yellowbrick 联合绘图。
警告

在这个 .fit 方法中,Xy 分别指代一列。通常情况下,X 是一个 DataFrame,而不是一个 Series。

您还可以使用 seaborn 库创建一个联合图(见图 6-5):

>>> from seaborn import jointplot
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> p = jointplot(
...     "age", "fare", data=new_df, kind="reg"
... )
>>> p.savefig("images/mlpr_0605.png", dpi=300)

Seaborn 联合图。

图 6-5. Seaborn 联合图。

对角网格

seaborn 库可以创建一个对角网格(见图 6-6)。这个图是列和核密度估计的矩阵。要通过 DataFrame 的某一列进行着色,可以使用 hue 参数。通过目标变量进行着色,我们可以看到特征对目标的不同影响:

>>> from seaborn import pairplot
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> vars = ["pclass", "age", "fare"]
>>> p = pairplot(
...     new_df, vars=vars, hue="target", kind="reg"
... )
>>> p.savefig("images/mlpr_0606.png", dpi=300)

Seaborn 对角网格。

图 6-6. Seaborn 对角网格。

箱线图和小提琴图

Seaborn 提供了多种绘制分布的图形。我们展示了箱线图和小提琴图的示例(见图 6-7 和 图 6-8)。这些图形可以将一个特征与目标变量可视化:

>>> from seaborn import box plot
>>> fig, ax = plt.subplots(figsize=(8, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> boxplot(x="target", y="age", data=new_df)
>>> fig.savefig("images/mlpr_0607.png", dpi=300)

Seaborn 箱线图。

图 6-7. Seaborn 箱线图。

小提琴图可以帮助可视化分布:

>>> from seaborn import violinplot
>>> fig, ax = plt.subplots(figsize=(8, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> violinplot(
...     x="target", y="sex_male", data=new_df
... )
>>> fig.savefig("images/mlpr_0608.png", dpi=300)

Seaborn 小提琴图。

图 6-8. Seaborn 小提琴图。

比较两个序数值

这是 pandas 代码,用于比较两个序数类别。我将年龄分为十个分位数,将 pclass 分为三个区间。该图被归一化,以填充所有垂直区域。这使得很容易看出,在第 40% 分位数中,大多数票是第三等舱的(见图 6-9):

>>> fig, ax = plt.subplots(figsize=(8, 6))
>>> (
...     X.assign(
...         age_bin=pd.qcut(
...             X.age, q=10, labels=False
...         ),
...         class_bin=pd.cut(
...             X.pclass, bins=3, labels=False
...         ),
...     )
...     .groupby(["age_bin", "class_bin"])
...     .size()
...     .unstack()
...     .pipe(lambda df: df.div(df.sum(1), axis=0))
...     .plot.bar(
...         stacked=True,
...         width=1,
...         ax=ax,
...         cmap="viridis",
...     )
...     .legend(bbox_to_anchor=(1, 1))
... )
>>> fig.savefig(
...     "image/mlpr_0609.png",
...     dpi=300,
...     bbox_inches="tight",
... )
注意

这些行:

.groupby(["age_bin", "class_bin"])
.size()
.unstack()

可以用以下内容替换:

.pipe(lambda df: pd.crosstab(
 df.age_bin, df.class_bin)
)

在 pandas 中,通常有多种方法可以完成某项任务,还有一些辅助函数可组合其他功能,如 pd.crosstab

比较序数值。

图 6-9. 比较序数值。

相关性

Yellowbrick 可以在特征之间创建成对比较(见图 6-10)。此图显示皮尔逊相关性(algorithm 参数也接受 'spearman''covariance'):

>>> from yellowbrick.features import Rank2D
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> pcv = Rank2D(
...     features=X.columns, algorithm="pearson"
... )
>>> pcv.fit(X, y)
>>> pcv.transform(X)
>>> pcv.poof()
>>> fig.savefig(
...     "images/mlpr_0610.png",
...     dpi=300,
...     bbox_inches="tight",
... )

用 Yellowbrick 创建的协方差图。

图 6-10. 用 Yellowbrick 创建的协方差相关性。

seaborn 库中还有类似的热图(见图 6-11)。我们需要将相关性 DataFrame 作为数据传入。遗憾的是,除非矩阵中的值允许,或者我们添加 vminvmax 参数,否则颜色条不会跨越 -1 到 1:

>>> from seaborn import heatmap
>>> fig, ax = plt.subplots(figsize=(8, 8))
>>> ax = heatmap(
...     X.corr(),
...     fmt=".2f",
...     annot=True,
...     ax=ax,
...     cmap="RdBu_r",
...     vmin=-1,
...     vmax=1,
... )
>>> fig.savefig(
...     "images/mlpr_0611.png",
...     dpi=300,
...     bbox_inches="tight",
... )

Seaborn 热图。

图 6-11. Seaborn 热图。

pandas 库也可以提供 DataFrame 列之间的相关性。我们只显示结果的前两列。默认方法是 'pearson',但也可以将 method 参数设置为 'kendall''spearman' 或一个返回两列之间浮点数的自定义可调用函数:

>>> X.corr().iloc[:, :2]
 pclass       age
pclass      1.000000 -0.440769
age        -0.440769  1.000000
sibsp       0.060832 -0.292051
parch       0.018322 -0.174992
fare       -0.558831  0.177205
sex_male    0.124617  0.077636
embarked_Q  0.230491 -0.061146
embarked_S  0.096335 -0.041315

高度相关的列并不增加价值,反而可能影响特征重要性和回归系数的解释。以下是查找相关列的代码。在我们的数据中,没有任何高度相关的列(记住我们已删除 sex_male 列)。

如果我们有相关的列,我们可以选择从特征数据中删除 level_0 或 level_1 中的列之一:

>>> def correlated_columns(df, threshold=0.95):
...     return (
...         df.corr()
...         .pipe(
...             lambda df1: pd.DataFrame(
...                 np.tril(df1, k=-1),
...                 columns=df.columns,
...                 index=df.columns,
...             )
...         )
...         .stack()
...         .rename("pearson")
...         .pipe(
...             lambda s: s[
...                 s.abs() > threshold
...             ].reset_index()
...         )
...         .query("level_0 not in level_1")
...     )

>>> correlated_columns(X)
Empty DataFrame
Columns: [level_0, level_1, pearson]
Index: []

使用具有更多列的数据集,我们可以看到许多列之间存在相关性:

>>> c_df = correlated_columns(agg_df)
>>> c_df.style.format({"pearson": "{:.2f}"})
 level_0     level_1   pearson
3   pclass_mean      pclass  1.00
4   pclass_mean  pclass_min  1.00
5   pclass_mean  pclass_max  1.00
6    sibsp_mean   sibsp_max  0.97
7    parch_mean   parch_min  0.95
8    parch_mean   parch_max  0.96
9     fare_mean        fare  0.95
10    fare_mean    fare_max  0.98
12    body_mean    body_min  1.00
13    body_mean    body_max  1.00
14     sex_male  sex_female -1.00
15   embarked_S  embarked_C -0.95

RadViz

RadViz 图将每个样本显示在一个圆圈上,特征显示在圆周上(见 图 6-12)。数值被标准化,你可以想象每个图像都有一个弹簧,根据数值将样本拉向它。

这是一种用于可视化目标间可分性的技术之一。

Yellowbrick 可以做到这一点:

>>> from yellowbrick.features import RadViz
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> rv = RadViz(
...     classes=["died", "survived"],
...     features=X.columns,
... )
>>> rv.fit(X, y)
>>> _ = rv.transform(X)
>>> rv.poof()
>>> fig.savefig("images/mlpr_0612.png", dpi=300)

Yellowbrick RadViz 图。

图 6-12. Yellowbrick RadViz 图。

pandas 库也可以绘制 RadViz 图(见 图 6-13):

>>> from pandas.plotting import radviz
>>> fig, ax = plt.subplots(figsize=(6, 6))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> radviz(
...     new_df, "target", ax=ax, colormap="PiYG"
... )
>>> fig.savefig("images/mlpr_0613.png", dpi=300)

Pandas RadViz 图。

图 6-13. Pandas RadViz 图。

平行坐标

对于多变量数据,您可以使用平行坐标图来直观地查看聚类情况(见 图 6-14 和 图 6-15)。

再次,这里是 Yellowbrick 版本:

>>> from yellowbrick.features import (
...     ParallelCoordinates,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> pc = ParallelCoordinates(
...     classes=["died", "survived"],
...     features=X.columns,
... )
>>> pc.fit(X, y)
>>> pc.transform(X)
>>> ax.set_xticklabels(
...     ax.get_xticklabels(), rotation=45
... )
>>> pc.poof()
>>> fig.savefig("images/mlpr_0614.png", dpi=300)

Yellowbrick 平行坐标图。

图 6-14. Yellowbrick 平行坐标图。

还有一个 pandas 版本:

>>> from pandas.plotting import (
...     parallel_coordinates,
... )
>>> fig, ax = plt.subplots(figsize=(6, 4))
>>> new_df = X.copy()
>>> new_df["target"] = y
>>> parallel_coordinates(
...     new_df,
...     "target",
...     ax=ax,
...     colormap="viridis",
...     alpha=0.5,
... )
>>> ax.set_xticklabels(
...     ax.get_xticklabels(), rotation=45
... )
>>> fig.savefig(
...     "images/mlpr_0615.png",
...     dpi=300,
...     bbox_inches="tight",
... )

Pandas 平行坐标图。

图 6-15. Pandas 平行坐标图。

第七章:数据预处理

本章将探讨使用此数据的常见预处理步骤:

>>> X2 = pd.DataFrame(
...     {
...         "a": range(5),
...         "b": [-100, -50, 0, 200, 1000],
...     }
... )
>>> X2
 a     b
0  0  -100
1  1   -50
2  2     0
3  3   200
4  4  1000

标准化

某些算法(如 SVM)在数据标准化时表现更好。每列的均值应为 0,标准偏差应为 1。Sklearn 提供了一个.fit_transform方法,结合了.fit.transform

>>> from sklearn import preprocessing
>>> std = preprocessing.StandardScaler()
>>> std.fit_transform(X2)
array([[-1.41421356, -0.75995002],
 [-0.70710678, -0.63737744],
 [ 0\.        , -0.51480485],
 [ 0.70710678, -0.02451452],
 [ 1.41421356,  1.93664683]])

拟合后,我们可以检查各种属性:

>>> std.scale_
array([  1.41421356, 407.92156109])
>>> std.mean_
array([  2., 210.])
>>> std.var_
array([2.000e+00, 1.664e+05])

这是一个 pandas 版本。请记住,如果用于预处理,您需要跟踪原始的均值和标准偏差。稍后用于预测的任何样本都需要使用这些相同的值进行标准化:

>>> X_std = (X2 - X2.mean()) / X2.std()
>>> X_std
 a         b
0 -1.264911 -0.679720
1 -0.632456 -0.570088
2  0.000000 -0.460455
3  0.632456 -0.021926
4  1.264911  1.732190

>>> X_std.mean()
a    4.440892e-17
b    0.000000e+00
dtype: float64

>>> X_std.std()
a    1.0
b    1.0
dtype: float64

fastai 库也实现了这一功能:

>>> X3 = X2.copy()
>>> from fastai.structured import scale_vars
>>> scale_vars(X3, mapper=None)
>>> X3.std()
a    1.118034
b    1.118034
dtype: float64
>>> X3.mean()
a    0.000000e+00
b    4.440892e-17
dtype: float64

缩放到范围

缩放到范围是将数据转换为 0 到 1 之间的值。限制数据的范围可能是有用的。但是,如果您有异常值,可能需要谨慎使用此方法:

>>> from sklearn import preprocessing
>>> mms = preprocessing.MinMaxScaler()
>>> mms.fit(X2)
>>> mms.transform(X2)
array([[0\.     , 0\.     ],
 [0.25   , 0.04545],
 [0.5    , 0.09091],
 [0.75   , 0.27273],
 [1\.     , 1\.     ]])

这是一个 pandas 版本:

>>> (X2 - X2.min()) / (X2.max() - X2.min())
 a         b
0  0.00  0.000000
1  0.25  0.045455
2  0.50  0.090909
3  0.75  0.272727
4  1.00  1.000000

虚拟变量

我们可以使用 pandas 从分类数据创建虚拟变量。这也称为独热编码或指示编码。如果数据是名义(无序)的,虚拟变量特别有用。pandas 中的get_dummies函数为分类列创建多个列,每列中的值为 1 或 0,如果原始列有该值:

>>> X_cat = pd.DataFrame(
...     {
...         "name": ["George", "Paul"],
...         "inst": ["Bass", "Guitar"],
...     }
... )
>>> X_cat
 name    inst
0  George    Bass
1    Paul  Guitar

这是 pandas 版本。注意drop_first选项可以用来消除一列(虚拟列中的一列是其他列的线性组合)。

>>> pd.get_dummies(X_cat, drop_first=True)
 name_Paul  inst_Guitar
0          0            0
1          1            1

pyjanitor 库还具有使用expand_column函数拆分列的功能:

>>> X_cat2 = pd.DataFrame(
...     {
...         "A": [1, None, 3],
...         "names": [
...             "Fred,George",
...             "George",
...             "John,Paul",
...         ],
...     }
... )
>>> jn.expand_column(X_cat2, "names", sep=",")
 A        names  Fred  George  John  Paul
0  1.0  Fred,George     1       1     0     0
1  NaN       George     0       1     0     0
2  3.0    John,Paul     0       0     1     1

如果我们有高基数名义数据,我们可以使用标签编码。这将在下一节介绍。

标签编码器

虚拟变量编码的替代方法是标签编码。这将把分类数据转换为数字。对于高基数数据非常有用。该编码器强加序数性,这可能是需要的也可能不需要的。它所占空间比独热编码少,而且一些(树)算法可以处理此编码。

标签编码器一次只能处理一列:

>>> from sklearn import preprocessing
>>> lab = preprocessing.LabelEncoder()
>>> lab.fit_transform(X_cat)
array([0,1])

如果您已经编码了值,则可以应用.inverse_transform方法对其进行解码:

>>> lab.inverse_transform([1, 1, 0])
array(['Guitar', 'Guitar', 'Bass'], dtype=object)

您也可以使用 pandas 进行标签编码。首先,将列转换为分类列类型,然后从中提取数值代码。

此代码将从 pandas 系列创建新的数值数据。我们使用.as_ordered方法确保分类是有序的:

>>> X_cat.name.astype(
...     "category"
... ).cat.as_ordered().cat.codes + 1
0    1
1    2
dtype: int8

频率编码

处理高基数分类数据的另一种选择是频率编码。这意味着用训练数据中的计数替换类别的名称。我们将使用 pandas 来执行此操作。首先,我们将使用 pandas 的.value_counts方法创建一个映射(将字符串映射到计数的 pandas 系列)。有了映射,我们可以使用.map方法进行编码:

>>> mapping = X_cat.name.value_counts()
>>> X_cat.name.map(mapping)
0    1
1    1
Name: name, dtype: int64

确保存储训练映射,以便以后使用相同的数据对未来数据进行编码。

从字符串中提取类别

提高 Titanic 模型准确性的一种方法是从姓名中提取称号。找到最常见的三重组的一个快速方法是使用 Counter 类:

>>> from collections import Counter
>>> c = Counter()
>>> def triples(val):
...     for i in range(len(val)):
...         c[val[i : i + 3]] += 1
>>> df.name.apply(triples)
>>> c.most_common(10)
[(', M', 1282),
 (' Mr', 954),
 ('r. ', 830),
 ('Mr.', 757),
 ('s. ', 460),
 ('n, ', 320),
 (' Mi', 283),
 ('iss', 261),
 ('ss.', 261),
 ('Mis', 260)]

我们可以看到,“Mr.” 和 “Miss.” 非常常见。

另一个选项是使用正则表达式提取大写字母后跟小写字母和句点:

>>> df.name.str.extract(
...     "([A-Za-z]+)\.", expand=False
... ).head()
0      Miss
1    Master
2      Miss
3        Mr
4       Mrs
Name: name, dtype: object

我们可以使用 .value_counts 查看这些的频率:

>>> df.name.str.extract(
...     "([A-Za-z]+)\.", expand=False
... ).value_counts()
Mr          757
Miss        260
Mrs         197
Master       61
Dr            8
Rev           8
Col           4
Mlle          2
Ms            2
Major         2
Dona          1
Don           1
Lady          1
Countess      1
Capt          1
Sir           1
Mme           1
Jonkheer      1
Name: name, dtype: int64
注意

对正则表达式的完整讨论超出了本书的范围。此表达式捕获一个或多个字母字符的组。该组后面将跟随一个句点。

使用这些操作和 pandas,您可以创建虚拟变量或将低计数的列组合到其他类别中(或将它们删除)。

其他分类编码

categorical_encoding 库 是一组 scikit-learn 转换器,用于将分类数据转换为数值数据。该库的一个优点是它支持输出 pandas DataFrame(不像 scikit-learn 那样将它们转换为 numpy 数组)。

该库实现的一个算法是哈希编码器。如果您事先不知道有多少类别,或者正在使用词袋表示文本,这将会很有用。它将分类列哈希到 n_components。如果您使用在线学习(可以更新模型),这将非常有用:

>>> import category_encoders as ce
>>> he = ce.HashingEncoder(verbose=1)
>>> he.fit_transform(X_cat)
 col_0  col_1  col_2  col_3  col_4  col_5  col_6  col_7
0      0      0      0      1      0      1      0      0
1      0      2      0      0      0      0      0      0

序数编码器可以将具有顺序的分类列转换为单个数字列。在这里,我们将大小列转换为序数数字。如果在映射字典中找不到一个值,则使用 -1 的默认值:

>>> size_df = pd.DataFrame(
...     {
...         "name": ["Fred", "John", "Matt"],
...         "size": ["small", "med", "xxl"],
...     }
... )
>>> ore = ce.OrdinalEncoder(
...     mapping=[
...         {
...             "col": "size",
...             "mapping": {
...                 "small": 1,
...                 "med": 2,
...                 "lg": 3,
...             },
...         }
...     ]
... )
>>> ore.fit_transform(size_df)
 name  size
0  Fred   1.0
1  John   2.0
2  Matt  -1.0

参考 解释了 categorical_encoding 库的许多算法。

如果您有高基数数据(大量唯一值),考虑使用其中一个贝叶斯编码器,它们会为每个分类列输出单独的列。这些包括 TargetEncoder, LeaveOneOutEncoder, WOEEncoder, JamesSteinEncoderMEstimateEncoder

例如,要将 Titanic 生存列转换为目标的后验概率和给定标题(分类)信息的先验概率的混合,可以使用以下代码:

>>> def get_title(df):
...     return df.name.str.extract(
...         "([A-Za-z]+)\.", expand=False
...     )
>>> te = ce.TargetEncoder(cols="Title")
>>> te.fit_transform(
...     df.assign(Title=get_title), df.survived
... )["Title"].head()
0    0.676923
1    0.508197
2    0.676923
3    0.162483
4    0.786802
Name: Title, dtype: float64

日期特征工程

fastai 库有一个 add_datepart 函数,它将根据日期时间列生成日期属性列。这对大多数机器学习算法很有用,因为它们无法从日期的数值表示中推断出这种类型的信号:

>>> from fastai.tabular.transform import (
...     add_datepart,
... )
>>> dates = pd.DataFrame(
...     {
...         "A": pd.to_datetime(
...             ["9/17/2001", "Jan 1, 2002"]
...         )
...     }
... )

>>> add_datepart(dates, "A")
>>> dates.T
 0           1
AYear                    2001        2002
AMonth                      9           1
AWeek                      38           1
ADay                       17           1
ADayofweek                  0           1
ADayofyear                260           1
AIs_month_end           False       False
AIs_month_start         False        True
AIs_quarter_end         False       False
AIs_quarter_start       False        True
AIs_year_end            False       False
AIs_year_start          False        True
AElapsed           1000684800  1009843200
警告

add_datepart 会改变 DataFrame,这是 pandas 能做到的,但通常不这样做!

添加 col_na 特征

fastai 库曾经有一个函数用于创建一个列以填充缺失值(使用中位数)并指示缺失值。知道一个值是否缺失可能会有一些信号。以下是该函数的副本及其使用示例:

>>> from pandas.api.types import is_numeric_dtype
>>> def fix_missing(df, col, name, na_dict):
...     if is_numeric_dtype(col):
...         if pd.isnull(col).sum() or (
...             name in na_dict
...         ):
...             df[name + "_na"] = pd.isnull(col)
...             filler = (
...                 na_dict[name]
...                 if name in na_dict
...                 else col.median()
...             )
...             df[name] = col.fillna(filler)
...             na_dict[name] = filler
...     return na_dict
>>> data = pd.DataFrame({"A": [0, None, 5, 100]})
>>> fix_missing(data, data.A, "A", {})
{'A': 5.0}
>>> data
 A   A_na
0    0.0  False
1    5.0   True
2    5.0  False
3  100.0  False

下面是一个 pandas 版本:

>>> data = pd.DataFrame({"A": [0, None, 5, 100]})
>>> data["A_na"] = data.A.isnull()
>>> data["A"] = data.A.fillna(data.A.median())

手动特征工程

我们可以使用 pandas 生成新特征。对于 Titanic 数据集,我们可以添加聚合船舱数据(每个船舱的最大年龄、平均年龄等)。要获得每个船舱的聚合数据并将其合并回原始数据中,使用 pandas 的 .groupby 方法创建数据。然后使用 .merge 方法将其与原始数据对齐:

>>> agg = (
...     df.groupby("cabin")
...     .agg("min,max,mean,sum".split(","))
...     .reset_index()
... )
>>> agg.columns = [
...     "_".join(c).strip("_")
...     for c in agg.columns.values
... ]
>>> agg_df = df.merge(agg, on="cabin")

如果你想总结“好”或“坏”列,你可以创建一个新列,该列是聚合列的总和(或另一个数学操作)。这在某种程度上是一种艺术,也需要理解数据。