ARTICLE DETAIL

资讯详情

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

Pandas核心操作实战:数据清洗、分组聚合与可视化全解析

Pandas核心操作实战:数据清洗、分组聚合与可视化全解析 做数据分析这几年Pandas 是我几乎每天都要打开的工具。说实话第一次接触它的时候我对着DataFrame和Series这两个概念懵了好一阵——明明 Excel 里就是一个表为什么 Python 里非要拆成两种结构后来踩的坑多了才明白这两个数据结构恰恰是整个数据分析流程的地基。这篇是数据分析系列的第三篇我会重点围绕 Pandas 的核心操作展开内容包括数据加载、结构理解、类型排查、分组聚合、可视化对接以及我在实际项目中遇到过的各种“反直觉”问题。适合刚学完 Python 基础、准备上手真实数据集做分析的同学也适合已经用过 Pandas 但经常在数据类型转换和数据清洗上报错的人。我尽量用“做一个完整销售数据集分析”的场景来串起所有知识点。这样你学到的不是一个一个孤立的函数而是从拿到原始数据到最后输出结论的一整套处理思路。1. 整体设计拿到一份数据后应该先做什么很多人学 Pandas 的时候喜欢照着教程一行一行敲代码。今天学df.groupby()明天学df.merge()每个函数都能跑通但真拿到一份乱七八糟的 Excel 或者 CSV 时还是不知道从哪下手。我刚开始也是这样直到一位老同事跟我说了一句话“拿到数据先别急着算先把它看清楚。”所谓“看清楚”其实就是一套固定的检查流程。我自己的习惯是这样读取进来只看前几行确认表头和数据范围。看整体形状几行几列心里有个数。看列名和数据类型判断哪些列是数字、哪些是文本、哪些是日期。检查缺失值看看哪些字段空得厉害。单独扫一遍文本列看有没有乱七八糟的脏值比如“不详”“-”“空格”之类的。这套流程走完你对这份数据的基本盘就心里有数了。后面的清洗、转换、聚合都是在这个基础上做文章。这次我准备用一个电商订单明细数据来做演示。字段包括订单号、客户ID、下单日期、商品分类、商品数量、单价、折扣率、销售额等。这份数据是我模拟构造的但它把我在真实项目中见过的很多问题都埋在里面了日期列被读成字符串、销售金额里有千分位逗号、商品分类里有前后空格、同一个客户ID存在大小写混用、还有少量缺失的订单号。这些情况我相信你自己处理数据的时候也遇到过。Pandas 在这条流程里扮演的角色通俗讲就是“表格处理器 计算器”。它在内存里维护一张或几张表你可以对它们做增删改查、按条件筛选、按某列排序也能把多张表像 Excel 里的 VLOOKUP 一样关联起来。区别是你用 Python 的语法写这些操作结果可复现、过程可记录数据量一大也不会卡死。2. 核心数据结构DataFrame 与 Series 的搭配使用2.1 DataFrame一张自带目录的表DataFrame是 Pandas 里最常用的结构。你可以把它想象成 Excel 的一张工作表行有行号列有列名每个单元格是一个具体数值。它的强大之处在于不但能存数据还能给每一列起名字用名字去访问数据。日常构造 DataFrame 的方式有很多。最常用的是从 CSV 读取但有时候需要手动创建一个小的表来做测试这时候可以用字典import pandas as pd data { 商品分类: [手机, 电脑, 手机, 平板], 销量: [120, 80, 150, 60], 单价: [1999, 4599, 2499, 3099] } df pd.DataFrame(data) print(df)输出结果会是一个规规矩矩的表格。每一列的数据类型Pandas 会自动推断。这里“商品分类”是字符串“销量”和“单价”是数值。2.2 Series一列数据就是独立的个体Series可以简单理解成 DataFrame 中的一列单独的序列。它比普通的 Pythonlist多了索引和名字。比如上面df[销量]返回的就是一个 Series。 Series 做运算非常灵活对某个 Series 乘 2相当于对每个元素都乘 2这一点比 Python 原生 list 方便得多。sales df[销量] print(sales * 2)这个特性在做字段计算时很关键。比如新增一列“销售额”本质就是把“销量”和“单价”两个 Series 做乘法df[销售额] df[销量] * df[单价]2.3 为什么必须把它们分清楚我见过不少初学者在写代码的时候把 Series 当 DataFrame 用结果报错AttributeError: Series object has no attribute columns。反过来也有人把 DataFrame 当中单独的列去计算结果得到一串奇怪的结果。记住一个简单的经验法则如果要对一列做运算用 Series如果要对多列或多行做操作用 DataFrame。遇到不确定的情况用type()看数据类型就可以了。还有一个容易忽略的点是索引。Series 和 DataFrame 的索引默认是 0 到 n-1 的整数但一旦做了筛选或去重索引可能会变得不连续比如 1、3、5、9。这时候如果你用数字位置去取值就会出问题。最快的解决办法是调用reset_index(dropTrue)把索引重新变成从 0 开始排。这个坑我在合并数据的时候踩过一次后面在第三章节细说。3. 数据加载与初步体检read_csv 的进阶用法3.1 基础读取与参数调优读取 CSV 是一切的起点。最简单的写法就是pd.read_csv(文件路径.csv)。但真实世界里的 CSV 往往不像教程里那么干净常见的问题包括编码不对、分隔符不是逗号、有表头但带空格、日期格式不标准等。我常用的参数组合是这样的df pd.read_csv( order_data.csv, encodingutf-8-sig, sep,, dtype{订单号: str}, parse_dates[下单日期], thousands, )逐个解释一下我为什么这样设置encodingutf-8-sig很多从 Excel 另存过来的 CSV 文件是带 BOM 头的 UTF-8不加这个参数第一列表头会多出\ufeff这样的隐藏字符后面按列名筛选就会莫名失败。如果你遇到中文乱码可以试试gbk或gb18030。dtype{订单号: str}订单号虽然是数字但本质上是个编号不该参与计算。如果让它默认被读成整数一旦遇到以 0 开头的单号前面的 0 就会丢失最后关联的时候对不上非常坑。parse_dates[下单日期]直接把这一列解析成日期类型省得后面再转换。thousands,销售量或者金额里如果有千分位逗号这个参数能让 Pandas 自动识别并转成数字。3.2 初看数据的几板斧数据读进来之后不要直接head()看两行就去做分析建议按顺序执行这几个方法# 1. 形状几行几列 print(df.shape) # 2. 列名与类型总览 print(df.dtypes) # 3. 前几行 后几行兼顾头和尾 display(df.head()) display(df.tail()) # 4. 数值列的统计摘要 print(df.describe())df.describe()是个容易被忽略但极其好用的方法。它会自动计算出所有数值列的平均值、标准差、最小值、四分位数和最大值。我自己拿到陌生数据集时一定会先执行这条命令。比如看到某个数值列的最小值为负数但业务上不应该是负数那就说明数据里混入了异常值看到最大值特别离谱也能提前预警。它比自己去肉眼扫列高效得多。3.3 缺失值和脏数据的初次识别检查缺失值我习惯用一个链式写法missing df.isnull().sum() print(missing[missing 0])这样能把所有存在缺失的列一次性列出来并显示缺失数量。注意缺失值并不一定意味着要删除具体要看业务含义。比如“折扣率”为空可能表示该订单没有折扣我们可以用 0 来填充“销售额”为空那就需要检查是不是前面计算列出问题了。脏数据更隐蔽常见的是字符串列里混入空格。例如“商品分类”里有些行是“手机”有些行是“手机 ”多一个空格分组统计时会被当成两个不同的分类。我的排查办法是print(df[商品分类].unique())unique()会列出这一列所有不重复的值扫一眼就能发现异常。如果有空格用str.strip()清洗df[商品分类] df[商品分类].str.strip()还有一种情况是大小写不统一比如客户ID里既有ABC001又有abc001。这时候统一转成大写df[客户ID] df[客户ID].str.upper()这些处理看起来琐碎但如果不做后期的分组汇总结果会差之毫厘、谬以千里。4. 数据类型转换与字符串处理实战4.1 先看懂 dtypes 输出的含义Pandas 里常见的数据类型有这么几种类型说明典型场景object字符串或混合类型客户ID、商品名称int64整数销量、件数float64浮点数单价、折扣率datetime64[ns]日期时间下单日期bool布尔值是否发货object类型是最容易藏雷的地方。它不代表就是干净的字符串也有可能是“看起来像数字但其实不是”的字段。最典型的例子是单价。你在 Excel 里看到的是1999.00但读进来之后类型是object原因可能是源数据里单价列混入了一些“不详”“N/A”之类的文本Pandas 一看整列不全是数字就把整个列降级成字符串类型了。4.2 用 to_numeric 和 to_datetime 做安全转换处理这种“半吊子数字列”我推荐用pd.to_numeric()它自带错误处理机制。假设单价列混入少量脏值转换的时候这样写df[单价] pd.to_numeric(df[单价], errorscoerce)errorscoerce的意思是能转换的值就转成数字转换不了的值变成NaN。这样既保留了大部分有效数据又让异常值显性化方便后面统一处理。千万别用errorsraise除非你想让脚本一遇到脏数据就中断报错。日期转换同理常用pd.to_datetime()df[下单日期] pd.to_datetime(df[下单日期], errorscoerce)这里有一个我在项目里真实遇到的坑日期字符串格式不统一。有的行是2024-01-05有的行是2024/01/05还有的行是20240105如果不用pd.to_datetime()自动解析很容易排查半天。只要格式是常见的几种Pandas 基本能自动认出来转成标准的datetime64[ns]类型。4.3 字符串列处理的常用姿势字符串列处理是数据分析里花时间最多的一块。除了前面说的去空格、统一大小写还有几个常用操作值得记下来筛选包含某个关键字的行phone_orders df[df[商品分类].str.contains(手机)]替换某些脏值df[商品分类] df[商品分类].replace({手机通讯: 手机, Mobile: 手机})提取字符串中的部分信息。比如订单号格式是ORD-20240101-001想提取日期部分df[订单日期] df[订单号].str.extract(r(\d{8}))注意str.contains()、str.extract()这些方法如果用于空值上会返回NaN而不会直接报错这一点 Pandas 处理得还是比较友好的。4.4 转换后必须做的验证很多人转换完数据类型就急着往下走结果后面不仅没报错但结果怎么算都不对。原因就是没验证。我习惯在转换后立刻做两件事第一打印df.dtypes确认列类型已经变过来第二查看转换后是否有新增的NaN因为errorscoerce会把脏值变成NaN如果不处理后面求和、求平均的结果都会异常。比如统计销售额如果单价列里有 3 个NaN求出来的均值就偏低了。这时候需要决定是删除还是填充。我的经验是先看看NaN占比如果占比很低直接dropna(subset[单价])删掉这几行就好如果占比高可能说明源数据本身有问题需要回去检查导出逻辑。另外一个容易被忽略的细节是pd.to_datetime()转换后时间列虽然显示正常但数据的时区和精度可能有差异。如果只是做日常分析不用太纠结但如果涉及到跨时区数据合并就要特别小心最好统一转成 UTC 再处理。5. 分组聚合与透视表从明细到结论的关键一步5.1 groupby一问一答式的统计数据分析的核心动作其实就是一个字比。不同分类的销量谁高不同月份的销售额走势如何不同客户的购买频次差异大不大。这些问题的本质都是“按某个维度分组再对另一个维度算指标”在 Pandas 里对应的就是groupby()。我见过很多人写 groupby 时只看结果不看过程其实它的运作模式很简单先按指定列把数据分成若干组然后对每一组分别执行聚合函数最后把结果拼成一张新表。举个例子我想知道每个商品分类的总销量代码是result df.groupby(商品分类)[销量].sum() print(result)这个写法拆开理解分组列是商品分类要看的是销量列聚合方法是求和。如果我还想知道平均单价和销售额总和可以这样写result df.groupby(商品分类).agg( 总销量(销量, sum), 平均单价(单价, mean), 销售额合计(销售额, sum) ).reset_index()这里面有几个细节值得注意。第一groupby之后如果不调用reset_index()结果里“商品分类”会变成索引而不是普通列。很多后续操作需要把分类当作普通列来用所以记得重置索引。当然如果你只是想 quick view保留索引没问题。第二.agg()里支持同时计算多个指标还能给每个结果自定义列名。比先sum再mean再合并要清晰得多。第三分组后如果有缺失值分组会默认忽略但我们计算之前最好确认一下是否要保留“未分组”的数据。5.2 聚合函数选择什么时候用 sum什么时候用 count初学者最容易混淆的是sum和count。假设你要统计“每个分类的订单数”如果用df.groupby(商品分类)[订单号].sum()Pandas 会把订单号当成数字累加结果完全不是你想要的。正确做法是order_count df.groupby(商品分类)[订单号].count()这里的count()统计的是非空值的个数也就是订单记录条数。而sum()是数值求和。如果订单号被读成字符串sum()甚至会把订单号拼接成一段很长的文本排错的时候特别迷惑。还有nunique()也经常用到。比如想统计每个分类下有多少个不同客户购买用customer_count df.groupby(商品分类)[客户ID].nunique()它会统计唯一客户的数量而不是所有订单数量。在分析复购问题的时候这个方法比count()更有价值因为一个客户可能下了多笔订单。5.3 用 pivot_table 快速交叉对比如果分组维度有两个比如“商品分类”和“是否促销”你想做一个交叉表看看不同分类在促销和非促销情况下的销售额差异用pivot_table()比多层 groupby 更直观。pivot pd.pivot_table( df, values销售额, index商品分类, columns是否促销, aggfuncsum, fill_value0 )这段代码的意思是行是商品分类列是是否促销单元格里是销售额总和空值填充为 0。输出结果就像 Excel 的数据透视表一样规整一眼就能看出促销对每个类别的拉动效果。pivot_table还可以同时传多个 values 列和多个 aggfunc。比如想同时算出销售额、销量、订单数三样东西写成pivot pd.pivot_table( df, values[销售额, 销量, 订单号], index商品分类, columns是否促销, aggfunc{销售额: sum, 销量: sum, 订单号: count}, fill_value0 )注意aggfunc这里要写成一个字典键是列名值是对应的函数。不同列可以用不同的聚合方式这一点比 groupby 里的.agg()更灵活一点。5.4 排序和 TopN 筛选分组聚合出来之后为了看最有价值的部分通常需要排序。比如想看销售额排名前五的商品分类top5 result.sort_values(销售额合计, ascendingFalse).head(5)sort_values默认升序加ascendingFalse变成降序。这里有一个小坑如果数据里有NaN排序时它们会被排到最后但如果你用的是ascendingTrueNaN会被排到最前面容易看漏。建议排序前先dropna()或者明确填充分类信息。还有一种需求是从每个分组里取 TopN。比如每个分类下销量最高的三款商品。这个用简单 groupby 做不了需要用到groupby().apply()或者sort_values后做分组切片。我比较推荐后者因为apply在大数据量上性能略差。df_sorted df.sort_values([商品分类, 销量], ascending[True, False]) top3_each df_sorted.groupby(商品分类).head(3)思路是先整体排序确保每个分类内销量降序排列再按分类分组取前 3 行。这个写法简洁且不容易错。6. 与 Matplotlib 联动让分析结果可视化6.1 直接从 DataFrame 绘图Pandas 和 Matplotlib 是天然搭档。用df.plot()就能生成基础图表省去手动设置坐标轴的麻烦。最常见的操作是分组聚合得到结果后直接画柱子图。import matplotlib.pyplot as plt # 让图表在 Jupyter 里直接显示 %matplotlib inline result df.groupby(商品分类)[销售额].sum().sort_values(ascendingTrue) result.plot(kindbarh, figsize(8, 5)) plt.title(各商品分类销售额对比) plt.xlabel(销售额) plt.tight_layout() plt.show()这里面我特意用了横向柱状图barh而不是纵向柱状图。原因是分类名称如果比较长横向显示更友好不会被截断。很多人一开始图省事用默认的柱状图结果分类名挤成一团还得回头调xticks旋转角度其实不如直接横向来得快。6.2 时间序列折线图的处理如果要对“下单日期”做按月汇总趋势需要先把日期列设为索引或者直接用resample()方法。比较稳妥的写法是先把日期规范化再分组求和。df[月份] df[下单日期].dt.to_period(M) monthly df.groupby(月份)[销售额].sum() monthly.plot(kindline, markero, figsize(10, 5)) plt.title(月度销售额趋势) plt.ylabel(销售额) plt.show()dt.to_period(M)会把每个日期归到对应的月份。比如2024-03-15就会变成2024-03。这样按月分组非常方便。如果你的数据量很大这个操作也不会太慢。有一点要提醒如果分组后的索引是 Period 类型画出来的横轴坐标可能显示为2024-03样式的刻度但 Matplotlib 有时候会把它显示成数字。遇到这种情况可以先用astype(str)把索引转成字符串再画monthly.index monthly.index.astype(str)6.3 多图布局做分析报告时经常需要把多个图拼在一起比如左边看销售额趋势右边看分类占比。直接用 Matplotlib 的子图功能fig, axes plt.subplots(1, 2, figsize(14, 5)) # 左图月度销售额 monthly.plot(axaxes[0], markero) axes[0].set_title(月度销售额) axes[0].set_ylabel(销售额) # 右图销售额占比环形图 category_sum df.groupby(商品分类)[销售额].sum() category_sum.plot(axaxes[1], kindpie, autopct%.1f%%) axes[1].set_ylabel() axes[1].set_title(商品分类销售额占比) plt.tight_layout() plt.show()autopct%.1f%%用来控制饼图上百分比显示的格式保留一位小数。这里的axaxes[0]指定把图绘制到左边画布axaxes[1]绘制到右边画布。这个传参方式值得记住因为当你需要在循环里批量生成图表时它就是核心机制。6.4 一张图里的多个序列还有一种常见需求是同时展示销售额和订单量两个指标。由于两个指标的单位不同直接画在一张图里会把坐标轴搞乱。解决方法是使用双 y 轴fig, ax1 plt.subplots(figsize(10, 5)) ax1.bar(monthly.index.astype(str), monthly[销售额], alpha0.6, label销售额) ax1.set_ylabel(销售额) ax1.set_xlabel(月份) ax2 ax1.twinx() order_monthly df.groupby(月份)[订单号].count() ax2.plot(order_monthly.index.astype(str), order_monthly, colorred, markero, label订单量) ax2.set_ylabel(订单量) ax1.legend(locupper left) ax2.legend(locupper right) plt.show()这里的twinx()会创建一个共享 x 轴的新坐标轴。左侧是销售额柱状图右侧是订单量折线图信息密度很高适合放进分析报告。但我也要泼一点冷水可视化不是越炫越好。Pandas 配合 Matplotlib 生成的图做到“清晰、准确、有标题、有坐标轴标签”已经比大多数人做得好。重点先放在数据本身对不对颜色、字体、图例到底多精美可以后面再优化。7. 数据导出与编码陷阱7.1 to_csv 与 to_excel 的常用参数分析做完结果要交出去。最常见的导出方式就是to_csv()或to_excel()。result.to_csv(分析结果.csv, indexFalse, encodingutf-8-sig)注意两点第一indexFalse很重要。如果不加Pandas 默认会把行索引也写进 CSV文件里多出一列。第二encodingutf-8-sig如果不加用 Excel 打开中文 CSV 时可能会乱码。这算是我踩过最多次的坑之一每次用 Excel 打开同事发给我的 CSV 发现乱码十有八九就是导出时用的纯 UTF-8 编码。如果导出到 Excel可以用result.to_excel(分析结果.xlsx, indexFalse, sheet_name汇总)前提是电脑上装了openpyxl库。这个库通常是pip install pandas的时候不会自动带的需要单独装。7.2 不要用中文做列名导出再读回另外一个我自己给自己挖过的坑分析过程中为了图方便把中间结果列名改成了中文然后导出 CSV过两三天又要重新读取这份数据继续分析。此时如果脚本里没有显式指定encoding读取出来就可能会乱码而且列名里有中文在代码里写df[销售额]虽然没问题但别人接手这段代码时容易手误打错字。我的经验是中间结果导出用英文列名真正交付给业务方展示时再单独做一份中文列名的版本。既保证代码健壮性也兼顾可读性。8. 常见问题与排查技巧实录做数据分析最耗时间的往往不是建模和分析而是排错。下面整理的是我在实际项目中反复遇到的问题给出一线排查思路。8.1 明明有这个列却还报 KeyError这个问题多半是列名和你以为的不一样。排查步骤print(df.columns.tolist()) # 直接看列名列表常见原因有三种第一列表头带了 BOM 隐藏字符比如\ufeff订单号。列名有不可见的空格比如下单日期 。读取 CSV 时用了headerNone导致整张表的列名是数字 0、1、2。解决方案很简单要么用df.columns [订单号, 客户ID, ...]手动重命名要么在读取时用header0指定第一行是表头。8.2 数据明明没缺失sum 结果却是 NaN这种情况最常出现在多列相加的场景。假设销售额不是直接从源表读的而是手算出来的df[销售额] df[销量] * df[单价]如果“销量”或“单价”里混入了字符串Pandas 转换时会报错但如果你已经用pd.to_numeric(..., errorscoerce)转换过了那么只要有任意一列存在NaN运算结果就是NaN。解决方法是要么先fillna(0)要么在计算前dropna()。8.3 groupby 之后结果莫名少了数据如果分组列本身有缺失值groupby()默认会把这些缺失值所在的行丢弃。也就是说分组结果里不会出现NaN这个组。如果你希望把它单独归为一组可以df[商品分类] df[商品分类].fillna(未知分类)给缺失值一个明确的替补标签这样它就不会被默认丢掉了。8.4 索引错位导致合并后数据错乱用pd.concat()或pd.merge()合并数据时如果 DataFrame 的索引不连续concat(axis1)可能会对错位置。比如一个 DataFrame 索引是 0、1、2另一个是 0、1、3直接横着拼就会错行。排查方法第一件事就是看两个表的索引是否完全对得上print(df1.index, df2.index)最稳妥的办法是提前用reset_index(dropTrue)把两个表的索引都重置成连续的 0 到 n-1。8.5 常用排查速查表现象可能原因优先排查方法读取 CSV 中文乱码编码不对换成gbk或utf-8-sig列名 KeyErrorBOM、空格print(df.columns.tolist())数值列变 object混入文本脏值pd.to_numeric(errorscoerce)日期筛选无效日期列是字符串pd.to_datetime()转换分组后数据变少分组列存在 NaNfillna(未知)合并后行数翻倍一对多关联先看duplicated()确认键唯一性Excel 打开 CSV 乱码导出编码问题encodingutf-8-sig聚合结果有 NaN参与计算的列有空值dropna()或fillna(0)关于 Pandas 的个人经验与心得教了这么多方法和代码最后我想分享一点实操层面的心得。你可能已经把上面的代码都跑通了但你真正开始做自己的数据项目时还是会遇到新问题。第一不要迷信一个函数能解决所有问题。Pandas 的函数设计得再方便最终还是要靠你对数据的理解来做判断。比如drop_duplicates()删除重复值很简单但它默认保留第一个出现的行如果你觉得应该保留最后一条记录就得加keeplast。这种细节不用死记但用之前要想清楚业务逻辑。第二分析记录最好以代码的形式留存。Excel 的优点是操作简单缺点是你改了哪个单元格、删了哪一行根本没法回溯。而用 Pandas 做分析每一步操作都在脚本里哪天结果对不上原始需求翻代码就能定位问题。这也是我后来不管多小的分析都坚持写脚本的原因。哪怕只是一个临时任务我也会建一个.py或.ipynb文件把读取、清洗、聚合、导出的过程完整留下来。第三多练数据集。网上能找到很多公开的数据集比如各类比赛平台上的电商、金融、城市数据下载下来之后先别急着用机器学习模型就重复我今天讲的这套流程读进来、看结构、清洗、分组聚合、画图、导出结论。等你把这套流程练成肌肉记忆后面学任何新技术都会快很多。Pandas 看起来函数多、语法细但真正核心的东西其实就那几个数据结构、数据读取、数据清洗、类型转换、分组聚合、可视化、导入导出。把这条主线打通之后你再看复杂的数据分析项目会发现它们都是由这些基础操作组合出来的。最后再提醒一句遇到报错别怕先打印df.dtypes和df.head()百分之八十的问题都能从这两步里面找到线索。
返回列表