编者按: 机器学习开发课程第二课, 与 Mail.Ru Search 数据分析负责人 Egor Polusmak 和 Mail.Ru Group 数据科学家 Yury Kashnitsky 一起探索如何使用 Python 可视化数据
在机器学习领域中, 可视化并不仅仅用来制作漂亮的报表项目的各个阶段都大量使用可视化技术
在开始一项新任务时, 通过可视化手段探索数据往往是任务的第一步我们通过图表汇总数据, 放弃无关紧要的细节相比直接阅读许多行原始数据, 可视化能更好地帮助人类把握数据的要点令人惊叹的是, 仅仅通过可视化工具创建一些看上去再简单不过的图表, 就能获得大量洞见
接着, 在分析模型表现和模型报告的结果时, 我们也常常使用可视化有时候, 为了理解复杂的模型, 我们需要将高维空间映射为视觉上更直观的二维或三维图形
总而言之, 可视化是一个相对快捷的从数据中获取新知的手段因此, 学习这一极为重要的技术, 并将其纳入你的日常机器学习工具箱, 是至关重要的
本文将使用 pandasmatplotlib 和 seaborn 等流行的库, 带你上手可视化
概览
数据集
单变量可视化 数量和类型分布
多变量可视化 变量间的相互作用
全数据集 一窥高维空间
作业二
下面的材料以 Jupyter notebook 的形式查看效果最佳如果你克隆了本教程的代码仓库, 你也可以在本地运行
1. 数据集
首先初始化环境:
- import numpy as np
- import pandas as pd
- pd.options.display.max_columns = 12
- # 禁用 Anaconda 警告
- import warnings
- warnings.simplefilter('ignore')
- # 在 Jupyter Notebook 内部显示图形
- %matplotlib inline
- import matplotlib.pyplot as plt
- # 我们将使用 Seaborn 库
- import seaborn as sns
- sns.set()
- # SVG 格式的图像更清晰
- %config InlineBackend.figure_format = 'svg'
- # 增加默认的绘图尺寸
- from pylab import rcParams
- rcParams['figure.figsize'] = 5, 4
在第一篇文章中, 我们使用的是某电信运营商的客户离网率数据这里我们仍旧使用这个数据集
- df = pd.read_csv('../../data/telecom_churn.csv')
- df.head()
数据集的特征:
2. 单变量可视化
单变量 (univariate) 分析一次只关注一个变量当我们独立地分析一个特征时, 我们通常最关心的是该特征的值的分布, 而忽略数据集中的其他变量
下面我们将考虑不同统计类型的变量, 以及相应的可视化工具
2.1 数量特征
数量特征 (quantitative feature) 的值为有序数值这些值可能是离散的, 例如整数, 也可能是连续的, 例如实数, 通常用于表示技术和度量
直方图和密度图
最简单的查看数值变量分布的方法是使用 DataFrame 的 hist()方法绘制它的直方图(histogram)
- features = ['Total day minutes', 'Total intl calls']
- df[features].hist(figsize=(12, 4));
直方图依照相等的范围将值分组为柱直方图的形状可能包含了数据分布的线索: 高斯指数, 等等当分布基本上很有规律, 但有一些异常值时, 你也可以通过直方图辨认出形状的歪斜之处当你使用预设某一特定分布类型 (通常是高斯) 的机器学习方法时, 知道特征值的分布是非常重要的
在以上图形中, 我们看到变量 Total day minutes(每日通话时长)呈正态分布 (译者注: 正态分布即高斯分布), 而 Total intl calls(总国际呼叫数) 显著右倾(它右侧的尾巴更长)
除了直方图, 理解分布的另一个 (经常更清楚的) 方法是密度图 (density plots), 也叫(更正式的名称) 核密度图 (Kernel Density Plots) 它们可以看成是直方图平滑过的版本相比直方图, 它们的主要优势是不依赖于柱的尺寸让我们为上面两个变量创建密度图:
- df[features].plot(kind='density', subplots=True,
- layout=(1, 2), sharex=False, figsize=(12, 4));
我们也可以使用 seaborn 的 distplot()方法绘制观测数据的分布例如, Total day minutes(每日通话时长)的分布默认情况下, 图形将同时显示直方图和核密度估计(kernel density estimation,KDE)
sns.distplot(df['Total intl calls']);
这里直方图的柱形的高度已经归一过了, 表示的是密度而不是样本数
箱形图
箱形图 (box plot) 是另一种有用的可视化图形使用 seaborn 绘制箱形图:
- _, ax = plt.subplots(figsize=(3, 4))
- sns.boxplot(data=df['Total intl calls'], ax=ax);
箱形图的主要组成部分是箱子 (box)(显然, 这是它被称为箱形图的原因), 须(whisker) 和一些单独的数据点(离群值)
箱子显示了分布的四分位距; 它的长度由 25%(Q1, 下四分位数)和 75%(Q3, 上司分位数)决定箱中的水平线表示中位数(50%)
从箱子处延伸出来的线被称为须它们表示数据点的总体散布, 具体而言, 是位于区间 (Q1 - 1.5xIQR, Q3 + 1.5xIQR) 的数据点, 其中 IQR = Q3 - Q1, 也就是四分位距
离群值是须之外的数据点, 它们作为单独的数据点, 沿着中轴绘制
我们可以看到, 在我们的数据中, 大量的国际呼叫是相当少见的
提琴形图
我们最后考虑的分布图形是提琴形图(violin plot)
下图左侧是箱形图, 右侧是提琴形图
- _, axes = plt.subplots(1, 2, sharey=True, figsize=(6, 4))
- sns.boxplot(data=df['Total intl calls'], ax=axes[0]); sns.violinplot(data=df['Total intl calls'], ax=axes[1]);
箱形图和提琴形图的区别是, 箱形图显示了单独样本的特定统计数据, 而提琴形图聚焦于平滑后的整体分布
describe()
图形工具之外, 我们可以使用 DataFrame 的 describe()方法来获取分布的精确数值统计:
df[features].describe()
describe()的输出基本上是自解释性的
2.2 类别和二元特征
类别特征 (categorical features take) 具有固定数目的值每个值将一个观测数据分配到相应的组, 这些组称为类别 (category) 类别反映了样本的某个定性属性二元 (binary) 变量是一个重要的类别变量的特例, 其中类别的可能值正好为 2. 如果类别变量的值具有顺序, 称为有序 (ordinal) 类别变量
频率表
让我们查看下数据集的分类平滑: 目标变量离网率的分布首先, 我们使用 value_counts()得到一张频率表:
- df['Churn'].value_counts()
- False 2850
- True 483
- Name: Churn, dtype: int64
默认情况下, 频率由高到低排列
在我们的例子中, 数据是失衡的, 也就是说, 数据集中忠实客户和不忠实客户的比例并不相等只有一小部分的客户取消了他们的电信服务订阅我们将在以后的文章中看到, 这一事实可能暗示衡量分类表现时存在一些限制, 以后我们可能额外惩罚我们的模型在预测少数离网分类时所犯的错误
条形图
频率表的图形化表示是条形图创建条形图最简单的方法是使用 seaborn 的 countplot()函数 seaborn 中还有一个函数, 起了一个令人困惑的名字 (barplot()),barplot() 绝大部分情况下用于表示以某个类别特征分组的数值变量的一些基本统计数据
- _, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 4))
- sns.countplot(x='Churn', data=df, ax=axes[0]);
- sns.countplot(x='Customer service calls', data=df, ax=axes[1]);
尽管条形图和上面提到的直方图看起来很像, 它们是不一样的:
直方图用于查看数值变量的分布, 而条形图用于类别特征
直方图的 X 轴是数值; 条形图的 X 轴可能是任何类型: 数字字符串布尔值
直方图的 X 轴是一根笛卡尔坐标轴; 条形的顺序没有事先定义不过, 值得注意的是, 条形经常按照高度排序, 也就是值的频率同时, 当我们考虑有序变量 (例如 Customer service calls(客服呼叫数)) 时, 条形通常按照变量的值排序
左侧的图形生动地显示了目标变量的失衡性右侧 Customer service calls(客服呼叫数)的条形图暗示了大部分客户最多打了 2-3 个客服电话就解决了他们的问题不过, 既然我们想要预测少数分类, 我们可能对少数不满意的客户的表现更感兴趣很可能条形图的尾巴包含了大部分的离网客户目前这只是假想, 让我们来看一些更有趣更强大的可视化技术
3. 多变量可视化
多变量 (multivariate) 图形让我们得以在单张图像中查看两个以上变量的联系和单变量图形一样, 可视化的类型取决于将要分析的变量的类型
3.1 数量数量
我们先来看看数量变量之间的相互作用
相关矩阵
让我们看下数据集中的数值变量的相关性这一信息很重要, 因为有一些机器学习算法 (比如, 线性回归和逻辑回归) 不能很好地处理高度相关的输入变量
首先, 我们使用 DataFrame 的 corr()方法计算出每对特征间的相关性接着, 我们将所得的相关矩阵 (correlation matrix) 传给 seaborn 的 heatmap()方法, 该方法根据提供的数值, 渲染出一个基于色彩编码的矩阵:
- # 丢弃非数值变量
- numerical = list(set(df.columns) -
- set(['State', 'International plan',
- 'Voice mail plan', 'Area code', 'Churn',
- 'Customer service calls']))
- # 计算和绘图
- corr_matrix = df[numerical].corr()
- sns.heatmap(corr_matrix);
从上图我们可以看到, Total day charge(日话费总额)直接基于电话的分钟数计算得到(Total day minutes), 这样的变量有 4 个这 4 个变量称为因变量(dependent variable), 可以直接去除, 因为它们并不贡献任何额外信息让我们去掉它们:
- numerical = list(set(numerical) -
- set(['Total day charge', 'Total eve charge',
- 'Total night charge', 'Total intl charge']))
散点图
散点图 (scatter plot) 将两个数值变量的值显示为二位空间中的笛卡尔坐标 (Cartesian coordinate) 还有三维的散点图
让我们试下 matplotlib 库的 scatter()方法:
plt.scatter(df['Total day minutes'], df['Total night minutes']);
我们得到了两个正态分布变量的散点图, 这张图没什么意思看起来这两个变量并不相关, 因为类似椭圆的形状和轴是对齐的
seaborn 库创建的散点图有一个略微奇特的选项:
- sns.jointplot(x='Total day minutes', y='Total night minutes',
- data=df, kind='scatter');
jointplot()函数绘制了两张直方图, 某些情形下它们可能会有用这一函数还可以让我们绘制平滑过的 joint plot:
- sns.jointplot('Total day minutes', 'Total night minutes',
- data=df, kind="kde", color="g");
这个基本上是我们之前讨论过的核密度图的双变量版本
散点图矩阵
在某些情形下, 我们可能想要绘制如下所示的散点图矩阵 (scatterplot matrix) 它的对角线包含变量的分布, 而每对变量的散点图填充了矩阵的其余部分
- # 使用 SVG 格式可能导致 pairplot 变得非常慢
- %config InlineBackend.figure_format = 'png'
- sns.pairplot(df[numerical]);
有时候, 这样的可视化可能帮我们从数据中得出一些结论
3.2 数量类别
在这一小节中, 让我们的图形更有趣一点我们将尝试从数值和类别特征的相互作用中得到离网预测的新洞见
更具体地, 让我看看输入变量和目标变量离网的关系
先前我们了解了散点图散点图中的数据点可以通过色彩或尺寸进行编码, 以便在同一张图像中包括第三个类别变量的值我们可以通过之前的 scatter()函数达成这一点, 不过, 这次让我们换换花样, 用 lmplot()函数的 hue 参数来指定感兴趣的类别特征:
- sns.lmplot('Total day minutes', 'Total night minutes', data=df,
- hue='Churn', fit_reg=False);
看起来占少数的不忠实客户偏向右上角; 也就是倾向于在白天和夜间打更多电话的客户但这不是非常明显, 我们也不会基于这一图形下任何确定的结论
现在, 让我们创建箱形图, 以可视化两个互斥分组中的数值变量分布的统计数据: 忠实客户 (Churn=False) 和离网客户(Churn=True)
- # 有时我们可以将有序变量作为数值变量分析
- numerical.append('Customer service calls')
- fig, axes = plt.subplots(nrows=3, ncols=4, figsize=(10, 7))
- for idx, feat in enumerate(numerical):
- ax = axes[int(idx / 4), idx % 4]
- sns.boxplot(x='Churn', y=feat, data=df, ax=ax)
- ax.set_xlabel('')
- ax.set_ylabel(feat)
- fig.tight_layout();
从这一图表中, 我们可以看到, 两组之间分歧最大的分布是这三个变量: Total day minutes(日通话分钟数)Customer service calls(客服呼叫数)Number vmail messages(语音邮件数)在后续的课程中, 我们将学习如何使用随机森林 (Random Forest) 或梯度提升 (Gradient Boosting) 来判定特征对分类的重要性; 那时我们将看到, 前两个特征对于离网预测而言确实非常重要
让我们分别看下忠实客户和不忠实客户的日通话分钟数我们将创建箱形图和提琴形图
- _, axes = plt.subplots(1, 2, sharey=True, figsize=(10, 4))
- sns.boxplot(x='Churn', y='Total day minutes',
- data=df, ax=axes[0]);
- sns.violinplot(x='Churn', y='Total day minutes',
- data=df, ax=axes[1]);
在这一情形下, 提琴形图并没有提供关于数据的额外信息, 因为箱形图已经告诉了我们一切: 不忠实客户倾向于打更多的电话
一个有趣的观察: 平均而言, 终止他们的协议的客户是通讯服务更活跃的用户也许他们对话费不满意, 所以预防离网的一个可能措施是降低通话费率公司需要进行额外的经济分析, 以查明这样的措施是否有利
当我们想要一次分析两个类别维度下的数量变量时, 可以用 seaborn 库的 factorplot()函数例如, 在同一图形中可视化 Total day minutes(日通话分钟数)和两个类别变量的相互作用:
- sns.factorplot(x='Churn', y='Total day minutes',
- col='Customer service calls',
- data=df[df['Customer service calls'] < 8],
- kind="box", col_wrap=4, size=3, aspect=.8);
从上图我们可以总结出, 从 4 次呼叫开始, Total day minutes(日通话分钟数)可能不再是客户离网的主要因素也许, 除了我们之前猜测的话费, 有些客户因为其他问题对服务不满意, 或许这导致了日通话分钟数较少
3.3 类别类别
正如我们之前提到的, 变量 Customer service calls(客服呼叫数)的唯一值极少, 因此, 既可以看成数值变量, 也可以看成有序类别变量我们已经通过计数图 (countc plot) 查看过它的分布了现在我们感兴趣的是这一有序特征和目标变量 Churn(离网)之间的关系
让我们再一次使用计数图看下客服呼叫数的分布这次, 我们同时传入 hue=Churn 参数, 以便在图形中加入类别维度:
sns.countplot(x='Customer service calls', hue='Churn', data=df);
观察: 呼叫客服达到 4 次以上后, 离网率显著增加了
现在让我们看下 Churn(离网)和二元特征 International plan(国际套餐)Voice mail plan(语音邮件套餐)的关系
- _, axes = plt.subplots(1, 2, sharey=True, figsize=(10, 4))
- sns.countplot(x='International plan', hue='Churn',
- data=df, ax=axes[0]);
- sns.countplot(x='Voice mail plan', hue='Churn',
- data=df, ax=axes[1]);
观察: 开通国际套餐后, 离网率会高很多; 使用国际套餐是一个强烈的特征我们在语音邮件套餐上没有观察到相同的效应
列联表
除了使用图形进行类别分析之外, 还可以使用统计学的传统工具: 列联表(contingency table), 又称为交叉制表(cross tabulation), 使用表格形式表示多个类别变量的频率分布特别地, 它让我们可以通过查看一列或一行来得知某个变量在另一变量的作用下的分布
让我们通过交叉制表看看 Churn(离网)和类别变量 State(州)的关系:
pd.crosstab(df['State'], df['Churn']).T
State(州)的不同值很多: 51. 我们看到每个周只有少量数据点每个州只有 3 到 17 个客户抛弃了运营商让我们暂时忽略这一点, 计算每个州的离网率, 由高到低排列:
- df.groupby(['State'])['Churn'].
- agg([np.mean]).
- sort_values(by='mean', ascending=False).T
乍看起来, 新泽西和加利福尼亚的离网率超过了 25%, 夏威夷和阿拉斯加的离网率则不到 6%. 然而, 这些结论是基于极少的样本得出的, 我们的观察可能仅仅是这一特定数据集的性质我们可以通过 Matthews 和 Cramer 相关性假说确认这一点, 不过这个超出了这篇文章的范围
4. 全数据集
4.1 幼稚方法
上面我们查看了数据集的不同刻面(facet), 猜测感兴趣的特征, 每次选择其中的一小部分进行可视化我们一次仅仅处理两到三个变量, 能比较容易地观察到数据的结构和关系但是, 如果我们想一下子显示所有特征呢? 如何确保最终的可视化仍然是可解释的?
我们可以为整个数据集使用 hist()或者 pairplot()方法, 同时查看所有的特征不过, 当特征数目足够多的时候, 这样的可视化分析很快就变得缓慢和低效另外, 我们其实仍然可以成对地分析变量, 而不用一下子分析所有变量
4.2 降维
大多数现实世界的数据集有很多特征, 有时有上万个特征每一个特征都可以被看成数据点空间的一维因此, 我们经常需要处理高维数据集, 可视化整个高维数据集相当难
为了从整体上查看一个数据集, 我们需要在不损失很多数据信息的前提下, 降低用于可视化的维度这一任务称为降维 (dimensionality reduction) 降维是一个无监督学习 (unsupervised learning) 问题, 因为我们需要在不借助任何监督输入的前提下, 从数据自身得到新的低维特征
主成分分析 (Principal Component Analysis, PCA) 是一个著名的降维方法, 我们会在之后的课程中讨论它主成分分析有一个限制, 它是线性 (linear) 算法, 因而对数据有某些特定的限制
有许多非线性方法, 统称流形学习 (Manifold Learning) 最著名的流形学习方法之一是 t-SNE
4.3 t-SNE
让我们为离网数据创建一个 t-SNE 表示
这一方法的名字看起来很复杂, 有些吓人: t 分布随机近邻嵌入 (t-distributed Stohastic Neighbor Embedding) 它的数学也很令人印象深刻 (我们不会在这里深究数学, 勇敢的读者可以阅读 Laurens van der Maaten 和 Geoffrey Hinton 在 JMLR 上发表的原论文) 它的基本思路很简单: 为高维特征空间在二维平面 (或三维超平面, 不过基本上总是使用二维空间) 上寻找一个投影, 使得在原本的 n 维空间中相距很远的数据点在屏幕上同样相距较远而原本相近的点在平面上仍然相近
本质上, 近邻嵌入寻找保留了样本的邻居关系的新的维度较低的数据表示
现在让我们做些练习首先, 加载类:
- from sklearn.manifold import TSNE
- from sklearn.preprocessing import StandardScaler
我们去除 State(州)和离网 (Churn) 变量, 然后用
pandas.Series.map()
方法将二元特征的 Yes/No 转换成数值:
- X = df.drop(['Churn', 'State'], axis=1)
- X['International plan'] = X['International plan'].
- map({'Yes': 1, 'No': 0})
- X['Voice mail plan'] = X['Voice mail plan'].
- map({'Yes': 1, 'No': 0})
我们同样需要归一化数据我们从每个变量中减去均值, 然后除以标准差这些都可以使用 StandardScaler 来完成
- scaler = StandardScaler()
- X_scaled = scaler.fit_transform(X)
现在可以构建 t-SNE 表示了:
- tsne = TSNE(random_state=17)
- tsne_repr = tsne.fit_transform(X_scaled)
然后可视化它的图形:
plt.scatter(tsne_repr[:, 0], tsne_repr[:, 1]);
让我们根据离网情况给 t-SNE 表示加上色彩(绿色表示忠实用户, 红色表示不忠实用户)
- plt.scatter(tsne_repr[:, 0], tsne_repr[:, 1],
- c=df['Churn'].map({False: 'green', True: 'red'}));
我们可以看到, 离网的客户集中在低维特征空间的一小部分区域
为了更好地理解这一图像, 我们可以使用剩下的两个二元特征给图像着色: International plan(国际套餐)和 Voice mail plan(语音邮件套餐)绿色代表相应的二元特征是正值
- _, axes = plt.subplots(1, 2, sharey=True, figsize=(12, 5))
- for i, name in enumerate(['International plan', 'Voice mail plan']):
- axes[i].scatter(tsne_repr[:, 0], tsne_repr[:, 1],
- c=df[name].map({'Yes': 'green', 'No': 'red'}))
- axes[i].set_title(name)
现在很清楚了, 许多退订的不满意客户集中在西南聚类(表示开通了国际套餐但没有开通语音邮件套餐)
最后, 让我们了解下 t-SNE 的缺陷:
高计算复杂度 scikit-learn 的实现在真实任务中往往不太管用如果你有大量样本, 你应该转而使用 Multicore-TSNE(多核)
随机数种子的不同会导致图形大不相同, 这给解释带来了困难请参考文末相关资源给出的 t-SNE 教程通常而言, 你不应该基于这些图像做出任何深远的结论, 因为它可能和单纯的猜测差不多当然, t-SNE 图像中的某些发现可能会启发一个想法, 这个想法可以通过更全面深入的研究得到确认, 但这并不经常发生
偶尔, t-SNE 可以让你从数据中得到非常好的直觉下面的论文展示了一个这样的例子: Visualizing MNIST(可视化 MNIST)
有时 t-SNE 真的能够帮助你更好地理解数据, 有时 t-SNE 能够帮助你画出圣诞树玩具 :-)
5. 作业二
第二次作业将分析心血管疾病数据集克隆教程仓库, 在本地编辑这个 Jupyter notebook(你需要填充缺失的 Python 代码), 并在这个 Google 表单中选择正确答案 (提交表单之后, 在截止日期之前, 仍然可以编辑你的回答) 请保持电子邮件地址和之前的课程与作业中登记的一致
截止日期: February 18, 23:59 CET
6. 相关资源
本次教程中使用的库的官方文档: matplotlibseabornpandas
使用 seaborn 创建的图形样例画廊
scikit-learn 的流形学习文档
高效的 t-SNE 实现 Multicore-TSNE
Distill.pub 上的 How to Use t-SNE Effectively(如何有效地使用 t-SNE)
来源: https://juejin.im/entry/5aab695d518825556a722a15