☰
Pandas数据清洗到可视化全流程实战,附高频避坑指南
2026/10/22 8:07:32 网站建设 项目流程

拿到一份导出好的销售明细表、用户行为日志或者问卷数据,第一反应是什么?很多新手会直接pd.read_csv()然后立刻开始groupby()画图。但如果数据本身是乱的——日期格式不统一、金额字段混着文本、同一客户重复出现三次、缺失值东一块西一块——你画出来的图再漂亮,结论也站不住脚。我做数据分析这几年,最大的体感是:真正花时间的从来不是建模和画图,而是数据进到模型和图表之前的清洗和处理。这篇就以Pandas为主线,从一份典型的脏数据出发,完整走一遍数据清洗、字段加工、聚合分析到可视化的流程。适合刚入门想建立完整工作流的人,也适合已经会用dropna()但没理清整个处理逻辑的同学。

1. 拿到数据别急着写代码,先想清楚三件事

1.1 先搞清楚“要回答什么问题”

这听起来像废话,但真实项目里最容易翻车的就是这一步。数据清洗没有绝对标准,同样是缺失值,如果你要做销售总额统计,缺失的订单金额必须处理;如果你只是在画客户地区分布图,那条缺失金额的记录可能根本不影响结果。

拿到数据后,我习惯先写三行注释:

# 业务问题:哪个产品线毛利贡献最高?最近三个月趋势如何? # 需要的字段:产品类别、销售额、成本、订单日期 # 输出形式:月度毛利趋势图 + 产品线毛利占比排名

这一步能帮你决定清洗到什么程度。比如只需要和订单相关的字段,那收货地址里的乱码可以暂时不管;如果要做客户画像,那地址和联系方式就必须逐条清理。先定义“干净”的标准,效率会高很多。

1.2 用df.info()和df.head()给数据“照个CT”

建议任何数据到手,先跑这两个方法,不要急着describe()。df.info()看的是骨架——每一列的类型、非空数量、内存占用;df.head()看的是长相——前几行数据的格式是不是正常人类能看懂的样子。

我常用的开场代码是这样的:

import pandas as pd df = pd.read_csv('sales_data.csv', encoding='utf-8') df.info() print(df.head(10))

注意看几个信息:dtype列出的字段类型符不符合直觉(比如“成交时间”如果显示object而不是datetime,就说明格式有问题);非空数量是否明显少于总行数;再扫一遍head()里有没有“不详”“暂无”“N/A”之类的异常文本。这些脏数据往往不会直接显示为NaN,而是藏在字符串里,只靠isnull()根本查不出来。

1.3 先备份一份原始数据

这一步很重要,每轮清洗之前,先存一份原始版本:

df_raw = df.copy()

清洗是破坏性操作。你可能觉得某列全删了没问题,后来说要按那个字段细分,发现回不去了。copy()一下成本极低,但能让所有试错都有后悔药。真实工作里我见过不止一次因为清洗前没备份导致整个流程重跑的情况,这种低级坑真的踩一次就长记性了。

2. 数据清洗实操:把“脏数据”处理成“干净数据”

2.1 缺失值:先判断机制,再决定删还是补

处理缺失值,核心不是“怎么补”,而是“为什么缺失”。

调查数据里“家庭年收入”这一列大量为空,很可能意味着收入高的人不愿意填;电商订单里“退款金额”为空,则可能只是该订单没有退款。前一种缺失带有系统性偏差,直接丢弃会扭曲结论,需要结合业务判断;后一种缺失通常可以直接视为0。千万别一上来就df.dropna(),那样很可能把有效样本删掉一大半。

我通常先做一次缺失概览:

miss_rate = df.isnull().sum() / len(df) miss_rate = miss_rate[miss_rate > 0].sort_values(ascending=False) print(miss_rate)

对于缺失率很低的字段(比如低于1%),如果数据量够大,直接删除该行通常影响不大。对于关键数值字段,可以先画个分布、看均值和中位数再决定填充策略:

  • 数值字段偏正态分布,用均值填充。
  • 数值字段偏态严重(比如收入、价格),用中位数填充更稳。
  • 类别字段,用众数填充,或者单独增加一个“缺失”类别。
  • 时间序列数据,优先用ffill()或bfill()做前后填充。
df['price'] = df['price'].fillna(df['price'].median()) df['category'] = df['category'].fillna('未知') df['sales'] = df['sales'].fillna(0)

注意:填充本身是“引入假设”的操作,填充之后最好在分析结果里注明,别让看报告的人以为这些值是真实记录。

2.2 重复数据:区分完全重复和部分重复

df.duplicated()只会判断整行完全重复。但真实场景里更常见的是“部分重复”——同一订单号出现两行,其他字段略有差异,或者是同一客户在“客户ID”维度重复,但每次购买的产品不同。

先看完全重复:

print(df.duplicated().sum()) df = df.drop_duplicates()

再看“关键列重复”:

dup_keys = df[df.duplicated(subset=['order_id'], keep=False)] print(dup_keys.sort_values('order_id').head(20))

如果同一订单号有多行,需要先搞清楚是正常的“一单多品”(一行一个商品)还是真的重复录入。一单多品是正常结构,强行去重会把有效数据删掉;重复录入则需要保留其中一行。判断方法很简单:看其他字段是否完全相同,如果金额、日期、数量全一致,基本可以断定是重复。

df = df.drop_duplicates(subset=['order_id', 'product_id'])

这里我用了订单号和产品号一起作为唯一标识,处理一单多品的场景就比较稳。

注意:去重前先想清楚“唯一性”的定义是什么。不是数据里没有重复,而是你的业务逻辑允许什么样的重复存在。

2.3 异常值:不是所有偏离都是脏数据

最常见也最坑的异常值来源是输入错误——比如销售数量多了个零变成15000,或者单价被写成负数。但也不能所有极端值都一刀切删掉,因为真实业务本身就存在极端值(大客户一次性采购金额本来就高)。

先看描述统计,心里有个底:

df[['sales_amount', 'quantity']].describe()

describe()里的min和max最能暴露问题。如果一个订单数量最大99999,最小-10,那基本可以断定有问题。我用两种方式定位:

  • 用IQR(四分位距)做初步筛选:低于Q1-1.5xIQR或高于Q3+1.5xIQR的数值先标出来看一眼。
  • 再看业务逻辑:数量不能为负,折扣不能大于1,金额不能为0但数量不为0。

可疑值先标个标记列,而不是直接删:

Q1 = df['quantity'].quantile(0.25) Q3 = df['quantity'].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR df['quantity_outlier'] = (df['quantity'] < lower) | (df['quantity'] > upper)

然后逐个看标记出来的行是不是输入错误。异常值处理原则是:能追踪到原因的(比如小数点多打一位)就修正;无法验证的,要么剔除,要么在报告里单列出来说明。千万不要让几个异常值悄悄拉偏了你的均值和图表比例,那比不清理还危险。

2.4 统一数据格式:字符串里的“隐形脏”

很多表看着整洁,但仔细一抠全是问题。我总结最常见的几类:

  • 日期格式混乱:同一列里“2024-01-01”“2024/1/1”“01-01-24”混着来。
  • 数值被写成字符串:“1,200元”或者“1200 “(带空格)。
  • 文本大小写不一:“APPLE”“Apple”“apple”本应归为一类。
  • 全半角符号混用,中文括号和英文括号同时存在。

日期统一是最典型的,用pd.to_datetime()解决:

df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce')

errors='coerce'会让无法解析的值变成NaT(缺失时间),这样事后能统计出有多少条日期异常。

数值清洗更常见,比如金额字段里混了逗号和货币符号:

df['sales_amount'] = ( df['sales_amount'] .astype(str) .str.replace('元', '') .str.replace(',', '') .astype(float) )

文本类型统一可以通过str.lower()或者str.strip()。做客户地域分析时,“广东”“广东省”“广东省广州市”三个值会被当成三种地区,如果不做归一化,饼图里就会出现一堆碎片。数据清洗的价值在这里体现得最直接——它决定了你后面统计的每一张表格是否有意义。

3. 字段加工与聚合:从“能用”到“好用”

3.1 类型转换与时间字段拆解

清洗完的数据可以用了,但通常还不能直接拿来分析。最典型的是时间字段——你知道order_date是datetime类型了,但要分析月份趋势、星期规律、季度分布,还需要拆出具体的时间成分。

df['year'] = df['order_date'].dt.year df['month'] = df['order_date'].dt.month df['quarter'] = df['order_date'].dt.quarter df['weekday'] = df['order_date'].dt.dayofweek # 0=周一, 6=周日

dt访问器是Pandas里非常好用的一个接口,处理datetime类型的数据时,可以快速提取年、月、日、周、季度等成分。这里有一个隐藏的好处:把时间拆成独立列之后,后续groupby(['year', 'month'])的聚合代码可读性会好很多,而且不容易在日期格式上再犯错。

类别字段也建议转为category类型,尤其当某个列的重复值特别多时:

df['category'] = df['category'].astype('category')

这能降低内存占用,某些操作(如groupby)速度也会有提升。更重要的是,它让Pandas知道这是一个有固定取值的维度列,后续做交叉表时会自动按类别排序,不会乱序。

3.2 用transform和apply自己做字段加工

数据分析里经常需要“按组计算,再映射回每一行”的操作。比如计算每个产品类别的平均销售额,你希望给每个订单打上“是否高于该类别的平均销售额”标记。新手最容易这样写错:

# 错误示范:groupby之后的行数变了,直接赋值会报错 # df['avg_by_cat'] = df.groupby('category')['sales_amount'].mean()

groupby之后返回的是每个类别一条记录,直接用会导致索引对不上。正确做法是用transform,它会保留原始索引,把计算结果映射回每一行:

df['avg_sales_by_cat'] = df.groupby('category')['sales_amount'].transform('mean') df['sales_above_cat_avg'] = df['sales_amount'] > df['avg_sales_by_cat']

transform和apply的区别在于:apply大概率是把结果压缩成组级别的汇总,transform是把组级别的计算结果广播回每一行。理解了这一点,就能避免非常常见的“行数不匹配”报错。

apply也可以写,但要注意性能。我经常用apply来处理一些没有内置向量化方法的逻辑,比如给产品按价格区间打标签:

def price_tier(price): if price < 50: return '低单价' elif price < 200: return '中单价' else: return '高单价' df['price_tier'] = df['unit_price'].apply(price_tier)

3.3groupby聚合:明确“粒度”后再动手

groupby是数据分析的核心操作,但它最大的陷阱在于很多人没想清楚粒度的变化。拿订单明细表来说,一行是一笔订单里的一个商品;如果想统计“平均每笔订单的金额”,直接用订单明细的sales_amount.mean()是错的,因为一个订单可能有多个商品行。

正确做法是先从明细表聚合到订单级:

order_level = df.groupby('order_id', as_index=False).agg( total_amount=('sales_amount', 'sum'), total_quantity=('quantity', 'sum'), order_date=('order_date', 'first'), category_list=('category', lambda x: '、'.join(x.unique())) )

agg可以传多个统计方式进去,比如同时算总和、均值、计数、最大最小。命名用新列名=(原始列名, 聚合方式)这种写法,代码清晰又灵活。这里建议:聚合前先明确“我要计算的最小业务粒度是什么”,这句话会帮助你想清楚groupby到底应该按哪些列分组。

多维分组也很常见,比如按“区域 + 季度”统计销售额:

result = df.groupby(['region', 'quarter'], as_index=False)['sales_amount'].sum()

得到的result就是一个规整的聚合表,直接扔给绘图函数,或者再透视成行列结构,都很方便。

4. 用可视化的方式把分析结果“讲出来”

4.1 先算后画,避免为了画图而画图

数据可视化不是“把表变成图”这么简单。它首先是一个信息筛选的过程——你得知道这个图想表达什么观点,才谈得上选什么图类型。

我见过很多把散点图、箱线图堆一屏的报表,看起来炫酷,但看完没记住任何结论。正确路径是先聚合计算,把结论想清楚,再用图表支撑结论。比如:“各产品线毛利贡献占比”是饼图或条形图;“月度销售趋势”是折线图;“不同区域的客单价差异”是箱线图。先算出数,再决定怎么画。

4.2 pandas内置绘图 vs matplotlib

Pandas每个Series和DataFrame都有一个.plot方法,底层调的是matplotlib。它的最大优势是API顺手,适合快速出图。比如前面聚合出的result:

import matplotlib.pyplot as plt # 解决中文显示问题 plt.rcParams['font.sans-serif'] = ['SimHei'] # Windows plt.rcParams['axes.unicode_minus'] = False result.plot(x='quarter', y='sales_amount', kind='bar') plt.show()

Pandas内置绘图适合做探索性分析,你不需要关心坐标轴怎么设置,几秒钟就能看到趋势。但它的缺点是定制能力有限,一旦要调整图例、颜色、标签位置,就得直接用matplotlib。更推荐的做法是把聚合结果准备好,然后用matplotlib画图,这样可控性最好:

fig, ax = plt.subplots(figsize=(10, 6)) ax.bar(result['quarter'].astype(str), result['sales_amount'], color='#4C72B0') ax.set_title('2024年各季度销售额趋势') ax.set_xlabel('季度') ax.set_ylabel('销售额(元)') plt.tight_layout() plt.show()

注意一个小细节:用result['quarter']直接作为x轴时,如果它是整数列,matplotlib会把它当数值刻度,出来的图可能不是等距分类轴。转成str再画会更稳定,这是很容易被忽略的坑。

4.3 从不同维度设计图表组合

单一图表通常只能回答一个问题,组合图表才能支撑起一份完整分析。我常用的思路是这样的:

  • 先画总览:比如各区域销售额汇总条形图,快速看大盘分布。
  • 再画时序:选择一个重点区域或产品线,画月度趋势折线,看涨跌。
  • 最后切细分:用透视表对“区域 × 产品类别”做交叉分析,画堆叠柱状图。

透视表配合图形是很好的组合拳:

pivot_tbl = pd.pivot_table( df, index='region', columns='category', values='sales_amount', aggfunc='sum', fill_value=0 ) pivot_tbl.plot(kind='bar', stacked=True, figsize=(12, 7)) plt.title('各区域不同产品类别的销售额结构') plt.tight_layout() plt.show()

pd.pivot_table在这里的作用是把数据从“明细长表”变成“透视宽表”,每一格是一个交叉维度的聚合值。fill_value=0这个参数很重要,它避免出现NaN导致画图时某个柱子缺失一段。这样一张堆叠图,既能看出各区域的绝对体量,也能看出品类结构差异,信息量很大。

另外一个很实用的图是箱线图,专门用来看分布。比如比较不同价格档位的订单金额分布:

df.boxplot(column='sales_amount', by='price_tier', figsize=(10, 6)) plt.suptitle('') plt.title('不同价格档位的订单金额分布') plt.show()

注意:df.boxplot()默认会生成一个多余的suptitle,加一行plt.suptitle('')可以去掉它,否则标题会重复。

画完图不是终点。导出图表和数据,写清楚结论是最后一步。我的习惯是每张图都配一句“看图结论”,比如“华东区域二季度销售额下降明显,主要由通讯产品线贡献下滑导致”,这样图表才真正变成了决策依据。

5. 常见问题与排查技巧实录

5.1 高频报错速查表

报错信息出现原因解决办法
KeyError: 'xxx'列名不存在,可能多了空格或大小写不一致df.columns查看真实列名,用df.rename修正
ValueError: cannot reindex两个DataFrame索引对不上检查groupby后索引是否保留,必要时用.reset_index(drop=True)
TypeError: unsupported operand type(s)字符串和数值混着做运算先pd.to_numeric转换,errors='coerce'处理脏值
ParserErrorCSV格式异常,可能分隔符或编码不对指定sep和encoding,用engine='python'兜底
SettingWithCopyWarning在分片后的DataFrame上做修改要么用.loc显式操作,要么先.copy()再改

最后这个SettingWithCopyWarning是新手最容易遇到但最容易忽略的。它通常不是报错,而是警告诉你“你可能在改一个视图而不是原始数据”。我见过有人对一个筛选出的子集赋值,结果原数据没变,后面分析全乱了。最稳妥的规避方式,是在筛选之后马上.copy():

sub_df = df[df['region'] == '华东'].copy() sub_df['flag'] = 1

加了.copy()之后,sub_df和原DataFrame就完全分开了,怎么改都不会互相影响。

5.2 几个容易忽视的细节坑

第一个是CSV的编码问题。早期项目里我习惯直接read_csv不指定编码,直到某次读取发现所有中文都变成乱码,才去排查编码。比较稳的做法是优先utf-8,如果报UnicodeDecodeError再换gbk。最稳妥的是用chardet或chardet类库先探测编码,不过大多数时候utf-8加errors='ignore'也能保底。

第二个是groupby之后reset_index的问题。很多人写df.groupby('region')['sales'].sum()之后,随后又想把这个Series转成DataFrame或者按其他列merge,发现索引对不上。这里有个小习惯:groupby聚合时加上as_index=False,让分组列变成普通列而不是索引,能省掉很多后续麻烦。

第三个是Excel文件的格式坑。read_excel读取时如果表头上方有标题行,要用header=1跳过;如果文件里有多个sheet,要先明确sheet_name。这种问题看起来简单,但在真实数据里出现频率远比你想象的高。

5.3 关于性能:大数据量下的实用优化

数据量上了几十万行,有些写法会明显变慢。三个实用优化技巧:

尽量用向量化操作,避免循环。df['col'].str.replace()远快于for i in range(len(df));针对复杂逻辑可以先定义函数再用apply,也比显式循环好。

聚合时先删掉不需要的列。groupby之前的DataFrame如果列很多,先把分析无关的字段drop掉,内存占用和计算量都能降。我在处理百亿行级别的日志时,每次只保留需要的5-6列,速度能快好几倍。

按需分批读取。文件太大时用pd.read_csv(..., chunksize=100000),分块处理再合并。虽然Pandas本身不是为超大数据集设计的,但配合分块处理,几千万行的日常数据完全够用。

写在最后

整套流程走下来,你会发现数据分析真正吃功夫的地方不在画图那一下,而在前面那些看似繁琐的清洗和加工里。我自己刚接触Pandas时也很不耐烦,总想赶紧出结果,后来踩过几次坑才明白:数据质量的底子不打牢,后面做再多分析都是沙上筑塔。现在每个项目,我都会把“数据清洗清单”当作正式交付物的一部分——哪些字段做了去重、哪些缺失值用什么方式填充、异常值怎么处理,都会记录清楚。这不仅是对自己负责,也是让看报告的人对结论有信心的基本素养。分析工具一直在变,但“数据先行、逻辑清楚、结论可验证”这套工作习惯,放哪个项目里都不过时。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询