资讯详情

Pandas数据处理实战:从Series、DataFrame到分组聚合与性能优化

📅 2026/10/14 22:56:13 | 华诺云谱 👁 阅读
Pandas数据处理实战:从Series、DataFrame到分组聚合与性能优化
做数据分析这行pandas大概是绕不开的第一座山。有人形容它是Python数据处理里的“瑞士军刀”有人干脆说它就是内存里的Excel这两种说法都有点道理但都不够准确。pandas真正厉害的地方不是某个函数多好用而是它把“表格数据处理”这一整套流程——读取、清洗、转换、聚合、合并、输出——全部统一到了一个库里让你能用同一套语法处理各种乱七八糟的真实数据。这篇内容适合刚入门Python数据分析、或者已经在用Excel但想转向自动化处理的同学。我会从核心数据结构讲起把读写、清洗、分组聚合、合并、时间序列、性能优化这些日常最高频的模块都过一遍最后再给一个完整的实战案例和一份避坑清单。很多细节是我在实际项目里踩过坑才总结出来的希望能帮你少走几个月的弯路。1. pandas到底解决了什么问题——核心设计思路拆解1.1 Series与DataFrame一张表和一列数的差别pandas最核心的概念就是两种数据结构。Series是一维带标签的数组你可以把它理解成Excel里的一列数据只不过它额外多了一个“索引”DataFrame是二维表格由多个Series组成看起来就是一张完整的Excel工作表。import pandas as pd # Series一列数据索引是行标签 s pd.Series([10, 20, 30], index[01-01, 01-02, 01-03]) # DataFrame二维表 df pd.DataFrame({ 日期: [01-01, 01-02, 01-03], 销售额: [1200, 980, 1560], 区域: [北区, 南区, 东区] })很多人学pandas几个月还是觉得别扭核心原因就是没理解“索引”这个概念。索引是行标签它不一定是0、1、2这样的数字也可以是日期、城市名、ID等等。pandas在做匹配、对齐、分组操作时背后大量依赖索引。你可以把索引理解成表格第一列的“行名”但它又不完全等价于普通数据列——它承担的是“定位”功能。实战里最常见的误区是有人把某一列普通数据用set_index设成索引后后用reset_index把它救回来来回折腾其实是没有想清楚这列到底该放在索引里还是当作普通字段。1.2 pandas与Excel、SQL相比优势在哪里很多人一开始接触数据分析会纠结一个问题Excel那么好用SQL又那么成熟为什么还要学pandas这个问题的答案等你处理过几次“脏数据”之后就会很清晰。工具擅长场景明显短板Excel交互操作、可视化、快速看数行数稍大就卡、手动操作难复用、逻辑不可追溯SQL大规模存储与查询、并发、分布式复杂清洗与科学计算能力弱写出来很长pandas清洗、转换、探索性分析、算法预处理单机内存有限不适合几十亿行超大数据我自己的经验是三者不是替代关系而是不同阶段用不同的工具。数据在数据库里做第一层过滤常见的做法是先写SQL把需要的大范围数据拉出来拉出来之后各种清洗和判断逻辑用pandas处理是最舒服的因为它的表达能力强几行代码就能完成很多Excel里需要录宏或者手拖几步才能实现的操作最终结果再导回Excel给业务同事看。这套流程一旦跑通你想换参数重跑的时候只需要重新执行一遍脚本比在Excel里手动调整省太多事。1.3 为什么pandas“慢”却依然是很多团队的默认选择网上经常有人吐槽pandas慢这个评价要分两层看。如果你拿它和Spark、ClickHouse这些分布式计算引擎比那确实不是一个量级但现实是大多数业务场景的数据量也就是百万行以内这个体量用pandas处理耗时通常只在几秒到几十秒之间完全在可接受范围内。更重要的是pandas的开发效率极高——你写一个for循环可能要十几行用pandas往往一行就搞定省下来的开发时间比那几秒的运行时间值钱得多。真正让pandas变慢的往往是使用方式的问题。比如有人习惯在几千行的DataFrame里一层层嵌套for循环那当然慢换成向量化操作后速度提升几十倍都不奇怪。关于性能优化的具体手段我会在第6部分详细展开。总之单机内存能装下的数据规模内pandas就是那个“用飞机去买菜”的例子你明明楼下就是超市没必要非得开战斗机出门。2. 数据读取与数据清洗的实操要点2.1 read_csv里那些容易踩坑的参数读取数据是每个pandas项目的起点但read_csv这个函数真不是填一个路径那么简单。我见过太多人在这一步就翻车尤其是从业务系统导出的CSV文件格式千奇百怪。df pd.read_csv( 订单数据.csv, encodingutf-8-sig, dtype{订单金额: float64, 数量: int32}, parse_dates[下单时间], usecols[订单ID, 下单时间, 区域, 订单金额, 数量], nrows1000, )这里每个参数都有它的用途。encoding指定编码中文CSV最常见的是“utf-8”和“gbk”如果你的文件读出来全是乱码大概率就是编码选错了用“utf-8-sig”而不是“utf-8”可以顺手处理掉表头前面的BOM标记很多从Windows环境导出的文件都有这个问题。dtype参数容易被人忽略它能在读取阶段就确定列类型避免pandas把本来应该是数值的列读成object类型后面还得再费劲转换。parse_dates可以直接把时间列解析成datetime类型省得之后用pd.to_datetime再转一步。usecols只读需要的列既能省内存又能减少后续干扰nrows1000先试读前1000行用来快速确认表头和数据格式是否正常尤其是面对陌生文件时这个习惯能帮你少踩很多坑。你可以在正式读全量之前先用nrows读一小部分打印出dtypes看一眼确认每个字段的类型符合预期再放开读全量。这比一次读完发现错了再返工要高效得多。2.2 缺失值处理不该一删了之拿到数据之后第一步是看缺失值很多新手一看到NaN就直接dropna()删掉这种做法很危险。删除永远是最简单的处理方式但不一定是对业务理解最合理的处理方式。# 统计缺失 df.isnull().sum() # 删除某些关键列缺失的行 df.dropna(subset[订单ID, 销售额], inplaceTrue) # 用各组中位数填充缺失销售额 df[销售额] df.groupby(区域)[销售额].transform( lambda x: x.fillna(x.median()) )这里有个重要的经验判断如果销售额这一列存在缺失直接删行会损失该条订单的其他信息用整列均值填充又容易被极端值带偏。更稳妥的方式是按区域分组用各组的中位数来填充——比如北区和南区的客单价差距很大用全局平均值就会让两组数据的特征都被“平均化”稀释。时间序列数据则更适合用前向填充也就是fillna(methodffill)因为连续日期的价格往往更接近前一天的数值而不是更接近全局平均值。还有一点要提醒dropna有一个subset参数只在指定列存在缺失时才丢弃行。这个参数很好用因为一张订单表可能有几十个字段有些字段缺失完全不影响你的分析目标不需要因为某个不重要的列有NaN就删掉整行。2.3 字符串脏值和类型统一的处理真实数据里最磨人的不是大计算量而是各种不规整的字符串。金额前面带着人民币符号、数字中间夹着千分位逗号、日期格式五花八门这些才是日常工作的常态。df[金额文本] df[金额文本].str.strip() df[金额文本] df[金额文本].str.replace(¥, ) df[金额文本] df[金额文本].str.replace(,, ) df[金额] pd.to_numeric(df[金额文本], errorscoerce)这段代码做的事情很朴素先把字符串两端的空格清掉再把人民币符号和千分位逗号替换掉最后统一转成数值。errorscoerce这个参数非常关键——它能在转换失败时把值变成NaN而不是直接抛异常中断程序。比如某条记录里金额写了个“待定”你用默认参数转换脚本当场崩溃用coerce之后它会变成NaN你后面再用缺失值处理逻辑统一兜底程序能继续往下跑最后在结果排查阶段再决定怎么处理这些脏数据。另外注意区分df[金额文本].str.replace和Python内置的str.replace方法前者是pandas对Series的向量化字符串操作后者是单个字符串对象的方法。很多新手在Series上直接调用原生方法结果报AttributeError其实就是这个区别。3. 筛选、分组、聚合——日常分析最常用的三把刀3.1 loc与iloc一个按标签一个按位置筛选数据是pandas使用频率最高的操作而loc和iloc的区分又是新手最容易懵的点。我用一句话总结loc按索引标签筛iloc按整数位置筛。方法依据典型示例说明loc行/列标签df.loc[df[区域]北区, [销售额]]布尔条件筛选必须用lociloc整数位置df.iloc[0:3, 0:2]切片含头不含尾# 按条件筛选行 按列名取列 north df.loc[df[区域] 北区, [订单ID, 销售额]] # 按位置取前三行前两列 top_left df.iloc[0:3, 0:2]为什么布尔条件筛选要用loc因为df[df[销售额] 1000]这种写法虽然也可以用但它在pandas内部走的是另一套“便捷索引”逻辑一旦遇到链式赋值就可能触发SettingWithCopyWarning。更重要的是loc能同时过滤行和筛选列一个表达式把逻辑写完读起来也清晰。还有一个细节df[[销售额]]取出来的是DataFrame而df[销售额]取出来的是Series这两个在后续处理中行为有差异很多bug就是因为没搞清楚二选一的区别。3.2 groupby agg多维度统计的核心模式如果说loc是pandas的“眼睛”那groupby就是pandas的“拳头”。按组统计这种需求在Excel里要用透视表在SQL里要写GROUP BY在pandas里就是一行groupby加一行agg。result df.groupby(区域).agg( 订单笔数(订单ID, nunique), 销售总额(销售额, sum), 客单价(销售额, mean) ).reset_index()这段代码用的是pandas新版本支持的“命名聚合”写法返回的列名就是你在等号左边起的名字非常直观。agg里可以给不同列分配不同的聚合方式订单笔数用nunique去重统计因为同一张订单可能有多条明细直接count会把重复订单算进去销售总额用sum客单价本质上是平均单笔销售额所以用mean。这里要特别注意nunique和count的区别。count是非空值的个数不关心重复nunique是去重后非空值的个数。统计订单ID时如果你想要“真实订单数”几乎肯定要用nunique而不是count。还有一个高频操作是groupby之后要不要reset_index。默认groupby会把分组字段变成索引如果你打算后续把这个结果和其他表merge或者写入数据库最好reset_index把它变回普通列如果只是看一眼结果保持索引形式反而更紧凑。3.3 apply与transform什么时候用哪个apply和transform是pandas里最容易混淆的两个函数但它们的设计目的完全不同。apply是“逐组计算返回任意形状的结果”transform是“逐组计算但保持原始DataFrame的行数不变”。简单说apply像是汇总transform像是广播。# 按区域标准化销售额 df[销售标准化] df.groupby(区域)[销售额].transform( lambda x: (x - x.mean()) / x.std() )这里的逻辑是对每个区域分别计算销售额的均值和标准差然后把这一组内每个原始值都减去组均值、除以组标准差最后回填到对应行。这个操作如果用apply做你会得到每组只有一行计算结果还得想方设法merge回原表用transform一步就搞定也不改变行数写起来非常清爽。这类操作的典型应用场景是特征工程在做机器学习模型之前要对不同分组的数据分别做标准化或者缺失值填充transform就是为此设计的。在2.2节里我们用它填充组内中位数本质上和标准化是同一个套路。所以我的建议是凡是需要“按组计算一个值然后填回每一行”的场景先想transform而不是apply。4. 数据合并与时间序列——进阶必过的两道坎4.1 merge与concat两张表怎么拼数据分析很少只处理一张表订单表、商品表、用户表、区域表……日常操作就是把这些表拼起来。merge和concat是两种完全不同的“拼法”merge是按公共字段匹配类似SQL的JOINconcat是物理上的拼接要么上下堆行要么左右堆列。merged pd.merge( order_df, product_df, on商品ID, howleft )这段代码按商品ID把订单表和商品表进行左连接保留订单表的全部行商品信息能匹配上的就填上匹配不上的就是NaN。how参数里最常用的是left和innerleft适合以一张表为主表补全信息inner适合只保留两边都能匹配上的记录。在实际业务中左连接用的频率最高因为你通常希望主表的行数不要因为连接而减少。使用merge之前最好先检查连接键是否有重复值。如果A表的某个键有多行B表对应键也有多行merge会产生笛卡尔积行数会爆炸。写代码前可以用df[商品ID].duplicated().sum()看看重复情况。再有就是列名对齐问题两张表的公共列如果叫法不一致需要在merge时分别指定left_on和right_on合并后又会出现两个颜色相近的列名用suffixes参数给它们加后缀避免冲突。concat的使用场景相对简单核心就是上下堆叠。你从三个不同月份的文件里分别读入数据结构完全一致只要pd.concat([df1, df2, df3])就能拼成一张长表远比一行一行append高效。它还有一个参数axisaxis0是上下堆行axis1是左右堆列默认是0注意别搞反。4.2 时间序列从字符串到resample时间序列处理是pandas的强项但前提是先把时间列转成真正的datetime类型而不是字符串。这一步不做后面的排序、切片、按月汇总全部都是错的而且你还很难发现错在哪里因为字符串排序和日期排序的差异很隐蔽。df[日期] pd.to_datetime(df[日期]) df df.set_index(日期) # 按月汇总销售额 monthly df.resample(ME).agg({销售额: sum}).reset_index()resample是pandas的时间聚合利器它先把时间索引划分为多个时间段然后再对每段做聚合。示例里的“ME”表示“Month End”也就是以每个月最后一天作为分组边界。这里要特别注意版本问题旧版本里常用“M”表示月末但在新版本里“M”已经弃用并报警告改成“ME”才是正道。如果搞不清自己项目用的什么版本可以在requirements.txt里锁住版本避免代码在别人环境下行为不一致。有了月度汇总算环比就顺理成章df[销售额环比] df[销售额] / df[销售额].shift(1) - 1shift(1)表示取前一行也就是上一个周期的值。用当前值除以上一周期值再减1就是环比增长率。这个思路简单但极其常用无论是日报、周报还是月报都离不开它。另一个常用的是diff()它直接算前后两行的差值适合看绝对变化量。4.3 滚动窗口计算rolling与expanding滚动平均是时间序列里经常用到的平滑手段比如某直播平台每天都有活跃用户数单看某天可能起伏很大看7天滚动平均才能看出来真实趋势。df[活跃用户_7日均] df[活跃用户].rolling(window7, min_periods3).mean()rolling(7)表示向前取7天窗口包括当天在内的最近7天min_periods3表示如果窗口内有效数据不足3天结果就是NaN。这个参数在你处理数据开头几天、窗口还没填满时特别有用避免了前6天全是NaN的尴尬局面。滚动窗口默认是“右对齐”的也就是当前日期的值是利用当前及之前的6天算出来的这适合做趋势判断如果你想做“未来7天均值”的某种探索那就得调center参数或者自己手动shift不要想当然。expanding则是“扩展窗口”从第一行开始一直累积到当前行常用于计算累计平均值、累计总和。比如你判断销售额是否达到全年目标的某个节点就可以用expanding().sum()算出截至每天的累计销售额。它和cumsum有点像但更灵活因为它能配合任意聚合函数。5. 实战案例——一个完整的报表处理流程5.1 场景描述某电商平台的月度经营报表纸上谈兵讲再多不如完整跑一遍流程。我拿一个虚构的某电商平台订单数据来说事。假设有一张订单明细表字段包括订单编号、下单日期、区域、商品类目、销售额、数量。需求是生成一份月度区域汇总报表包含每个区域每个月的订单量、销售总额、客单价同时输出一份按日销售额的趋势表供上游可视化使用。5.2 完整流程代码import pandas as pd # 1. 读取原始数据 df pd.read_csv( 订单明细.csv, encodingutf-8-sig, parse_dates[下单日期], dtype{销售额: float64, 数量: int32}, ) # 2. 清洗 df df.dropna(subset[订单编号, 销售额]) df df.drop_duplicates(subset[订单编号]) df df[df[销售额] 0] # 剔除负金额异常记录 # 3. 新增月份字段 df[月份] df[下单日期].dt.to_period(M).astype(str) # 4. 分组汇总区域 月份 summary df.groupby([区域, 月份]).agg( 订单量(订单编号, nunique), 销售总额(销售额, sum), 客单价(销售额, mean), ).reset_index() # 5. 透视表区域为行月份为列 pivot summary.pivot_table( index区域, columns月份, values销售总额, aggfuncsum, fill_value0, ) # 6. 按日趋势 daily df.groupby(下单日期).agg( 每日销售额(销售额, sum), 每日订单量(订单编号, nunique), ).reset_index() # 7. 输出Excel with pd.ExcelWriter(月度经营报表.xlsx) as writer: summary.to_excel(writer, sheet_name汇总表, indexFalse) pivot.to_excel(writer, sheet_name透视表) daily.to_excel(writer, sheet_name每日趋势, indexFalse)这段代码覆盖了前面讲的大部分核心操作读文件、清洗缺失值、去重、筛选异常、分组聚合、透视表、时间聚合、多sheet输出。步骤2里的顺序是刻意的先删缺失行再去重再筛异常值每一步都会改变行数所以每执行完一步都应该用df.shape确认行数变化是否符合预期。比如删除缺失值前是10万行删完后变成9万8多了200行NaNdrop_duplicates之后又变成9万5说明有300个重复订单编号。5.3 结果检查技巧别急着把报表发出去报表做完最重要的不是写代码而是核对数据。我自己吃过大亏之后养成了一个习惯所有汇总结果都必须从两个独立角度交叉验证一遍才敢交付。第一个检查是总数对齐。看汇总表的销售总额加总是不是等于daily表的每日销售额加总如果不等说明分组逻辑或者去重逻辑有问题。第二个检查是订单量。这里用nunique统计订单编号但如果你之前没做drop_duplicates统计结果就会虚高如果你拿这个数字跟业务同学核对对方一查系统发现对不上信任就直接打折扣。第三个检查是抽样验证。从汇总表里随机选一个区域和月份回原数据里手动筛选看几笔确保计算逻辑没有偏差。第四个检查是异常值。专门看一下销售额列是否存在极大值或负值如果有要判断是真实的大单还是录入错误。这个过程听着繁琐但真正做起来其实只要十几分钟却能把报表出错的概率大幅降低。尤其在月底着急交付的时候花这十几分钟绝对值得。6. 性能优化与内存管理——数据量一大就得注意这些6.1 用向量化思维替换for循环数据量到百万行上下的时候代码性能的差异就开始变得明显了。最常见的问题就是满屏for循环。pandas的底层是基于NumPy的向量化计算它对一整个Series做运算时循环发生在C语言层面速度飞快而如果你自己在Python层写for循环每一行都要经过Python解释器慢得不是一星半点。# 不推荐逐行循环计算 for i in range(len(df)): df.loc[i, 新列] df.loc[i, 销售额] * 1.1 # 推荐向量化一次搞定 df[新列] df[销售额] * 1.1两段代码结果一致但性能差距可能是几十倍。哪怕你只是给一个几万行的DataFrame加一列用for循环也会明显感觉卡一下用向量化则是瞬间完成。apply虽然比for循环快一些但它本质上还是逐行调用Python函数如果这段逻辑可以用向量化表达式替代就别用apply。判断标准很简单能直接对一列Series做的运算就不要想着“一行一行来”。6.2 dtype优化让DataFrame瘦身一半pandas的内存占用经常被忽略直到你处理大文件时遇到MemoryError才追悔莫及。其实很多时候在读取阶段用对dtype就能省下一半内存。df[区域] df[区域].astype(category) df[数量] df[数量].astype(int32) df[销售额] df[销售额].astype(float32)区域这种字符串列去重后的值很少非常适合转成category类型。category类型在底层只存储分类编码和分类映射表而不是重复存一堆字符串内存占用能大幅下降。数值列用int32、float32通常也够用为什么非得默认的int64、float64在大多数业务场景下销售额的精度用float32完全符合要求。内存降下来了数据读取和处理速度自然也会跟着提升因为数据搬运量变小了。还有一个容易被忽略的行为差异category类型的列在groupby时无序分类会按分类的自然顺序排序而不是你直觉里的字母序或数字序。如果你希望按特定顺序需要用pd.CategoricalDtype显式指定类别顺序否则结果可能和你设想的不一致。这个坑比较隐蔽遇到groupby结果排序不对劲的时候可以想一下是不是category类型在作祟。6.3 大文件读取策略不硬扛分块处理当单个文件大到快接近内存上限read_csv就不好使了。这时候不是让你加内存条而是换一种读取策略。chunk_list [] for chunk in pd.read_csv(超大文件.csv, chunksize100000): # 对当前块做聚合 chunk_result chunk.groupby(区域)[销售额].sum() chunk_list.append(chunk_result) # 合并每块结果 result pd.concat(chunk_list).groupby(level0).sum()read_csv的chunksize参数会把文件按每块10万行的方式分批读取每次只占一小部分内存。注意这里仍然是纯单机方案它能解决“单文件稍大、超过几个G内存”的情况但代价是你要自己处理分块和合并。如果文件已经大到几十个G这种级别说实话就不该硬用pandas了先考虑把数据导入数据库用SQL做初步聚合再把聚合后的结果拉出来用pandas做精细分析。方向选对比所有技巧都重要。7. 常见问题与排查技巧实录7.1 高频报错速查表我在教学和项目里汇总过一份pandas高频报错清单大多数问题集中在下面这几类。现象常见原因解决办法SettingWithCopyWarning对切片后的DataFrame做赋值的链式操作用.loc定位后赋值或提前.copy()KeyError: 列名列名不存在、拼写错误或含隐藏字符先df.columns查看真实列名用repr排查隐藏字符不同类型比较出错数值列被读成object字符串和数字比较用pd.to_numeric或astype统一类型合并后行数暴涨连接键存在重复值发生笛卡尔积先对连接键去重或改用不同的关联方式MemoryError数据量超过内存承载只读需要的列、分块读取必要时换数据库7.2 我在实际项目里的几条避坑心得真正上手做项目之后你会发现pandas报错本身并不可怕可怕的是那些“不报错但结果不对”的坑。我有几条经验想分享。第一每次改变行数的操作之后都打印一下shape。删掉缺失值之后行数少了多少去重之后又少了多少这个习惯能帮你把很多潜在问题扼杀在摇篮里不用等到最后出报表才发现总数对不上。第二不要写一长串的链式调用。比如df.groupby([A]).agg(...).sort_values(...).reset_index()这种看起来很高端但一旦报错你根本不知道是哪一环出了问题。拆成多行、用中间变量排错成本低得多。第三拿到任何外部文件先用文本编辑器或read_csv(nrows5)看一眼最原始的样子再写处理逻辑。有些CSV分隔符根本不是逗号而是分号有些表头自带空格或Unicode字符这些都得在拿到数据的第一时间确认。还有两个细节也很重要。一是浮点数判断判断金额是否为0时绝对不要用x 0因为浮点数运算可能会得到0.000000001这种结果要用abs(x) 1e-6或者np.isclose。二是pandas版本问题新版旧版API差异很大有些代码今天能跑换一个环境就崩所以在项目里锁定版本号是个好习惯。7.3 给新手的一个学习顺序建议学pandas最忌讳上来就想背完所有API。坦白说我用了这么多年也没记住全部函数。更高效的方法是用一个真实需求驱动学习比如试着统计你自己一个月的支付宝账单、分析你所在班级的期末考试成绩、或者复现一份店铺经营日报。做项目的过程中你自然会发现需要用什么函数遇到不会的再查文档、看报错记忆会非常深刻。跑通一个小项目之后再回头补原理比如索引对齐是怎么工作的、内存和性能如何取舍。这个“先实践、后原理”的顺序比拿着一本厚书从头啃到尾要有效得多。等你有过两三个完整项目经验绝大部分日常需求都不用查文档手比脑子快那才算真正上手了。我个人在实际操作里还有个习惯任何处理流程的第一步永远先复制一份原始数据作为备份代码里所有清洗操作都在副本上进行。这样即使中间逻辑写错了也能随时回到最初状态重来。最后再分享一个小技巧每做一步就用to_pickle或to_csv把中间结果存一版尤其是那种跑起来要几分钟的长流程这样一旦后面出错不用整个重跑加载中间结果接着调试就行。这些小习惯看着不起眼长期积累下来效率差距就是这么一点一点拉开的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑