ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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, encodingutf-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], errorscoerce)注意errorscoerce这个参数它会把实在转不了的值变成 NaN。这样既不会报错中断脚本又会让脏数据暴露在缺失值层面之后统一处理。如果不用coerce遇到一个未知这样的字符串整个转换过程就崩了。还有一个小坑有些表格里的数字是用全角字符写的比如。这在从网页导出的数据里非常常见。我一般会在替换阶段顺手做一次全角转半角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], errorscoerce)大部分常见格式它都能识别。真正难缠的是那种不同行格式不一样的脏数据比如同一年份列里既出现2024-01-05又出现20240105。这种情况建议先看看到底有几种格式df[order_date] df[order_date].astype(str) formats df[order_date].str.replace(r\d, , regexTrue).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], keepfirst)keep参数也很关键。默认保留第一条但如果后续数据比前导数据完整你要考虑keeplast。我见过有人在做订单去重时保留了一条金额为 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, indexyear_month, columnscity, valuesamount, aggfuncsum, fill_value0 )反过来有时候我们需要把宽表变成长表给可视化工具用melt()就派上用场了。我做过一个门店销售报表原始数据是每个门店一列、日期一行但画图工具要求日期一列、门店一列、销售额一列这时候long_df df.melt( id_vars[date], value_vars[北京店, 上海店, 广州店], var_namecity, value_nameamount )id_vars是要保留的标识列value_vars是我要拆成行的列。这几乎是所有宽转长需求的模板建议直接背下来。4. 可视化先想清楚回答什么问题再动手画图4.1 一张图回答一个问题可视化最大的误区是为了画图而画图。我见过有人随手df.plot()出来一张乱七八糟的折线图坐标轴重叠、图例分不清最后自己都解释不了。我现在的流程是先写下我要回答的问题再选图。你想回答的问题推荐图形Pandas 实现某个指标随时间怎么变化折线图df.plot(xmonth, ysales)不同类别的量级对比柱状图df.plot(kindbar, xcategory, yamount)两个指标之间的相关性散点图/热力图df.plot(kindscatter, xa, yb)各部分占比饼图/堆积柱状图df.plot(kindpie, yamount)用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[销售额], markero) ax.set_title(月度销售额趋势) ax.set_xlabel(月份) ax.set_ylabel(销售额) plt.xticks(rotation45) plt.tight_layout() plt.show()两个中文字体设置参数一定记得写上不然图表里全是小方块这是所有用 Pandas 画图的中文用户都会遇到的第一道坎。seaborn我通常用来画热力图比如看不同商品类目在周几的销量关系heat_data df.pivot_table(indexcategory, columnsweekday, valuesamount, aggfuncsum) sns.heatmap(heat_data, cmapYlOrBr, annotTrue, fmt.0f) plt.show()热力图的好处是能一眼发现周四的数码产品销量异常高这类肉眼难以察觉的规律。annotTrue会把具体值标在格子里汇报时非常直观。4.3 让图表能进会议室的三个细节我自己的经验画图想从自己看懂升级到汇报可用至少要做三件事加标题、加轴标签、调图例。很多 Pandas 默认图是没有标题的直接拿去群里发别人根本不知道横轴是什么。# 一个细节对图表尺寸和 dpi 做统一控制 fig, ax plt.subplots(figsize(8, 4), dpi120) # 一个细节数值轴的千分位格式化 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, encodingutf-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], errorscoerce) df[order_date] pd.to_datetime(df[order_date], errorscoerce) df[refund_time] pd.to_datetime(df[refund_time], errorscoerce) # 4. 去重 df df.drop_duplicates(subset[order_id], keepfirst) # 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[销售额], markero) plt.title(月度销售额趋势) plt.xticks(rotation45) plt.tight_layout() plt.show() # 各城市销售额对比 city_sales df.groupby(city)[amount].sum().sort_values(ascendingFalse) plt.figure(figsize(8, 4)) city_sales.plot(kindbar, colorskyblue) plt.title(各城市销售额) plt.xlabel(城市) plt.ylabel(金额) plt.show() # 退款率分析 refund_rate df.groupby(city)[refund_time].apply( lambda x: x.notna().mean() ).sort_values(ascendingFalse) 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, indexFalse, encodingutf-8-sig)utf-8-sig会写一个 BOM 头Excel 打开就不会乱码。如果你要给别人发数据我建议直接上这个编码。indexFalse则是不把行索引写进文件这也是很多人保存后多了一列无名列的原因。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 inplaceTrue 的争议新版 Pandas 的默认方向Pandas 里df.dropna(inplaceTrue)这种写法一直有争议。我能给的建议是新项目尽量不用inplaceTrue而是写成df df.dropna()。原因有两个。第一inplaceTrue对某些方法比如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 的学习曲线其实不难难的是你在真实数据面前保持耐心一步一步把流程理顺。希望这篇文章能让你少走几次我走过的弯路。
返回列表