news 2026/10/8 2:31:57

Pandas数据清洗与可视化实战:从脏数据到业务分析图表

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pandas数据清洗与可视化实战:从脏数据到业务分析图表

第一次拿到一份两万行的销售明细表时,我的反应是:读进来,跑个 describe(),完事。结果呢?日期列是“2024/1/5”和“20240105”混着的文本,金额列里夹着“¥1,234.56”这种让人无从下手的字符串,订单号还有两百多条重复。那天的教训让我明白一件事:在 Pandas 里面,百分之八十的功夫根本不在建模,而是在数据清洗。这篇东西就是我想分享给你们的一套完整实操路径,从用 Pandas 把脏数据一点点收拾干净,到类型转换,再到最后画出能直接拿去汇报的图表。适合刚学完 Pandas 语法、但面对真实数据还是有点发怵的同学;当然,如果你已经写过不少脚本,里面也有一些可能被你忽略的细节。

1. 拿到数据别急着跑模型,先给数据做个体检

1.1 用 shape、info() 和 describe() 快速摸清底细

我见过太多新手拿到 CSV 的第一件事就是df.groupby(...)或者直接塞进机器学习模型,结果被各种报错按在地上摩擦。Pandas 处理数据的第一步,永远不是"处理",而是"看清"。具体来说,我的体检四件套是这样的:

import pandas as pd df = pd.read_csv("sales.csv", encoding="utf-8") print(df.shape) # (行数, 列数),先确认规模 df.info() # 每列的非空数量 + 数据类型 df.head(10) # 肉眼看前10行,比任何统计量都直观 df.describe() # 数值列的均值/标准差/分位数

shape告诉你有多少行多少列。info()是你最容易忽略但最有价值的东西:它会把每一列的dtype、非空值个数全部列出来。我通常先扫一眼info(),就能立刻定位出"哪一列缺得厉害"、"哪一列类型明显不对"。比如订单金额列显示的object而不是float64,你就得警惕了。

describe()则是给数值列做一次快速体检,重点看的是min和max。如果订单金额的最小值是 -1000,最大值是 999999,说明数据里既有负数又有异常离谱的大额,这两个地方就够你后续折腾一阵了。

1.2 脏数据最常见的四种毛病

根据我的经验,真实数据翻来覆去就那么几种病,记熟了之后清洗思路会很清晰:

毛病典型表现简单判断方式
类型错乱日期是字符串、金额是文本info()里 dtype 与预期不符
缺失值单元格为空、NaNdf.isnull().sum()统计
重复数据同一订单出现多次df.duplicated().sum()统计
异常值负金额、超大数据df.describe()看 min/max

这四种病在大多数实际表格里是叠加出现的。所以我的习惯是先做一次完整"体检",记下哪里有毛病,再统一动手,而不是看到一个问题改一个问题,改到后面自己都忘了改过什么。

2. 类型转换与缺失值处理:数据清洗的主战场

2.1 把"长得像数字"的字符串变成真正的数字

to_numeric()是我在清洗阶段用得最频繁的函数,没有之一。真实数据里的数字列经常长这样:"1,234.56"、"¥120"、"约300人"。如果你直接astype(float),等待你的必然是 ValueError。

正确做法是先把非数字字符去掉,再转换。我常用的是这种组合拳:

# 金额列:去掉货币符号和千分位逗号 df["amount"] = df["amount"].astype(str).str.replace("¥", "").str.replace(",", "") df["amount"] = pd.to_numeric(df["amount"], errors="coerce")

注意errors="coerce"这个参数,它会把"实在转不了"的值变成 NaN。这样既不会报错中断脚本,又会让脏数据暴露在缺失值层面,之后统一处理。如果不用coerce,遇到一个"未知"这样的字符串,整个转换过程就崩了。

还有一个小坑:有些表格里的数字是用全角字符写的,比如"500"。这在从网页导出的数据里非常常见。我一般会在替换阶段顺手做一次全角转半角:

def to_half_width(s): return "".join(chr(ord(ch) - 0xFEE0) if ord('!') <= ord(ch) <= ord('~') else ch for ch in str(s)) df["amount"] = df["amount"].apply(to_half_width)

2.2 日期时间转换:to_datetime()的宽容度比你想象大

日期列是另一大重灾区。常见的情况有五种:2024-01-01、2024/1/1、20240101、01-Jan-2024、2024年1月1日。如果是刚学 Pandas,可能会想写一堆正则去拆字符串,其实pd.to_datetime()已经内置了很强的解析能力:

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

大部分常见格式它都能识别。真正难缠的是那种"不同行格式不一样"的脏数据,比如同一年份列里既出现"2024-01-05"又出现"20240105"。这种情况建议先看看到底有几种格式:

df["order_date"] = df["order_date"].astype(str) formats = df["order_date"].str.replace(r"\d", "", regex=True).unique() print(formats) # 看看非数字部分是哪些符号

如果只是/和-混用,可以先统一替换后再转换。转换完之后,一定要做一次抽查:

df["year_month"] = df["order_date"].dt.to_period("M")

.dt这个访问器只有真正的 datetime 类型才能用。如果.dt报错,说明你的转换没有成功,那就要回头处理 format 问题了。

2.3 缺失值的处理,先想业务含义再动手

fillna()和dropna()很容易,难的是"该填还是该删"。我给个自己常用的决策思路:

场景处理方式理由
缺失超过 50%,且没有可靠的填充依据直接删列或删行留着的价值不大,反而污染分析
缺失代表"没有发生"(如退款金额)fillna(0)数值含义明确,缺失等于 0
缺失是统计口径需要的连续值用中位数或均值填充避免均值被异常值拉偏,优先中位数
缺失是分类标签单独填"未知"保留"缺失"本身也是信息

举例子:电商订单表的"退款时间"列缺失,并不代表数据出错,而是这笔订单根本没退款,填成NaT或0都对。但"用户年龄"缺失就不能拍脑袋填 0,填 0 会把均值毁掉,我会优先用中位数。

df["refund_time"] = df["refund_time"].fillna(pd.NaT) df["age"] = df["age"].fillna(df["age"].median())

2.4 重复数据:drop_duplicates()的 subtle 陷阱

drop_duplicates()看起来一行搞定,但我建议永远带上subset参数,并且先想清楚"重复"的定义是什么。两份一模一样的行是重复;更常见的是同一个订单号对应多行,但金额行之间略有差异。

df = df.drop_duplicates(subset=["order_id"], keep="first")

keep参数也很关键。默认保留第一条,但如果后续数据比前导数据完整,你要考虑keep="last"。我见过有人在做订单去重时,保留了一条金额为 0 的记录,丢掉了正确的那条,最后月度营收少了一位数。去重后记得核一下:

print(df["order_id"].nunique()) # 去重后的唯一数 print(df.shape) # 和去重前对比,确认删掉了多少

3. 筛选、排序和分组:让数据开始"说话"

3.1 loc、iloc 与布尔筛选,别再用 Python 循环了

数据清洗干净之后,下一步是筛选与分析。很多新手习惯写 for 循环逐行判断,这在数据量上了万行之后会慢得让人崩溃。Pandas 的向量化操作快得多,而且代码更短。

# 只看上海地区的有效订单 shanghai = df.loc[df["city"] == "上海", ["order_id", "amount", "order_date"]] # 金额大于 500 且不是退款单 big_orders = df.loc[(df["amount"] > 500) & (df["is_refund"] == 0)] # 按金额排序,取前 10 top10 = df.nlargest(10, "amount")

我强调用loc而不是直接df[...],是因为loc可以让"行筛选"和"列选择"一起完成,结果更明确。nlargest()和nsmallest()也是被低估的函数——想取 Top N,别再sort_values()之后再head(N)绕圈子了。

3.2 groupby 聚合:从明细表到业务指标

筛选只是第一步,真正让数据"说话"的是聚合。groupby()的常规操作是"分组 + 聚合函数",但很多人只会sum()和mean()。我这里列几个我常用的组合:

# 按月统计销售额、订单数和客单价 monthly = df.groupby("year_month").agg( 销售额=("amount", "sum"), 订单数=("order_id", "nunique"), 客单价=("amount", "mean") ).reset_index()

注意nunique在统计"订单数"时非常重要。如果直接count(),它数的是行数,而同一订单可能拆成了多行(比如一个订单买了三个商品就是三行)。用nunique()才能算出真实订单数量。这个细节,年报口径出错往往就出在这里。

agg()的好处是一次性把多个统计指标写清楚。如果只用groupby().sum(),后续还得再groupby().mean()再来一轮,效率低且容易绕晕。

3.3 数据重塑:pivot_table 与 melt

真实业务里,表格经常"长得不顺手"——你想要的是一张交叉表(行是月份、列是地区),但原始数据是一张流水明细。这时pivot_table()是救星:

pivot = pd.pivot_table( df, index="year_month", columns="city", values="amount", aggfunc="sum", fill_value=0 )

反过来,有时候我们需要把宽表变成长表给可视化工具用,melt()就派上用场了。我做过一个门店销售报表,原始数据是"每个门店一列、日期一行",但画图工具要求"日期一列、门店一列、销售额一列",这时候:

long_df = df.melt( id_vars=["date"], value_vars=["北京店", "上海店", "广州店"], var_name="city", value_name="amount" )

id_vars是要保留的标识列,value_vars是"我要拆成行的列"。这几乎是所有"宽转长"需求的模板,建议直接背下来。

4. 可视化:先想清楚回答什么问题,再动手画图

4.1 一张图回答一个问题

可视化最大的误区是"为了画图而画图"。我见过有人随手df.plot()出来一张乱七八糟的折线图,坐标轴重叠、图例分不清,最后自己都解释不了。我现在的流程是:先写下我要回答的问题,再选图。

你想回答的问题推荐图形Pandas 实现
某个指标随时间怎么变化折线图df.plot(x="month", y="sales")
不同类别的量级对比柱状图df.plot(kind="bar", x="category", y="amount")
两个指标之间的相关性散点图/热力图df.plot(kind="scatter", x="a", y="b")
各部分占比饼图/堆积柱状图df.plot(kind="pie", y="amount")

用df.plot()的好处是它直接基于 DataFrame,底层是 matplotlib,写起来比plt.plot()少几行。但一旦需要定制细节,还是得落到 matplotlib 或 seaborn 上。

4.2 折线图、柱状图和热力图的实战写法

以我前面整理出的monthly表为例:

import matplotlib.pyplot as plt import seaborn as sns plt.rcParams["font.sans-serif"] = ["SimHei"] # 中文字体,Mac 换 [Arial Unicode MS] plt.rcParams["axes.unicode_minus"] = False # 解决负号显示成方块的问题 fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(monthly["year_month"].astype(str), monthly["销售额"], marker="o") ax.set_title("月度销售额趋势") ax.set_xlabel("月份") ax.set_ylabel("销售额") plt.xticks(rotation=45) plt.tight_layout() plt.show()

两个中文字体设置参数一定记得写上,不然图表里全是小方块,这是所有用 Pandas 画图的中文用户都会遇到的第一道坎。seaborn我通常用来画热力图,比如看不同商品类目在周几的销量关系:

heat_data = df.pivot_table(index="category", columns="weekday", values="amount", aggfunc="sum") sns.heatmap(heat_data, cmap="YlOrBr", annot=True, fmt=".0f") plt.show()

热力图的好处是能一眼发现"周四的数码产品销量异常高"这类肉眼难以察觉的规律。annot=True会把具体值标在格子里,汇报时非常直观。

4.3 让图表"能进会议室"的三个细节

我自己的经验,画图想从"自己看懂"升级到"汇报可用",至少要做三件事:加标题、加轴标签、调图例。很多 Pandas 默认图是没有标题的,直接拿去群里发,别人根本不知道横轴是什么。

# 一个细节:对图表尺寸和 dpi 做统一控制 fig, ax = plt.subplots(figsize=(8, 4), dpi=120) # 一个细节:数值轴的千分位格式化 ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x:,.0f}"))

第二行代码能解决"销售额轴上的数字是 1234567,读起来太费劲"的问题。这在大数值场景(营收、UV)几乎是刚需。

5. 综合实战:一份电商订单数据从清洗到可视化的完整过程

5.1 场景与原始数据

这一节我把前面所有东西串起来。假设你拿到一份orders.csv,字段包括order_id、order_date、city、category、amount、quantity、refund_time。已知问题:金额列带¥和逗号;日期格式混杂;有重复订单;部分quantity为 0 或负数;退款时间大量缺失。我建议你们跟着这个顺序走,不容易乱。

5.2 清洗流程示例

# 1. 读取 df = pd.read_csv("orders.csv", encoding="utf-8") # 2. 体检 print(df.info()) print(df.duplicated(subset=["order_id"]).sum()) # 3. 类型转换 df["amount"] = df["amount"].astype(str).str.replace("¥", "").str.replace(",", "") df["amount"] = pd.to_numeric(df["amount"], errors="coerce") df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce") df["refund_time"] = pd.to_datetime(df["refund_time"], errors="coerce") # 4. 去重 df = df.drop_duplicates(subset=["order_id"], keep="first") # 5. 处理缺失与异常 df = df.dropna(subset=["order_id", "order_date", "amount"]) df = df.loc[df["quantity"] > 0] df["refund_time"] = df["refund_time"].fillna(pd.NaT) # 6. 新增分析列 df["year_month"] = df["order_date"].dt.to_period("M") df["weekday"] = df["order_date"].dt.dayofweek

每个步骤的顺序是有讲究的。先去重再做缺失值处理,是因为重复行可能带着不同的缺失状态;先做类型转换再做异常值筛选,是因为"¥1,234.56"如果不转成 float,就没办法判断> 0。我一开始也习惯想到哪写到哪,后来发现清洗步骤的顺序混乱会导致"你以为清洗完了,结果后面建模又报错"。

5.3 分析与可视化输出

清洗完之后,就可以进入分析和可视化环节:

# 按月销售额趋势 monthly = df.groupby("year_month").agg( 销售额=("amount", "sum"), 订单数=("order_id", "nunique") ).reset_index() plt.figure(figsize=(10, 5)) plt.plot(monthly["year_month"].astype(str), monthly["销售额"], marker="o") plt.title("月度销售额趋势") plt.xticks(rotation=45) plt.tight_layout() plt.show() # 各城市销售额对比 city_sales = df.groupby("city")["amount"].sum().sort_values(ascending=False) plt.figure(figsize=(8, 4)) city_sales.plot(kind="bar", color="skyblue") plt.title("各城市销售额") plt.xlabel("城市") plt.ylabel("金额") plt.show() # 退款率分析 refund_rate = df.groupby("city")["refund_time"].apply( lambda x: x.notna().mean() ).sort_values(ascending=False) print(refund_rate)

到这里,一份能用于汇报的图表就出来了。注意refund_time的notna().mean()这种写法——对布尔列求均值,得到的就是"退款率"(有退款时间的单量占比)。这是 Pandas 里一个非常实用的小技巧,比你自己用sum / count干净得多。

6. 我在 Pycharm 里装 Pandas 和处理环境问题时踩过的坑

6.1 安装慢、装不上,先检查 Python 环境和 pip 源

身边不少朋友卡在"装不上 Pandas"这一步。如果你在 PyCharm 里用pip install pandas卡了半天,多半是网络源的问题。我自己一直用清华源种 PyPI 镜像,安装速度快很多:

pip install pandas -i https://pypi.tuna.tsinghua.edu.cn/simple

在 PyCharm 里如果装不成功,先确认你用的解释器是哪个 Python 版本,以及 Python 是 3.8 还是 3.12。Pandas 对较新版本的 Python 适配偶尔会有延迟,这时你可以用python -m pip install --upgrade pip先把 pip 升级。还有一个常见错误:报错信息里出现microsoft visual c++ 14.0 is required,这是 Windows 环境下缺编译工具,解决办法是直接下载对应的.whl文件,或者安装 Visual C++ Redistributable。与其在编译上死磕,不如直接换一个带预编译包的镜像装。

6.2 清洗完别忘保存:to_csv 的编码问题

如果你在 Windows 上把清洗好的数据用to_csv()存下来,再用 Excel 打开,发现中文全变成乱码了,不要慌,这是 UTF-8 编码和 Excel 默认编码不一致导致的。解决办法是保存时指定编码:

df.to_csv("clean_orders.csv", index=False, encoding="utf-8-sig")

utf-8-sig会写一个 BOM 头,Excel 打开就不会乱码。如果你要给别人发数据,我建议直接上这个编码。index=False则是不把行索引写进文件,这也是很多人保存后"多了一列无名列"的原因。

7. 那些让我半夜挠头的 Pandas 坑

7.1 SettingWithCopyWarning:不是报错,但比报错更讨厌

SettingWithCopyWarning应该是 Pandas 最常见的警告了。它的本质是:你用筛选得到的一个 DataFrame 视图,试图修改其中数据,Pandas 不确定你是否在修改原表,所以发出警告。网上很多回答是"忽略它",我不建议这么干。正确做法是:

sub = df.loc[df["city"] == "上海"].copy() sub.loc[sub["amount"].isna(), "amount"] = 0

关键就是这个.copy()。只要你想"先筛选,再修改",就在筛选结果上加.copy(),保证你改的是一个新的独立对象。这能从根源上消除这警告,也能避免你发生"改了子表却连带污染原表"的诡异 bug。

7.2 inplace=True 的争议:新版 Pandas 的默认方向

Pandas 里df.dropna(inplace=True)这种写法一直有争议。我能给的建议是:新项目尽量不用inplace=True,而是写成df = df.dropna()。原因有两个。第一,inplace=True对某些方法(比如str.replace的子链式操作)不生效,容易让人误以为改了其实没改;第二,链式写法可读性更强,每一步都返回新对象,方便调试。而且新版 Pandas 已经明确表示未来会逐步弱化inplace参数,不如早点适应。

7.3 pd.datetime 没了,老代码记得更新

如果你在网上看到抄来的老代码,里面有pd.datetime,在新版 Pandas 里会直接报错AttributeError。这个类在 Pandas 2.0 之后被移除了,统一用pd.Timestamp或 Python 原生的datetime。类似地,df.append()在 Pandas 2.0 之后也移除了,合并多个 DataFrame 请用pd.concat()。

我自己的经验是,每次装好新环境跑一遍老脚本,先看一遍 DeprecationWarning,把过时代码顺手改掉,能省掉后面一大堆莫名其妙的问题。

最后分享一个我自己的小习惯

我习惯在清洗流程里给每一步加上一个print输出当前 shape,比如print("去重后:", df.shape)。别看这行字简单,它能帮你快速定位"哪一步把行数搞没了"。有一次我因为某个筛选条件写反了,数据从两万行直接变两百行,就是靠一步步打印才发现问题出在quantity的筛选上。Pandas 的学习曲线其实不难,难的是你在真实数据面前保持耐心,一步一步把流程理顺。希望这篇文章能让你少走几次我走过的弯路。

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

AI智能体开发平台实战:从模型选型到工具调用与生产落地

简介&#xff1a;《大模型应用-AI智能体开发平台》PPT课件聚焦大模型应用开发平台&#xff0c;面向希望掌握智能体设计与搭建的产品经理、开发者及高校师生。内容从平台定义与核心价值切入&#xff0c;明确可视化界面、预置模型、全流程工具集成如何降低开发门槛&#xff0c;并…

作者头像 李华
网站建设 2026/10/8 2:30:56

缺失值填充全攻略:从判断类型到模型插补的完整实践

简介&#xff1a;一份面向Python数据分析初学者的缺失值处理专题PDF&#xff0c;系统梳理了数据缺失的原因、类型及对应处理策略&#xff0c;适用于数据清洗、特征工程等数据预处理场景。资源为单个PDF文件&#xff0c;大小约450KB&#xff0c;内容紧凑、按方法分节组织&#x…

作者头像 李华
网站建设 2026/10/8 2:30:26

银河麒麟V10密码忘了怎么办?单用户模式重置密码实战指南

简介&#xff1a;银河麒麟桌面操作系统V10(sp1)用户若忘记登录密码而无法进入系统&#xff0c;这份PDF手册提供了基于单用户模式的完整恢复路径&#xff0c;并兼顾X86与arm两种架构。资源共1个文件、约327KB&#xff0c;内容精炼&#xff0c;围绕grub界面启动项编辑、进入单用户…

作者头像 李华
网站建设 2026/10/8 2:29:54

消费级显卡微调8B模型实战:用LoRA打造高质量营销文案生成器

简介&#xff1a;面向具备机器学习基础的技术人员和市场营销从业者&#xff0c;这份实战指南讲解如何借助大型AI模型生成高质量训练数据&#xff0c;再通过Unsloth微调8B小模型&#xff0c;以较低计算成本产出适用于Facebook、Twitter、邮件等渠道的营销内容。内容覆盖环境搭建…

作者头像 李华
网站建设 2026/10/8 2:29:54

NetApp FAS8300集群初始化与NFS/SMB/iSCSI接入实践

简介&#xff1a;围绕NetApp FAS8300存储设备的部署实施&#xff0c;这份PDF手册为存储工程师和系统管理员提供了从初始化到多协议访问的完整路径&#xff0c;既适合初次配置FAS系列的新手&#xff0c;也可作为标准化安装流程的参考。重点涵盖集群创建、节点加入、aggr与卷/LUN…

作者头像 李华
网站建设 2026/10/8 2:28:55

PDI Carte配置与启动优化实战:从原理到踩坑复盘

搞数据集成时间长的兄弟应该都躲不开PDI这套工具&#xff0c;以前叫Kettle&#xff0c;后来统称Pentaho Data Integration。单机用Spoon拖拖拽拽做转换、跑作业&#xff0c;一般不会出太大问题&#xff0c;可真到了生产环境&#xff0c;任务要定时调度、要远程分发、要扛并发&a…

作者头像 李华