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. 超越可视化:从洞察到行动

可视化本身不是终点,基于洞察采取行动才是。结合以上四种视图的分析,我们可以制定一个系统性的数据清洗方案。

第一步:处理高度相关的缺失列 针对购买次数平均客单价,既然它们高度共缺失,我们可以:

  1. 确认这些行是否代表需要排除的样本(如长期未活跃用户)。如果是,考虑整行删除。
  2. 如果需要保留,则不宜对两列进行独立的简单插补(如用均值)。更好的方法是:
    • 使用多变量插补法(如sklearnIterativeImputer)。
    • 或利用其他完整列(如最近登录日期是否会员)建立回归模型来预测缺失值。

第二步:处理有业务意义的缺失列 对于客服评分大量缺失且与是否会员相关的情况:

  • 先进行交叉验证。创建一个简单的频数表来确认我们的观察:
# 检查‘是否会员’与‘客服评分’缺失的关系
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带你穿透表象,直击数据缺失的本质。

更多推荐