news 2026/10/27 3:13:25

Pandas散点图数据探查:快速识别离群点与异常值

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pandas散点图数据探查:快速识别离群点与异常值

做数据分析的人大概都有过这样的时刻:数据拿到手,行数、列名、类型都看过了,缺失值也统计了一遍,心里觉得“差不多了,可以开始建模了”,结果跑出来的模型指标忽高忽低,回头一查,某几行数值离谱得离谱。这类问题,十有八九在数据探查阶段就该被发现,只是当时没人把两列数据摆在一张散点图上看一眼。这篇内容聊的就是Pandas 数据探查里最不起眼、却最能救命的一步——离散点图(也就是我们常说的散点图分布分析),用它去找离群点和异常值,把数据的“形状”先摸清楚。它不需要复杂算法,几行代码就能出图,适合刚接触数据分析的新手,也适合做了几年却常常跳过可视化直接上手建模的老手。下面我会把散点图放在整个探查链路里讲清楚位置,再拆解画法、判别方法、踩坑记录和脚本骨架,尽量做到看完能直接抄进自己的 notebook 里跑。

1. 别急着算指标,先看清数据长什么样

1.1 数据探查这一步到底在探什么

很多人理解的“探查”就是df.info()加df.describe(),跑完打印一堆统计量,看着挺专业,实际上信息密度很低。平均值、标准差这些东西是压缩过的信息,它把几千行的分布压缩成一两个数字,分布的形状、双变量之间的关系、极端值的具体位置,全被压没了。真正的探查至少要做四件事:一是结构和类型,确认每列的 dtype 是不是跟业务含义对得上,日期列是不是被读成了字符串,金额列是不是被读成了 object;二是缺失情况,缺失比例、缺失是否成片出现、缺失本身有没有业务含义;三是单变量分布,值域、偏态、峰值、有没有明显不可能出现的取值;四是变量之间的关系,两两之间是线性、是曲线、还是压根没关系。

前两件事用info、isnull、value_counts就能搞定,第三件事用直方图和箱线图解决,第四件事就得靠离散点图了。顺序上我不建议跳步:先看结构和缺失,再看单变量,最后看双变量关系。原因很实在——如果某列里有 30% 的缺失,你画出来的散点图会少掉三分之一的数据点,图上看不见的那些点可能恰恰是最异常的,先知道缺失情况,看图时才不会误判。反过来,如果先看关系再看缺失,很容易把“因为缺失导致的关系不明显”错当成“两列无关”。

提醒:describe()的统计量只对数值列有效,object 列和 category 列要单独用value_counts()看,别指望一行 describe 覆盖所有列。

1.2 离散点图在整条链路里的位置

散点图的本质是把两列数值分别映射到横纵坐标,每一个数据点是一个样本。它一次能同时呈现三件事:相关性的方向和强弱、分布的聚集与稀疏程度、以及离群点的位置和数量。这三点在纯数字表里是看不见的,必须靠图形。举个具体的例子,一批订单数据里,“订单金额”和“商品件数”理论上应该高度正相关,单看两列的均值都在正常范围,但散点图一画,你会发现右上角有几个点,件数只有个位数,金额却是六位数——这类数据在统计量里不明显,在图上非常扎眼。

我习惯把离散点图放在探查的第二个环节,也就是在单变量分布之后、特征工程之前。理由很直接:单变量图告诉你每一列自己有没有毛病,双变量图告诉你列与列之间有没有矛盾。数据的可信度,往往就藏在“列与列对不上”的地方。比如“注册日期”晚于“首次下单日期”,“累计消费金额”除以“订单数”得到的客单价低得不合理,这些矛盾单看一列都正常,两列一对照就露馅了。散点图就是做这个对照最廉价的工具,没有之一。

1.3 一个跳过这步的翻车现场

说个我听来的例子,某团队做一个用户流失预测的小项目,数据量几万行,特征做了标准化,模型换了两三种,AUC 一直卡在 0.6 出头,怎么调都上不去。折腾了一个多星期,后来有人顺手画了一张“最近一次活跃距今天数”和“累计使用时长”的散点图,问题一眼看出来了:有几个样本的“距今天数”是负数。负数的天数在业务上完全说不通,说明数据导出时的时区或者系统时间出了问题。这几个点数量不多,但在标准化之后会被放大成极端值,把整个特征分布的尺度拉歪,模型自然学不好。

删掉或者修正这几个点之后,同样的模型、同样的参数,指标直接上了一个台阶。这个例子说明的事很朴素:异常值不会自己举手报告,它安静地躺在表里,直到你在图上看见它。而且这类问题越早发现越便宜,等做完特征工程、跑完交叉验证再回头,时间和算力都浪费了。所以我的个人原则是:任何一份新数据,在写第一行特征代码之前,至少要扫一遍双变量散点图,重点列全覆盖,非重点列抽样看。这个习惯几乎不需要额外成本,十几分钟的事。

1.4 谁需要把这一步做扎实

如果你是学生或者刚转行,正在做课程项目、练手数据集,这一步能让你养成正确的顺序感,别一上手就fit,先图后模是好习惯。如果你是从业者,日常处理业务数据,这一步能帮你在评审会上少被问倒,别人问“这列数据你确认过吗”,你能拿图说话。如果你是带人的人,把这份检查清单固化成模板,团队里每个人拿到新数据都跑一遍,能省掉大量“数据有问题”的来回扯皮。

反过来说,什么情况下这一步可以简化?数据量极小(几百行以内)、字段极少(三五个)、来源完全可信(自己刚生成的模拟数据),那看一眼 describe 也就够了。但只要数据来自外部、经过多人转手、跨系统拼接,就别省这一步。数据的可信度和它的流转次数成反比,这个经验我至今没见它失效过。

2. 环境搭稳,样例数据先跑通

2.1 依赖选择和版本上的小坑

做这件事只需要三样东西:Pandas做数据操作,Matplotlib出底层图,Seaborn可选,用来做分组着色和密度增强。装的时候建议把 NumPy 一起装上,造样例数据要用。版本上我建议 Pandas 用 1.5 以上或 2.x,Matplotlib 3.5 以上,太老的版本在plot.scatter的参数支持上有差异,比如s、c、alpha的接受类型在旧版本里比较挑剔。如果你在 Jupyter 里跑,记得开头加一行%matplotlib inline,否则有些环境里图不会直接显示,只打印一个对象地址,新手容易被这个吓到,以为是绘图失败。

另一个常被忽略的点是后端。在无图形界面的服务器上跑脚本,默认后端可能会报错或者静默不保存图片。稳妥做法是在导入 pyplot 之前设置matplotlib.use('Agg'),这样不弹窗,但savefig能正常出文件。我在本地写脚本时通常两套环境分开:notebook 里用交互后端边看边调,正式脚本用 Agg 后端批量输出,互不干扰。

# 依赖安装,建议放进虚拟环境 pip install pandas matplotlib seaborn numpy
import pandas as pd import numpy as np import matplotlib matplotlib.use('Agg') # 无界面环境必备,本地 notebook 可去掉 import matplotlib.pyplot as plt import seaborn as sns plt.rcParams['font.sans-serif'] = ['SimHei'] # 中文标题不乱码 plt.rcParams['axes.unicode_minus'] = False # 负号正常显示 print(pd.__version__, matplotlib.__version__)

2.2 造一份可控的样例数据

练习阶段最忌讳直接拿一坨真实数据上来就调,因为你不确定图上的异常到底是真的异常还是自己代码写错了。我的做法是先造一份已知答案的数据:主体部分服从一个明确的关系,然后手工塞进去几个“我确定它是离群点”的记录,看自己的代码能不能把它们揪出来。这样图一出来,对不对一眼就知道。代码不用复杂,np.random.seed固定随机种子保证可复现,normal造主体,concat拼上手工构造的异常行。

np.random.seed(42) n = 800 x = np.random.normal(50, 12, n) # 模拟某指标 A y = 2.1 * x + np.random.normal(0, 18, n) # 与 A 近似线性相关 base = pd.DataFrame({'metric_a': x, 'metric_b': y}) # 手工注入 6 条已知离群点 outliers = pd.DataFrame({ 'metric_a': [95, 98, 12, 10, 60, 62], 'metric_b': [20, 15, 210, 230, 260, 245] }) df = pd.concat([base, outliers], ignore_index=True) print(df.shape) # (806, 2) print(df.describe().round(2))

跑完你会看到metric_a的最小值 10、最大值 98,metric_b的最大值 260,这些都在 describe 里露头了,但你还是不知道它们是不是同一批样本。这就是统计量的局限,也是散点图存在的理由。把注入的那 6 行在图上找出来,是我每次教新人时的第一个小练习,通常五分钟就能建立直觉。

造数据时有一点要注意:主体数据的量级要和注入的异常点拉开差距,否则异常点混在正常分布里不明显。上例中主体metric_b大致在 105 附近波动,注入点要么只有十几,要么两百多,视觉上会明显跳出来。如果两者量级接近,图上就是一团糊,你反而会怀疑自己的方法有问题。

2.3 读入真实数据前的三项检查

换成真实数据,正式的读取之前有三项检查别省。第一,编码。中文环境下的 csv 经常是 GBK 或 GB18030,直接pd.read_csv可能抛UnicodeDecodeError,也可能不报错但读出乱码。我的习惯是先试utf-8,失败再试gbk,实在不行用chardet猜一下。第二,分隔符和表头。有些导出文件第一行是标题说明,不是列名,需要skiprows;有些用分号或制表符分隔,需要显式sep。第三,数值列被读成字符串。这在国内数据里极其常见,比如"1,234.5"带千分位逗号,或者带有单位后缀"35kg",Pandas 一律当字符串处理。

# 读取与类型矫正 df_raw = pd.read_csv('data.csv', encoding='utf-8') df_raw.columns = [c.strip() for c in df_raw.columns] # 去掉列名首尾空格 # 找出本该是数值、实际是 object 的列 obj_cols = df_raw.select_dtypes(include='object').columns.tolist() for col in obj_cols: converted = pd.to_numeric( df_raw[col].astype(str).str.replace(',', '', regex=False), errors='coerce' ) bad = df_raw[col].notna().sum() - converted.notna().sum() print(f'{col}: 转换失败 {bad} 条')

注意:errors='coerce'会把转不动的值变成 NaN,转换之后一定要统计有多少条变成了 NaN。如果不统计,直接把这一列拿去算均值,结果会静默偏小,比报错更危险。

这一步做完,你才有资格谈“图上的点是什么意思”。我见过不止一次,新人把字符串列直接丢给散点图,Matplotlib 报了个晦涩的类型错误,然后花了半小时去搜“scatter 报错”,其实根子在数据类型上。先修类型,再画图,顺序别倒。

3. 离散点图的几种画法,各有各的场合

3.1 一行代码的快速版

最省事的画法就是 Pandas 自带的.plot.scatter,它本质是对 Matplotlib 的封装,好处是不用管 fig、ax 的创建流程,直接给列名就行。

ax = df.plot.scatter( x='metric_a', y='metric_b', s=18, c='steelblue', alpha=0.6, figsize=(8, 5), grid=True, title='metric_a 与 metric_b 的散点分布' ) ax.set_xlabel('指标 A') ax.set_ylabel('指标 B')

这段在看一眼的场景下完全够用,尤其是一次要扫十几对变量的时候。但它有几个短板:想标出具体的异常点、想按第三个变量着色、想加参考线,都得绕回 Matplotlib 的 ax 对象去改,不如直接上底层。我的实际用法是:第一遍快速扫用.plot.scatter,确定要深挖某两张图的时候切到 Matplotlib 手工控制。这个分工能省不少时间,因为不是每一对变量都值得精雕细琢。

参数上有几个值得说。s是点面积,默认值偏大,几百个点以上建议降到 10 到 20;alpha是透明度,0.3 到 0.6 之间最好用,能通过颜色叠加看出密度;c给单色字符串就行,如果要按列着色就传 Series 并配上cmap。figsize别用默认值,默认的图太小,几千个点挤在一起什么都看不出来,8×5 是我的起步尺寸,点多了会加到 12×7。

3.2 手工控制的完整版

需要标注、需要参考线、需要双图对照的时候,老老实实写 Matplotlib。

fig, ax = plt.subplots(figsize=(9, 6)) ax.scatter(df['metric_a'], df['metric_b'], s=16, alpha=0.55, edgecolors='none', label='样本点') # 用 IQR 划出两条参考带(后面第 4 节详述) q1, q3 = df['metric_b'].quantile([0.25, 0.75]) iqr = q3 - q1 lo, hi = q1 - 1.5 * iqr, q3 + 1.5 * iqr ax.axhline(lo, ls='--', lw=1, color='gray', label='下界') ax.axhline(hi, ls='--', lw=1, color='gray', label='上界') ax.set_xlabel('指标 A'); ax.set_ylabel('指标 B') ax.set_title('metric_a 与 metric_b 分布及异常带') ax.legend(loc='upper left', frameon=False) ax.grid(alpha=0.25) plt.tight_layout() plt.savefig('scatter_basic.png', dpi=160, bbox_inches='tight')

提示:tight_layout()和bbox_inches='tight'双保险,能避免保存出来的图被切掉坐标轴标签。这两个都加,成本几乎为零。

手工版最大的价值是能叠加信息。图上除了点,还能画分位线、画拟合趋势线、把特定的点圈出来。信息叠得越多,读图时下的判断就越有据可依。但也要克制,一张图上叠超过四层信息就开始乱了,我的一般上限是:点 + 两条参考线 + 少量标注 + 图例,再多就拆图。

3.3 大数据量下的密度处理

数据上万行以后,散点图会退化成一块色斑,这时候纯粹的点图失去意义。三个处理办法,按使用频率排序。

第一个是降透明度和缩小点面积,s=4、alpha=0.15,用颜色深浅表达密度,简单粗暴但有效。第二个是抽样,从整体里随机抽几千条画图,注意固定random_state,保证每次抽出来的样本一样,不然你前后两次看图得出不同结论,会怀疑人生。第三个是hexbin 六边形分箱图或者二维直方图,把平面切成网格,用颜色表示每格里的点数,密度看得最清楚,代价是看不出单个离群点的精确位置。

# 方案一:抽样 + 低透明度 sample = df.sample(n=min(3000, len(df)), random_state=42) ax1 = sample.plot.scatter(x='metric_a', y='metric_b', s=8, alpha=0.25, figsize=(8, 5), title='抽样 3000 点,alpha=0.25') # 方案二:六边形分箱,看整体密度 fig, ax2 = plt.subplots(figsize=(8, 6)) hb = ax2.hexbin(df['metric_a'], df['metric_b'], gridsize=40, cmap='YlGnBu', mincnt=1) fig.colorbar(hb, ax=ax2, label='网格内样本数') ax2.set_xlabel('指标 A'); ax2.set_ylabel('指标 B')

实操建议:先用抽样点图找离群点,再用 hexbin 看主分布,两个视角互补。离群点在 hexbin 上通常表现为孤立的、颜色极浅的小格子,不难认,但精确位置不如点图直观。另外抽样的时候别只抽一次,我一般会抽两次对比,如果两次抽出来的图形状差很多,说明数据分布本身不均匀,简单随机抽样有偏,这时候要么分层抽样,要么干脆用全量配透明度。

3.4 第三个维度的引入方式

两列散点图只能看二维关系,但数据的异常往往跟第三个变量有关。比如某笔订单的金额和件数都在合理范围,但它发生在一个从未出现过的渠道上。这时候就可以用颜色把第三个变量编码进去。

# 按类别着色 categories = np.where(df['metric_a'] > 60, '高值组', '常规组') colors = {'高值组': '#d9534f', '常规组': '#4a90d9'} fig, ax = plt.subplots(figsize=(9, 6)) for name in ['常规组', '高值组']: sub = df[categories == name] ax.scatter(sub['metric_a'], sub['metric_b'], s=16, alpha=0.6, label=name, c=colors[name], edgecolors='none') ax.legend(frameon=False) ax.set_title('按指标 A 高低分组着色') plt.tight_layout()

用颜色编码类别时有个硬性要求:类别数控制在 5 到 8 个以内,超过之后颜色就分不清了,人眼对色相的区分能力有限。如果类别很多,要么合并小类,要么改成分面小图,用subplots按类别各画一张。千万别用彩虹色系编码连续值,那种图看着花哨,实际上没人能准确读出色阶对应的数值,换个顺序感知的色板(比如黄到蓝的渐变)会好很多。

4. 从“看着怪”到“有依据”:量化判别配合图

4.1 为什么光看图不够

图能告诉你“这里有几个点不对劲”,但说不出“为什么不对劲”,也说不清“到底有多少个”。更麻烦的是,判断会因人而异:同一个点,我今天看着像异常,明天看着又觉得能接受。要下结论,得有量化标准配合。我的做法是先图后算,图算互验——图用来发现疑点,计算用来确认边界,最后把算出来的边界画回图上复核,三步走完才敢动数据。

这里要强调一个观念:离群点不等于错误数据。离群可能来自真实的极端业务场景(大客户、促销日、突发流量),也可能是录入错误、系统故障、单位换算错误。前一种要保留,甚至要单独分析;后一种必须处理。散点图的价值在于把候选集找出来,但要不要删、怎么删,得靠业务判断。我踩过的最大的坑,就是早期拿到数据就按 IQR 一刀切掉,结果把一个真实大客户的记录删了,后来对账时才发现金额对不上,那种尴尬很难解释。

4.2 IQR 四分位距法:最稳妥的默认选择

IQR 的思路很朴素:把数据按大小排好,取 25% 分位和 75% 分位,中间这段就是主体分布,然后向外扩 1.5 倍这个宽度,超出范围的算可疑。它的好处是不假设数据服从正态分布,对偏态数据比标准差法稳得多,而且计算简单、边界直观,画成横线往图上一放,谁都能看懂。

参数含义常用取值
Q125% 分位数由数据决定
Q375% 分位数由数据决定
IQRQ3 − Q1由数据决定
k边界系数1.5(常规)/ 3.0(极端离群)
def iqr_bounds(series, k=1.5): q1, q3 = series.quantile([0.25, 0.75]) iqr = q3 - q1 return q1 - k * iqr, q3 + k * iqr lo, hi = iqr_bounds(df['metric_b'], k=1.5) mask = (df['metric_b'] < lo) | (df['metric_b'] > hi) print(f'边界: [{lo:.2f}, {hi:.2f}],命中 {mask.sum()} 条') print(df.loc[mask, ['metric_a', 'metric_b']].head(10))

k选 1.5 还是 3.0,取决于你的容忍度。1.5 会揪出一批“轻度偏离”的点,适合做数据清洗前的普查;3.0 只抓极端值,适合你不想动太多数据的时候。我个人的用法是两个都算一遍,差集单独看一眼:3.0 抓出来的基本可以确认有问题,1.5 到 3.0 之间的这批要人工过一遍,看是不是业务上的真实极端值。这个过程费点时间,但比事后返工划算得多。

注意:IQR 是逐列独立算的,它对“单列数值离谱”很敏感,但对“单列都正常、组合起来矛盾”的情况完全无能为力。后者只能靠散点图看点位关系,或者构造比值类特征再算。这一点必须心里有数。

4.3 标准分与稳健标准分

标准分(Z-score)的思路是看每个点离均值有几个标准差,通常以 |z| > 3 作为阈值。它的前提是数据近似正态,用在偏态数据上会出问题:右偏的金额数据,均值和标准差都被右侧长尾拉大了,导致真正的异常点 z 值反而不大,漏检。所以这个方法我只在确认过分布形状、或者做过对数变换之后用。

from numpy import median, absolute z = (df['metric_b'] - df['metric_b'].mean()) / df['metric_b'].std(ddof=0) print('Z-score 命中:', (z.abs() > 3).sum()) # 稳健版:用中位数和 MAD 代替均值和标准差 med = df['metric_b'].median() mad = median(absolute(df['metric_b'] - med)) if mad > 0: rz = 0.6745 * (df['metric_b'] - med) / mad print('修正 Z 命中:', (rz.abs() > 3.5).sum())

稳健标准分用中位数和绝对中位差,这两个统计量对极端值不敏感,所以异常点自己不会把“标尺”撑大,检出率更接近预期。代价是它对分布中心的偏移不敏感,如果异常点占了相当比例(比如超过 10%),中位数本身就被污染了,这招也不灵。所以无论用哪种方法,最后都要回到图上确认:把判定结果标在散点图上看一眼,比看十行数字都管用。

4.4 把判定结果标回图上

这是整个流程里我最看重的一步。算完边界、拿到索引,用不同的颜色和大小把候选点圈出来,再存一张图。这张图既能自己复核,也能直接拿去跟业务方沟通——“你看这几个点,是录入错误还是真实业务”,比口头描述高效得多。

mask_out = (df['metric_b'] < lo) | (df['metric_b'] > hi) fig, ax = plt.subplots(figsize=(10, 6)) ax.scatter(df.loc[~mask_out, 'metric_a'], df.loc[~mask_out, 'metric_b'], s=14, alpha=0.5, c='#9bb7d4', edgecolors='none', label='常规样本') ax.scatter(df.loc[mask_out, 'metric_a'], df.loc[mask_out, 'metric_b'], s=90, alpha=0.9, c='none', edgecolors='#c0392b', linewidths=1.6, label='IQR 候选离群点') ax.axhline(lo, ls=':', lw=1.2, color='#c0392b') ax.axhline(hi, ls=':', lw=1.2, color='#c0392b') ax.set_xlabel('指标 A'); ax.set_ylabel('指标 B') ax.set_title('离群点标注结果(k=1.5)') ax.legend(frameon=False, loc='upper left') ax.grid(alpha=0.2) plt.tight_layout() plt.savefig('scatter_outlier.png', dpi=160, bbox_inches='tight')

图出来之后,重点看两件事。第一,被圈出来的点里,有没有明显属于主体分布内部的——如果有,说明阈值设得偏紧,或者这一列本身就偏态严重,该换方法。第二,主体分布边缘有没有明显该圈却没圈的——这通常是分布不规则导致的线性阈值失效,比如呈弧形分布的数据,用水平线切显然不合适。遇到这种情况,别硬套公式,把两个变量的比值或者差值做成新的一列再算 IQR,往往比在原始列上切更合理。

5. 出图过程中的问题排查实录

5.1 图不显示、中文变方块、负号错乱

这三件事几乎每个人都会遇到,而且报错信息不明确,容易卡住。图不显示,先看是不是少了%matplotlib inline,再看是不是在无界面环境跑了交互后端,最后检查plt.show()有没有被注释掉。中文变方块,是因为默认字体不含中文,设置font.sans-serif即可,注意字体名要跟系统里实际装的对上,Windows 常用 SimHei,Linux 服务器上未必有,得换成开源的思源黑体或者文泉驿。负号错乱表现为负号变成小方块,加一行axes.unicode_minus = False就好,原理是关掉 Unicode 负号,改用 ASCII 减号。

提醒:服务器上跑脚本时中文字体经常缺失,最省事的做法是图中文字全用英文,标题用英文短语,避免折腾字体。如果必须出中文图,把字体文件一起部署,别依赖系统自带。

5.2 点全糊成一团,看不出密度

数据量一大就容易糊。排查顺序:先看s是不是太大了,几百个点用 20、几千个点用 8、上万点用 3 到 4;再看是不是没设alpha,不透明度的点叠在一起只会显示最上层那个颜色;最后看figsize是不是太小,8×5 塞一万个点,神仙也看不出结构。这三项调完还不行,就是数据本身密度太高,上抽样或者 hexbin。

还有一种情况是两个变量的量级差距太大,比如横轴是 0 到 100 的评分,纵轴是几十万的金额,图上所有点都会贴在底部,看着像一条线。这时候最好的办法不是硬画,而是对广义跨度大的变量取对数,或者干脆拆成两张单变量图分别看。对数变换后记得在坐标轴标题里注明,不然过几个月自己都忘了自己做过变换。

5.3 时间轴刻度挤成黑边

横轴放日期的时候,刻度和标签会重叠成一坨黑。原因通常是把日期当成字符串直接传进去了,Pandas 没识别成时间类型。正确做法是先用pd.to_datetime转成 datetime64,Matplotlib 会自动用日期定位器。如果转完还是挤,两个办法:plt.xticks(rotation=45)旋转标签,或者用mdates.AutoDateLocator控制刻度间隔,让它在合理的粒度上自动选点。

df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce') print('转换失败条数:', df['order_date'].isna().sum()) fig, ax = plt.subplots(figsize=(11, 5)) ax.scatter(df['order_date'], df['metric_b'], s=10, alpha=0.5) ax.xaxis.set_major_locator(mdates.AutoDateLocator(maxticks=10)) ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) plt.setp(ax.get_xticklabels(), rotation=30, ha='right')

注意:to_datetime加上errors='coerce'之后,一定要数一下有多少条变成了 NaT。国内数据里日期格式混乱非常常见,“2023/1/5”“2023-01-05”“20230105”混在同一列里,转换失败的记录如果直接丢掉,样本量会悄悄缩水。

5.4 常见问题速查表

现象可能原因处理方式
图完全不显示缺内联魔法命令 / 后端不匹配加%matplotlib inline,脚本用 Agg 后端
中文显示为方块字体不含中文设置font.sans-serif为已装中文字体
负号显示异常Unicode 负号axes.unicode_minus = False
点糊成一团点太大 / 无透明度 / 画布太小降s、加alpha、放大figsize
全图只有一个点数据类型是字符串先用to_numeric转换
日期刻度重叠未转 datetimepd.to_datetime+ 自动定位器
保存图片空白保存前没画完 / 后端问题先tight_layout再savefig
保存图片被裁切标签超出画布bbox_inches='tight'
抽样后结论反复未固定随机种子random_state固定整数
异常点数量异常多数据本身偏态严重换稳健标准分或先做对数变换

6. 把这套动作固化成习惯

6.1 一个可以反复用的脚本骨架

单次分析靠手敲没问题,长期做数据的人应该把它固化成函数,每次拿到新数据换个列名就能跑。我自己的模板长这样,结构很土但好用:先做类型矫正,再对指定列两两画散点,同时输出 IQR 候选列表,最后统一保存。

def quick_scan(df, x, y, save_dir='./scan', k=1.5, sample_n=5000): import os os.makedirs(save_dir, exist_ok=True) d = df[[x, y]].apply(pd.to_numeric, errors='coerce').dropna() lo, hi = iqr_bounds(d[y], k) flag = (d[y] < lo) | (d[y] > hi) if len(d) > sample_n: d_plot = d.sample(sample_n, random_state=42) else: d_plot = d fig, ax = plt.subplots(figsize=(9, 5.5)) ax.scatter(d_plot[x], d_plot[y], s=10, alpha=0.45, edgecolors='none') ax.axhline(lo, ls=':', color='#c0392b', lw=1) ax.axhline(hi, ls=':', color='#c0392b', lw=1) ax.set_title(f'{x} vs {y}') ax.grid(alpha=0.2) plt.tight_layout() fig.savefig(f'{save_dir}/{x}__{y}.png', dpi=150, bbox_inches='tight') plt.close(fig) return {'x': x, 'y': y, 'lo': round(lo, 3), 'hi': round(hi, 3), 'hit': int(flag.sum()), 'total': len(d), 'rate': round(flag.mean() * 100, 2)}

这套骨架有三个设计考虑,值得说一下。第一,先 dropna 再统计,避免 NaN 干扰分位数计算,同时也顺便知道了这一对变量有多少条是完整的。第二,超过阈值就抽样画图,但统计用全量,绘图是给人看的,统计是给判断用的,两者可以分开。第三,每张图画完立刻close,批量跑几十对变量的时候不关图会吃掉大量内存,尤其在服务器上,跑到一半内存爆掉很常见。

输出的统计字典可以攒成一个列表,最后pd.DataFrame(rows).to_csv存成一张总表,按命中率排序。命中率特别高的那几列(比如超过 10%)基本可以判定是分布问题而不是异常点问题,要单独处理。

6.2 可复现性和留痕

做探查最怕的是“我上周好像看过,但结论忘了”。我的习惯是三个东西必须落盘:图的 PNG 文件、统计结果的 CSV、以及一份简短的文字结论(哪几列有问题、怎么判断的、准备怎么处理)。文字结论不用长,三五句话就行,但一定要写,因为一周之后你只会记得“好像有问题”,具体是什么问题基本想不起来。

随机种子统一用一个固定值,比如全部写random_state=42,别有的地方写 0 有的地方写 42,否则复现的时候对不上。列名和文件名保持一致,x__y.png这种命名方式看着丑,但用ls一扫就知道哪对变量画过、哪对没画,比任何花哨的命名都实用。还有一点,图一定要带上标题和坐标轴标签,哪怕是临时图。我自己吃过亏,做完一堆图直接扔进汇报材料,被问“这张是哪个变量”的时候翻代码翻了半天。

6.3 什么时候该停,什么时候该深挖

最后一个问题是节奏。探查是无穷无尽的,你永远能想出新的切分角度,但项目有 deadline。我的经验是设三道闸:第一道,核心业务列全部过一遍双变量散点,这一步别省;第二道,命中率超过 5% 的列单独深挖,看是分布偏态还是真的有问题;第三道,涉及金额、身份、时间的关键列做交叉校验,比如金额和件数是否匹配、日期先后是否合理。

三道闸过完,即使还有没看到的角度,也基本不会出大问题。反过来说,如果时间特别紧,至少把第三道闸走完,因为那几类列出问题的影响最大。我在实际项目里发现,绝大多数数据事故都发生在少数几个关键列上,把这几列吃透,收益远大于漫无目的地扫全表。

另外一个个人体会是:别在探查阶段做决定性的数据修改。发现异常点,先标记、先记录,把清洗逻辑单独放在一个脚本里,原始数据保持不动。我早期的坏习惯是在探查的 notebook 里边看边改,改着改着原始数据变成了中间状态,等到要复查某条记录时,已经找不回原样了。现在我的 notebook 里只干两件事:看图、记结论。清洗逻辑一律另开文件,写清楚每一步的输入输出,这样出了问题能一步步往回倒。

散点图这件事,说穿了就是几行代码,没有算法门槛,也不需要什么高级工具。但它解决的恰恰是最容易被跳过的问题:数据到底可不可信。图上多看一眼,后面能省下的返工时间,远比画图花掉的十几分钟多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 3:12:20

IDC机房交换机选型、组网与运维实战

IDC机房交换机这玩意儿&#xff0c;说白了就是机房里所有服务器之间、服务器和外面世界之间的那个总开关。平时没人惦记它&#xff0c;一出事全机房的人都盯着它看。我干了这些年&#xff0c;见过太多项目在服务器上砸了大钱&#xff0c;结果在交换机上省了几千块&#xff0c;最…

作者头像 李华
网站建设 2026/10/27 3:11:50

Claude Code Monitor工具:AI主动监控的技术突破与应用

1. Claude Code Monitor工具的技术革新在AI技术快速发展的今天&#xff0c;Claude Code推出的Monitor工具标志着AI应用从传统的"被动问答"模式向"主动监控"模式的重大转变。作为一名长期关注AI技术落地的从业者&#xff0c;我深刻理解这一转变背后的技术挑…

作者头像 李华
网站建设 2026/10/27 3:11:10

Ubuntu APT镜像源配置避坑与优化指南

1. Ubuntu系统APT镜像源配置避坑指南作为Linux系统管理员&#xff0c;我最近在帮团队配置Ubuntu服务器时踩了个大坑——错误使用了腾讯云的APT镜像源导致系统更新异常。这个问题看似简单&#xff0c;实则涉及软件源信任链、数字签名验证和系统稳定性等关键机制。今天就把这个价…

作者头像 李华
网站建设 2026/10/27 3:11:02

虚拟机Linux磁盘报错根源:SMBus警告、clean日志与resize卡死解析

1. 这三个报错不是巧合&#xff1a;它们共同指向虚拟机Linux磁盘管理的底层断层你刚在VMware或VirtualBox里装好一个Ubuntu Server&#xff0c;启动时黑屏几秒后跳出一行灰底白字&#xff1a;SMBus Host Controller not enabled&#xff1b;等系统勉强进去了&#xff0c;df -h一…

作者头像 李华
网站建设 2026/10/27 3:10:57

2025年客户端技术趋势与端侧AI实践

1. 2025年客户端技术全景扫描过去一年客户端技术领域呈现出明显的"端侧智能化"趋势&#xff0c;各大平台的技术演进都围绕三个核心方向展开&#xff1a;更自然的交互体验、更高效的渲染性能、更智能的本地计算能力。让我们先看几个关键数据&#xff1a;全球移动设备端…

作者头像 李华
网站建设 2026/10/27 3:10:36

网状数据库模型原理与现代启示:导航式存储的兴衰逻辑

简介&#xff1a;本资源是《数据库原理&#xff08;第四版&#xff09;》配套教学课件&#xff0c;面向高校计算机、软件工程及信息管理类专业本科生与数据库初学者&#xff0c;系统解决数据库基础理论与核心模型理解难题。课件以PPT格式呈现&#xff0c;共1个文件&#xff0c;…

作者头像 李华