Python数据分析必备:missingno缺失值可视化实战(附完整代码)
Python数据分析必备:missingno缺失值可视化实战(附完整代码)
在数据科学项目的早期阶段,我们拿到一份新数据集时,最令人头疼的往往不是复杂的算法选择,而是那些隐藏在数据行间的“空白”——缺失值。这些空白单元格,有时是数据采集时的疏忽,有时是业务逻辑下的必然,但它们无一例外地会干扰后续的分析与建模。传统的df.isnull().sum()虽然能告诉我们缺失的数量,却无法揭示缺失的模式、关联与分布。这就像只拿到了地图上的坐标,却看不清地形全貌。今天,我们就来深入探讨一个能让你“看见”缺失值的强大工具:missingno。它专为Python数据分析设计,通过直观的可视化,将缺失数据的“黑洞”清晰地呈现在你眼前,无论是数据清洗新手还是经验丰富的老手,都能从中获得前所未有的洞察力。
1. 为什么我们需要“看见”缺失值?
在深入代码之前,我们不妨先思考一个更根本的问题:为什么可视化缺失值如此重要?仅仅知道有多少个NaN难道不够吗?
想象一下,你正在分析一份用户行为日志。df.isnull().sum()告诉你,“最后登录时间”这一列有30%的缺失。这只是一个孤立的数字。但如果通过可视化,你发现这30%的缺失用户,其“购买记录”和“活跃天数”也同时为NaN,这就揭示了一个模式:这些可能是已经流失的僵尸用户。反之,如果“最后登录时间”的缺失是随机散布的,与其他字段无关,那可能只是数据同步时的偶发错误。这两种情况,对应的数据清洗策略(是整行删除、插补还是标记)截然不同。
missingno的核心价值,就在于它提供了四种互补的视角来审视缺失数据:
- 矩阵图 (Matrix):全局鸟瞰,看缺失值在数据集中的分布模式。
- 条形图 (Bar Chart):量化评估,看每一列缺失的严重程度。
- 热力图 (Heatmap):关联分析,看哪些列的缺失是相关的。
- 树状图 (Dendrogram):聚类洞察,看哪些列具有相似的缺失模式。
下面,我们将从一个完整的实战案例出发,手把手带你掌握这四种“武器”。
2. 环境准备与数据加载
首先,确保你的工作环境已经就绪。我们使用pip进行安装,这是最直接的方式。
pip install missingno pandas numpy matplotlib seaborn
提示:建议在Jupyter Notebook或JupyterLab环境中运行本文的代码,以便实时看到交互式的图表结果。
接下来,我们导入必要的库,并创建一个包含可控缺失模式的数据集,这样能更好地观察missingno的效果。这里我们不使用完美的boston数据集,而是自己构造一个更典型的例子。
import missingno as msno
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 设置随机种子以保证结果可复现
np.random.seed(42)
# 创建一个模拟的销售数据集
n_samples = 200
data = {
'客户ID': range(1, n_samples + 1),
'年龄': np.random.randint(18, 70, n_samples),
'年收入(万)': np.random.normal(30, 10, n_samples).round(1),
'城市': np.random.choice(['北京', '上海', '广州', '深圳', '杭州'], n_samples),
'最近登录日期': pd.date_range('2023-01-01', periods=n_samples, freq='D'),
'购买次数': np.random.poisson(5, n_samples),
'平均客单价': np.random.uniform(50, 500, n_samples).round(2),
'客服评分': np.random.choice([1, 2, 3, 4, 5], n_samples, p=[0.05, 0.1, 0.2, 0.3, 0.35]),
'是否会员': np.random.choice(['是', '否'], n_samples, p=[0.7, 0.3])
}
df = pd.DataFrame(data)
# 人为制造有意义的缺失值模式
# 模式1:年收入较高的人,更可能隐藏收入信息(缺失)
high_income_mask = df['年收入(万)'] > 45
df.loc[high_income_mask.sample(frac=0.4, random_state=42).index, '年收入(万)'] = np.nan
# 模式2:非会员用户没有客服评分
df.loc[df[df['是否会员'] == '否'].sample(frac=0.6, random_state=42).index, '客服评分'] = np.nan
# 模式3:随机缺失一些“城市”信息(模拟数据录入错误)
df.loc[df.sample(frac=0.1, random_state=42).index, '城市'] = np.nan
# 模式4:最近未登录的用户,“购买次数”和“平均客单价”可能同时缺失(模拟数据未同步)
inactive_mask = df['最近登录日期'] < pd.Timestamp('2023-06-01')
df.loc[inactive_mask.sample(frac=0.25, random_state=42).index, ['购买次数', '平均客单价']] = np.nan
print("数据集形状:", df.shape)
print("\n各列缺失值统计:")
print(df.isnull().sum())
运行上述代码,你会得到一个包含200行、9列的DataFrame,以及各列的缺失值数量统计。这比一个全零缺失的数据集更有分析价值。
3. 缺失值可视化四部曲
3.1 矩阵图:缺失模式的全局侦察兵
矩阵图是missingno的招牌功能,它能让你在一瞥之间掌握整个数据集的缺失情况。
# 绘制缺失值矩阵图
plt.figure(figsize=(10, 6))
msno.matrix(df, labels=True, sparkline=False, fontsize=12)
plt.title('数据集缺失值矩阵图', fontsize=14, pad=20)
plt.show()
解读与洞察:
- 黑白条纹:图中的白色线条代表缺失值。你可以清晰地看到,
年收入(万)、客服评分等列的白色条纹并非完全随机分布,而是呈现出一定的“块状”或“带状”模式。 - 右侧数据条:图右侧的条形(sparkline,本例中已关闭)显示了数据完整性的密度。如果开启(
sparkline=True),你会看到一条曲线,波谷处对应缺失值集中的行。 - 坐标轴信息:左侧的纵坐标是数据行的索引,底部的横坐标是列名。右下角的数字(如“9 columns”)表示总列数。
一个关键发现:仔细观察,你可能会发现购买次数和平均客单价的白色条纹经常出现在同一行。这验证了我们构造数据时的假设——未活跃用户的两类行为数据同时缺失。这是条形图无法告诉你的关联信息。
3.2 条形图:缺失程度的量化仪表盘
如果说矩阵图给了你“地形”,那么条形图就给了你精确的“海拔数据”。
# 绘制缺失值条形图
plt.figure(figsize=(10, 6))
msno.bar(df, color='steelblue', fontsize=12, figsize=(10,6))
plt.title('各特征非缺失值数量与比例', fontsize=14, pad=20)
plt.tight_layout()
plt.show()
解读与洞察:
- 左侧y轴:表示非缺失值所占的百分比。柱子越高,说明该列数据越完整。例如,
客户ID和是否会员的柱子顶到了100%,说明这两列没有缺失。 - 右侧y轴:表示非缺失值的绝对数量。它与左侧百分比是联动的。
- 柱子上的数字:直接显示了非缺失值的数量。
这个视图让你能快速对数据质量进行排序和评估。例如,你可以一眼看出客服评分是缺失最严重的列之一,可能需要优先处理。
为了更直观地制定数据清洗策略,我们可以将条形图的信息整理成一张决策表:
| 特征 | 非缺失值比例 | 缺失严重度评级 | 建议的初步处理策略 |
|---|---|---|---|
| 客户ID | 100% | 无 | 无需处理,可作为索引。 |
| 年龄 | 100% | 无 | 无需处理。 |
| 年收入(万) | ~75% | 中等 | 考虑使用中位数/均值插补,或基于年龄、城市分组插补。 |
| 城市 | ~90% | 轻微 | 可使用众数插补,或标记为“未知”。 |
| 最近登录日期 | 100% | 无 | 无需处理。 |
| 购买次数 | ~85% | 轻微 | 可与平均客单价联动分析,对同时缺失的行考虑整行删除或联合插补。 |
| 平均客单价 | ~85% | 轻微 | 同上。 |
| 客服评分 | ~65% | 严重 | 深入分析缺失原因(是否与非会员强相关),考虑使用模型预测插补。 |
| 是否会员 | 100% | 无 | 无需处理,可作为分组依据。 |
3.3 热力图:缺失关联性的探测器
这是missingno中最能揭示数据背后故事的工具。它计算的是缺失相关性,即一列数据缺失时,另一列数据也缺失的可能性。
# 绘制缺失值热力图
plt.figure(figsize=(8, 6))
msno.heatmap(df, cmap='RdBu', fontsize=12) # 使用红蓝配色,更醒目
plt.title('缺失值相关性热力图', fontsize=14, pad=20)
plt.tight_layout()
plt.show()
解读与洞察:
- 颜色与数字:色块的颜色和其中的数字(范围从-1到1)代表了缺失相关性系数。1表示完全正相关(A缺则B必缺),-1表示完全负相关(A缺则B必不缺),0表示无关联。
- 关键发现:在我们的模拟数据中,你应该能看到
购买次数和平均客单价之间存在一个接近1(例如0.8以上)的强正相关值。这强烈暗示这两列数据的缺失是由同一个底层原因(如用户不活跃导致数据未记录)导致的。 - 对角线:对角线上的值均为1,因为一列与其自身的缺失模式当然是完全相关的。
注意:缺失相关性不等于数据本身的相关性(如皮尔逊相关系数)。它只关心“是否缺失”这个二元状态之间的关系。这是理解业务逻辑和数据处理流程漏洞的宝贵线索。
3.4 树状图:缺失模式的聚类图谱
树状图基于层次聚类算法,将具有相似缺失模式的列聚集在一起。
# 绘制缺失值树状图
plt.figure(figsize=(10, 6))
msno.dendrogram(df, fontsize=12)
plt.title('缺失模式聚类树状图', fontsize=14, pad=20)
plt.tight_layout()
plt.show()
解读与洞察:
- 树枝长度:图中竖线连接起来的“树枝”长度,代表了列之间缺失模式的差异程度。长度越短,说明两列的缺失模式越相似。
- 聚类解读:从根部(顶部)看起,算法会将所有列不断两两聚类。在我们的例子中,你很可能看到
购买次数和平均客单价最早被聚到一起(它们之间的横线最短),形成一个簇。这意味着它们的缺失模式几乎一模一样。而年收入(万)和客服评分可能各自成簇或与其他列聚类,表明它们的缺失模式相对独立。 - 实战应用:这个视图可以帮助你制定分组插补策略。对于聚类在一起的列,可以考虑使用相同的插补逻辑或模型,因为它们受相似的缺失机制影响。
4. 超越可视化:从洞察到行动
可视化本身不是终点,基于洞察采取行动才是。结合以上四种视图的分析,我们可以制定一个系统性的数据清洗方案。
第一步:处理高度相关的缺失列 针对购买次数和平均客单价,既然它们高度共缺失,我们可以:
- 确认这些行是否代表需要排除的样本(如长期未活跃用户)。如果是,考虑整行删除。
- 如果需要保留,则不宜对两列进行独立的简单插补(如用均值)。更好的方法是:
- 使用多变量插补法(如
sklearn的IterativeImputer)。 - 或利用其他完整列(如
最近登录日期、是否会员)建立回归模型来预测缺失值。
- 使用多变量插补法(如
第二步:处理有业务意义的缺失列 对于客服评分大量缺失且与是否会员相关的情况:
- 先进行交叉验证。创建一个简单的频数表来确认我们的观察:
# 检查‘是否会员’与‘客服评分’缺失的关系
missing_rating = df['客服评分'].isnull()
pd.crosstab(missing_rating, df['是否会员'], margins=True, margins_name='总计')
- 如果确认非会员普遍无评分,那么对于
是否会员为“否”的行的客服评分缺失,可以将其插补为一个特殊值(如0或“未评分”),这本身就是一个有信息量的操作。
第三步:处理随机缺失的列 对于城市这类看似随机缺失的列,可以采用简单插补:
# 使用众数进行插补
df['城市'].fillna(df['城市'].mode()[0], inplace=True)
第四步:处理需要谨慎处理的列 年收入(万)的缺失可能并非随机,且包含隐私信息。直接使用全局均值插补会引入偏差。更优的做法是:
# 基于‘城市’和‘年龄’分组的中位数进行插补
df['年收入(万)'] = df.groupby(['城市', pd.qcut(df['年龄'], q=4)])['年收入(万)'].apply(
lambda x: x.fillna(x.median())
)
完成这些步骤后,可以再次运行msno.matrix(df),你会看到一个干净得多的图表,白色条纹基本消失,这意味着你的数据集已经为下一步的探索性分析或建模做好了准备。
掌握missingno,相当于为你的数据分析工作流装上了一个高精度的“缺失值雷达”。它不能自动替你做出清洗决策,但它能把你需要做出决策的关键信息——缺失的模式、关联和集群——以最直观的方式呈现出来。下次当你面对一个满是漏洞的数据集时,别再只盯着数字摘要发呆,让missingno带你穿透表象,直击数据缺失的本质。
更多推荐

所有评论(0)