资讯动态

Pandas缺失值统计与分析:从体检到处理的完整实战指南

发布时间:2026/10/21 6:59:41 来源:尧图企业网站定制
数据分析这个行当里听过最多的一句话不是模型精度怎么提而是这数据怎么这么多空值。尤其是拿到一份别人导出的表、爬下来的数据、或者业务方给的历史明细第一件事不是跑模型也不是画图而是先跪在缺失值面前把它捋清楚。缺失值就像房间里的大象——你假装看不见它它会在均值计算、分组汇总、模型训练每一个环节忽然绊你一脚。用Pandas做缺失值统计与分析本质上是给数据做一次全面体检搞清楚哪些列缺、缺多少、缺在哪、为什么缺然后再决定是填、是删、还是留着。这篇文章不聊那些玄乎的数据治理框架就聊我用Pandas处理缺失值的完整套路从快速体检到逐列拆解从缺失模式识别到可视化呈现再到填充和删除的取舍逻辑。适合刚接触Pandas的初学者也适合已经写了段时间脚本但总在缺失值上翻车的同学。内容全部基于我实际跑过的脚本和踩过的坑可以直接copy到自己的项目里改改就用。1. 拿到数据后为什么要先摸清缺失值三个绕不开的实际原因先讲个真实场景。某次我接手一个跨月度销售明细二十多万行六十多列业务方拍着胸脯说这表很干净。结果我用info()一看光是客户区域这一列就空了将近四分之一更别提那些从第三个月才开始有记录的指标列。如果当时直接拿这张表去算月度环比、去训练预测模型出来的结果基本就是垃圾进垃圾出。缺失值统计之所以是数据分析的第一步不是因为流程规定而是因为以下三个环节全都依赖它。首先是描述性统计会失真。Pandas里的mean()、sum()、std()这些聚合函数默认会跳过NaN这听起来是好事但如果缺失比例达到30%算出来的均值代表的是那70%的数据而不是全量数据。更隐蔽的是如果你不检查缺失值就直接用groupby做分组对比某些分组可能因为缺失被整个剔除导致组间可比性彻底丧失。其次是特征工程的链条会被打断。做特征衍生的时候很多计算是跨列的比如客单价销售额/订单数一旦分母有缺失结果就是NaN而且这个NaN还会顺着特征传染给下游模型。你可以用fillna(0)硬填但填0和填均值、填中位数对模型的影响完全不同没有缺失值统计作依据填什么都像在闭眼猜。最后是可视化阶段会莫名报错或者出现诡异图形。用matplotlib画散点图缺失值会被直接跳过画出来的点连不成趋势用seaborn的heatmap画相关性矩阵只要有一列存在缺失整张相关性热力图都会出现空洞甚至直接无法计算。先做缺失值统计就是在给后面所有的分析步骤上一个保险。从操作层面讲Pandas提供了非常轻量的接口来快速摸清缺失全貌。最基础的两板斧是df.info()和df.isnull().sum()。前者看缩略体检报告后者看逐列缺失数量。我通常一上来就执行这两句先把整体水位测出来再决定要不要深入。2. 逐列拆解缺失情况数量、比例、类型三步走df.isnull().sum()只能告诉你每列缺多少但光有绝对数量没什么决策价值。一千行的表缺500个和十万行的表缺500个严重程度完全不同。所以我习惯马上加算缺失比例并且同时查看每一列的数据类型和取值范围把缺失情况拆成三维来看。2.1 缺失数量与缺失比例的搭配使用直接上代码我每次拿到新数据都会建一个缺失值概览表import pandas as pd def missing_overview(df): 返回DataFrame的缺失值概览 缺失数量、缺失比例、数据类型、非空唯一值个数 missing_count df.isnull().sum() missing_ratio (df.isnull().sum() / len(df)).round(4) dtypes df.dtypes nunique df.nunique() overview pd.DataFrame({ 缺失数量: missing_count, 缺失比例: missing_ratio, 数据类型: dtypes, 非空唯一值个数: nunique }) overview overview.sort_values(缺失比例, ascendingFalse) # 只保留缺失数量大于0的列减少干扰 overview overview[overview[缺失数量] 0] return overview # 用法 # df pd.read_excel(xxx.xlsx, sheet_name明细) # result missing_overview(df) # print(result.to_string())为什么还要加一列非空唯一值个数因为很多时候缺失值分析和列本身的取值分布强相关。如果某列只有两个取值比如是否首购缺失比例再高也容易处理无非是在是/否/缺失三个状态里做文章如果某列是连续型数值比如用户年龄那缺失的处理就要慎重得多直接删行可能丢掉大量样本盲目填均值又可能引入偏差。2.2 区分真缺失与假缺失容易被忽略的坑这一步是最容易被新手忽略、也是经验最值钱的地方。所谓假缺失是指数据里那些看起来是NaN、实际上根本不是缺值的单元格。我见过最典型的几种空字符串很多业务系统导出时空值不是NaN而是空字符串。df.isnull()对空字符串返回False但df[列].str.strip()之后发现全是空字符实际上等同于缺失。占位符比如NULL、NA、-、未知、none这些字符串混在数据里isnull()一概识别不了。全角空格Excel里常见的 肉眼看起来是空的Pandas认为是有值的。判断有没有这类假缺失我通常在概览表之前先做一步清洗检查# 找出所有object类型列中等于空字符串、特定占位符的情况 placeholder_values [, NULL, NA, N/A, -, 未知, none, null, ] for col in df.select_dtypes(include[object]).columns: freq df[col].value_counts(dropnaFalse).head(20) hits freq.index.intersection(placeholder_values) if len(hits) 0: print(f列 {col} 存在疑似占位符: {list(hits)})找到之后直接统一替换df df.replace(placeholder_values, pd.NA)把空字符串、占位符先转成真正的缺失值再做isnull()统计这样统计出来的缺失才是真实的缺失。这个步骤放在所有缺失值分析之前顺序千万别搞反否则后面所有结论都是错的。2.3 数值列和文本列的缺失要分开看待数值列的缺失往往影响的是统计指标和模型特征文本列的缺失影响的更多是分类汇总和业务判断。二者的填充手段也完全不同。数值列可以用均值、中位数、插值文本列更多是用众数、未知占位或者向前向后填充。所以做缺失值概览时我会把列拆成numeric和object两类分别跑一遍概览方便后面对症下药。numeric_cols df.select_dtypes(include[number]).columns object_cols df.select_dtypes(include[object, category]).columns print(数值列缺失概览) print(missing_overview(df[numeric_cols]).to_string()) print(\n文本列缺失概览) print(missing_overview(df[object_cols]).to_string())这一步还有一个附带作用排查数据类型存储是否合理。比如订单金额被读成了object类型那说明这列里大概率混入了暂无、待定之类的脏文本这本身就是一个数据质量问题光靠缺失值处理解决不了还得回头做类型转换。类型转换的常见姿势是pd.to_numeric(df[订单金额], errorscoerce)注意errorscoerce会把无法转换的文本变成NaN之后再走缺失值分析的流程这等于把一部分脏数据显式暴露成缺失值反而方便了后续处理。3. 从整体水位到行级剖析哪些行才是重灾区很多时候列视角的缺失统计做完之后你对数据的整体情况已经有了概念但还缺一个行视角的判断。同样的10%缺失率如果只是均匀散布在各行那对样本质量影响不大但如果集中在某一段时间、某一个地区、某几个特定用户身上那明摆着背后有业务层面的原因比如系统上线时间点不同、某个渠道的数据接入不全。这时候就需要切换到行维度来排查。3.1 行级缺失统计与全空行过滤我的习惯是先算每一行的缺失数量、缺失比例然后把最严重的行挑出来看row_missing_count df.isnull().sum(axis1) row_missing_ratio row_missing_count / df.shape[1] df_with_row_missing df.copy() df_with_row_missing[行缺失数量] row_missing_count df_with_row_missing[行缺失比例] row_missing_ratio.round(4) # 查看缺失行数最多的前20行 severely_missing df_with_row_missing.sort_values(行缺失数量, ascendingFalse).head(20) print(severely_missing.to_string())如果有些行几乎全空例如行缺失数量等于总列数减一或减二那基本就是无效记录直接删除就好。我通常设置一个经验阈值如果某一行缺失比例超过80%再看一下该行的关键业务字段比如订单号、用户ID、时间戳有没有值如果连这些都没有就丢进待删除列表。3.2 关键业务列缺失的定向筛查全行平均缺失比例低不代表关键列都健康。比如用户分析场景中用户ID和注册时间这两列的缺失是致命的即便只有0.1%的缺失也不能放过因为缺失意味着无法关联任何行为记录。我会把这类列单拎出来做定向筛查key_cols [用户ID, 订单号, 成交时间, 金额] for col in key_cols: col_missing_count df[col].isnull().sum() if col_missing_count 0: print(f关键列 [{col}] 缺失 {col_missing_count} 行缺失比例 {col_missing_count / len(df):.6%}) # 把缺失的具体行抽样打印出来肉眼确认是否异常 print(df[df[col].isnull()].head(3).to_string())这一段做的是业务逻辑筛查不是纯粹的统计。缺失值分析不能只停留在缺了多少的层面必须结合业务含义判断这个缺能不能忍。任何通用脚本都替代不了这一层判断这也是为什么我每次都坚持把关键列筛查写成独立步骤不混在概览函数里。3.3 缺失集中在特定分组的检查当怀疑缺失和业务分组有关时我会用groupby把缺失率按分组维度算一遍。比如想确认新老客户是不是缺失差异的来源就可以写df[是否缺失客户区域] df[客户区域].isnull().astype(int) result df.groupby(客户类型)[是否缺失客户区域].agg([count, sum, mean]) result.columns [样本数, 缺失数, 缺失率] print(result.to_string())同理如果你想看一下缺失是否跟时间有关可以按月份分组df[订单月份] pd.to_datetime(df[订单时间]).dt.to_period(M) monthly_missing df.groupby(订单月份)[客户区域].apply(lambda s: s.isnull().mean()) print(monthly_missing.to_string())这样算出来的比例比全局平均缺失率信息量大得多。如果发现某个月的缺失率异常飙升往往能追溯到那个月换了导出工具、或者某个渠道的数据没接进来这类业务根因通常是后续处理思路的关键依据。4. 缺失模式识别是随机的还是有规律的统计做多了之后我越来越意识到缺失值分析真正的分水岭不在于怎么填而在于判断缺失是怎么产生的。因为处理方式完全取决于缺失机制。学术界把缺失机制分成三大类实际工作中我们不用记那些术语但要清楚两种现实情况第一种是缺失完全随机和任何其他字段无关这种填什么方法都差不多第二种是缺失和其他字段存在关联这种就必须谨慎直接删除或者简单填充都会把这种关联带偏。4.1 用交叉表寻找缺失值之间的抱团现象缺失值的抱团指某几个列要么一起有值、要么一起空缺这种模式单纯看列缺失比例是看不出来的。可用交叉表检查把两列各自的缺失情况变成布尔值然后做分组计数很快就能看到是不是只有特定组合才出现。# 检查列A和列B是否同时缺失 col_a_missing df[列A].isnull() col_b_missing df[列B].isnull() cross pd.crosstab(col_a_missing, col_b_missing) cross.index [列A有值, 列A缺失] cross.columns [列B有值, 列B缺失] print(cross.to_string())如果列A缺失和列B缺失同时出现的频次异常高那这两列很可能是同一条采集链路里的字段比如一个埋点事件里一起上报的多个参数漏就一起漏了。这种情况的处理思路也基本就位要么补齐整条链路要么把这两列的缺失视为同一个问题合并处理。4.2 缺失值与数值分布的关联检查画个对比图另一种常见规律是缺失与否会影响列本身的数值分布。比如订单金额不缺的数据里折扣比例多数集中在0.1到0.3而折扣比例缺失的行订单金额往往偏高。这种时候直接删掉折扣缺失的行会让订单金额的分布塌掉一块后面做价格分析就会失真。判断这种关联最直接的办法是把缺失布尔值作为分组变量对比其他关键数值列的分布df[折扣是否缺失] df[折扣比例].isnull() grouped df.groupby(折扣是否缺失)[订单金额].describe() print(grouped.to_string())对比mean、50%、75%这些分位数如果差异很大说明缺失不是随机的和业务数值存在某种关联。用这段代码跑出来的结果通常能直接支撑你后面选择不删行、用填充这个决策非常实用。顺带提一句describe()输出里的count本身就包含非空数量信息比如两个组的count加起来不等于总样本数说明组内还是存在其他列的缺失这也能提醒你继续下钻分析。4.3 用延迟差分识别时间序列里的缺失空洞时间序列数据的缺失模式检查跟普通表格不一样。普通表格看的是行与列时间序列看的是缺口和时间间隔。我常用的是把时间列排序之后计算相邻时间差缺失规律一目了然df_sorted df.sort_values(时间戳) df_sorted[时间间隔] df_sorted[时间戳].diff().dt.total_seconds() # 查看时间间隔分布 print(df_sorted[时间间隔].describe().to_string()) # 找出间隔异常大的记录 big_gaps df_sorted[df_sorted[时间间隔] 3600] # 比如大于1小时的间隔 print(big_gaps.head(10).to_string())用这个方法你能肉眼看到每一天、每小时的数据是不是连续覆盖的。比如间隔重灾区集中在凌晨两点到五点那可能是定时采集任务在这个时段出过问题这类根因分析对后面的填充方案影响极大——如果你原本打算用前向填充结果空洞横跨了多个下午时段直接ffill()就会拿凌晨的数据填白天的缺口这个偏差一下子就带起来了。5. 缺失值可视化从数字到图形一眼锁定问题列光靠.sum()和.mean()处理单张表还可以但到了几十列、上百列的大型宽表光看数字就非常低效了。可视化的核心价值在于把缺失情况的形状呈现出来让眼睛直接发现模式。我常用的有三类图由浅入深分别是条形图、矩阵热力图和相关性矩阵。5.1 缺失比例条形图最基础的全局视野基于missing_overview()的结果画一张横向条形图按缺失比例排序一眼就能看到哪些列是问题列import matplotlib.pyplot as plt overview missing_overview(df) plt.figure(figsize(10, max(6, len(overview) * 0.4))) plt.barh(overview.index, overview[缺失比例], color#6b7a8f) plt.xlabel(缺失比例) plt.title(各列缺失比例分布) plt.tight_layout() plt.show()这张图的信息量其实非常大。如果看到的是少数列缺失比例断层式领先那基本是采集链路问题聚焦处理那几列即可如果看到的是所有列都带一点缺失那更像是系统性的数据质量下滑要考虑上游数据源整体排查。另外图的纵轴顺序是Pandas默认的逆序排列从下往上排我习惯先overview.sort_values(缺失比例, ascendingTrue)再画这样最严重的列会出现在图的最上方阅读顺序更自然。5.2 缺失值矩阵热力图定位缺失条纹如果要进一步看行与列之间的缺失模式我强烈建议试试missingno这个库它几乎是缺失值可视化的标准答案。import missingno as msno # 按行抽样避免行列太多时图形过于密集 sample_df df.sample(min(1000, len(df)), random_state42) msno.matrix(sample_df, figsize(12, 8)) plt.show()输出图形里每一行是一条样本每一列是一个字段白色间隙的就是缺失位置。盯着这张图看个几秒钟你往往能注意到一些特征成列的整段白色横带说明某一列在某个时间段整体缺失多条行全白说明某些样本只有零星字段有值。这种肉眼识别是任何数字统计都替代不了的。5.3 缺失相关性矩阵把一起空变成热力图逻辑数值最后再进一步用数值量化两列缺失之间的协同关系做法是把整个DataFrame映射成0/1矩阵有值为0缺失为1然后计算列与列之间的相关性missing_indicator df.isnull().astype(int) missing_corr missing_indicator.corr() plt.figure(figsize(12, 10)) plt.imshow(missing_corr, cmapcoolwarm, vmin-1, vmax1) plt.colorbar() plt.xticks(range(len(missing_corr.columns)), missing_corr.columns, rotation90) plt.yticks(range(len(missing_corr.columns)), missing_corr.columns) plt.title(缺失值相关性热力图) plt.tight_layout() plt.show()热力图中如果出现两个列接近深红色比如相关系数0.9以上说明这两列的缺失模式几乎完全同步处理时可以当作同一组变量来对待深蓝色则说明它们两个几乎互斥一个缺失时另一个基本有值这类结构有助于设计缺失替代方案比如用列B的值去估计列A的缺失区间。这张图还有一个意想不到的用处在建模阶段做特征筛选如果某个特征的缺失模式和其他特征强相关说明这个缺失本身可能携带了信息保留它作为特征比粗暴填充更有价值。6. 缺失值处理的取舍删除、填充、还是让它留在那统计和可视化都是诊断处理才是治疗。诊断做得越清楚治疗就越有依据。下面这部分我按自己实际使用频率把处理手段分成删、填、留三类每一类都会说明适用条件和不适用条件。注意这里没有哪个方法绝对正确一切取决于你的分析目标和缺失机制。6.1 按行删除的适用边界删除行是最简单粗暴的手段也是我最谨慎的手段。它的适用边界非常窄我一般只在这两种情况下执行缺失比例极低的行比如缺失率小于1%删掉对整体分布影响微乎其微行本身几乎全空没有任何分析价值保留只会干扰后续聚合。执行代码时我习惯先保留一份删除前的数据副本作为backup避免删完后悔df_backup df.copy() # 删除关键列缺失的行 df df.dropna(subset[用户ID, 订单号, 成交时间]) # 删除缺失比例高于80%的行 row_missing_ratio df.isnull().sum(axis1) / df.shape[1] df df[row_missing_ratio 0.8]这里要特别提醒一个隐藏问题如果两列强关联的缺失按行删除会把同时缺失的行整批去掉造成样本选择偏差导致后续分析结果不具代表性。所以在删除之前务必回顾一下上一章的缺失相关性矩阵确认没有强抱团的缺失列组合再动手。6.2 填充前的为什么均值、中位数、众数、前向填充怎么选填充是大多数人最常用的手段但选择填空方法时必须先回答一个为什么——为什么用均值而不是中位数为什么用中位数而不是0数值型、分布对称、无明显异常值用均值df[列].fillna(df[列].mean())。均值计算简单但要提前确认该列的分布没有长尾否则一个极端值就能把均值拉偏。数值型、分布右偏比如收入、金额类用中位数。中位数对异常值不敏感能更好地代表典型水平。类别型用众数。比如支付方式缺失最常见的是什么就填什么。时间序列用前向/后向填充。ffill()用上一个有效值填补缺口bfill()用下一个有效值回填。如果缺口很小且序列连续这个方案表现很好。我实际工作中比较稳妥的做法是先把数值列的填充方案集中定义好再一次性执行fill_strategies { 订单金额: df[订单金额].median(), 折扣比例: df[折扣比例].median(), 客户年龄: df[客户年龄].median(), 支付方式: df[支付方式].mode()[0] if not df[支付方式].mode().empty else 未知, } df df.fillna(fill_strategies)关于mode()有个细节如果众数有多个返回值是一个Series取[0]才能拿到第一个众数。另外如果整列都是缺失值mode()返回空Series此时代码里的if not ... empty判断会兜底填未知这个细节我至少被坑过两次写出来给大家避个雷。6.3 插值法和模型预测填充如果数据是连续型且有明显的趋势或周期直接填均值会让序列失去连续性这时interpolate()是更优解。它默认按线性插值处理也就是在缺失区间的两端之间拉一条直线取中间值df[销售额] df[销售额].interpolate(methodlinear)时间序列场景还可以换用时间感知的插值df[销售额] df[销售额].interpolate(methodtime)methodtime会把时间间隔考虑进去间隔不均匀时也能插得比较合理。例如某一天数据停了48小时下一笔记录的数值直接线性插值会把48小时压缩成和24小时一样的梯度而按时间插值就能体现这个差距。如果你已经到了建模阶段手里有一堆现成特征还可以用机器学习模型来预测缺失值。它的思路是把缺失列当作目标变量把其他列当作特征用非缺失样本训练模型再对缺失样本做预测。这个方案效果通常很好但成本也高适合在研究阶段对关键字段做精细补齐不适合放在每日跑批的ETL流程里。6.4 什么都不填把缺失当作信息本身最后一种思路是刻意不处理缺失。听起来反直觉但某些场景下缺失本身就是一个有区分度的信号。比如用户画像里是否填写了职业信息这一列填了职业的用户往往注册时长更长、活跃度更高直接把缺失合并为未填写这个类别反而比瞎猜一个职业更有价值。我常用的保留手段是给缺失单独打标签df[职业] df[职业].fillna(未填写)这一步的本质是显式缺失。它不同于填充因为你并没有试图恢复真实值而是把缺失本身编码成一个可分析的类别。在正式建模中这种做法既能保留样本量又能让模型自己学习这个类别的行为特征经常能意外地提升模型效果。6.5 处理之后必须做的验证闭环缺失值处理不是做完就完事必须验证处理前后数据分布有没有被扭曲。我每次处理完都要跑一个对比脚本看看填充前后的核心统计指标变化before df_backup[订单金额].describe() after df[订单金额].describe() comparison pd.DataFrame({填充前: before, 填充后: after}) print(comparison.to_string())重点看mean、50%、std这三个指标的变化幅度。均值波动超过5%就要警惕标准差急剧缩小则说明填充值过于集中可能在人为制造尖峰。如果看到这些异常回头调整填充策略或者改用保留缺失的方式。这一步也是我本人从填完就交差转变成填完要验证的关键节点它避免了大量返工。7. 实操建议与个人心得说句实在话缺失值统计与分析听着基础但它比很多花哨的模型都更考验数据敏感度。项目投入的时间和耐心很大比例不是花在写作代码上而是花在各种为什么这里会缺的追问上。下面几条经验是我用过很多次之后沉淀下来的新项目可以直接复用。第一做任何缺失值分析之前先把假缺失转真缺失。空字符串、占位符、全角空格这些不处理统计出来的全是一笔糊涂账。第二不要只算全局缺失比例一定要按关键分组维度拆开看同一个缺法背后可能是完全不同的原因处理策略也得跟着分开。第三填充方案宁可保守一点也不要一次填得太过花哨简单的中位数填充加一个显式缺失列往往就能满足大多数建模需求。第四所有把缺失值填充当成常规分析流程的人一定要养成保留完整备份的习惯df_backup df.copy()一行代码省下的可能是一整天重跑数据的功夫。借用我自己的表单习惯每次分析开始我都会把副本、概览表、填充策略、验证对比四个物件一次性建好。这套动作整体固化后处理缺失值就从一个需要反复判断的难题变成了一个自带节奏感的例行流程。希望这个思路也能帮你省掉一些重复劳动。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑