
我上一篇文章里写过一句话Python生态里能把“数据量不大但你很急”这件事处理得最体面的库Pandas排第二没人敢排第一。尤其当你用Excel卡在十万行数据处理到怀疑人生时Pandas一小时能给你交付完整结果。第48章我们聚焦Pandas用于数据分析的核心操作从数据装载、探查清洗到分组聚合和宽表转换一次性讲透。这一章的内容针对的是已经会写基础Python语法、但没系统用过Pandas的读者。我会默认你装好了Pandas没装的话pip install pandas一行搞定然后用一个贴近实际业务的数据集样例带你走完数据分析项目中最常用的操作链路读数据、摸清结构、清洗、筛选、运算、分组、透视、合并。每段操作我都会解释为什么这样做以及在真实项目中容易踩哪些坑。1. 数据入口read_excel和read_csv的参数选择Pandas最常用的数据装载函数有两个pd.read_excel()和pd.read_csv()。别看它们只是一个函数参数用好和不用好效率差出一倍。read_excel常见参数import pandas as pd df pd.read_excel( 销售明细.xlsx, sheet_name2024年, header0, usecolsA:G, dtype{客户ID: str}, parse_dates[下单时间], skiprows0 )这几个参数值得解释一下sheet_name如果一个Excel文件有多个sheet默认只读第一个。传sheet的名称或索引0表示第一个都可以。header指定哪一行作为列名。默认0第一行如果你的表格前几行是标题、说明文字就要改成合适的值比如header2。usecols只读需要的列传列名列表或B:F这样的范围。数据源列特别多的时候先用usecols做裁剪能明显加快读取速度、减少内存占用。dtype强制指定列的数据类型。最常见的坑是客户ID、订单号这种“以0开头的数字”如果不指定dtypestr读进来会被当成整型前导的0直接被吞掉而且那列会显示成科学计数法。parse_dates把字符串形式的日期列解析成真正的日期类型后续才能做时间序列排序、按月聚合等操作。read_csv的用法和read_excel大致相同差异点在分隔符和编码上。sep默认是英文逗号如果你拿到的是tab分隔或者分号分隔的文件要显式指定sep\t中文CSV在Windows下经常遇到乱码指定encodingutf-8或encodinggbk就好任选一个直到不乱码。还有一个容易被忽略的参数是nrows。项目刚启动时数据源有几千万行你只想先看数据结构用df pd.read_csv(big.csv, nrows1000)读1000行预览比傻等全量读取快得多。分块读取大文件的思路如果你的操作目标是“统计全量数据”但文件大到内存装不下Pandas支持分块读取chunk_iter pd.read_csv(超大文件.csv, chunksize500000) result [] for chunk in chunk_iter: result.append(chunk.groupby(类别)[金额].sum()) final pd.concat(result).groupby(level0).sum()这个用法的逻辑是每次读50万行对这50万行做一次局部聚合最后把所有局部结果合并再聚合一次。每个chunk的内存占用是可控的能处理远大于内存的CSV文件。2. 看完数据再动手info、describe和dtypes的使用节奏装上数据的第一件事不是马上清洗而是摸清字段结构、质量概况。我见过很多新人上来就df.head()看一眼就往下写处理逻辑结果处理到一半发现“这列混着字符串和数字”“那列空值占了一半”前面的代码全部白写。数据探查这个环节不能跳过。三个必用的探查方法df.info() df.describe() df.head(10)df.info()告诉你总共有多少行、每列的名称、非空值数量、dtype。执行一次就能快速发现哪些列缺数据、哪些列类型不对比如日期列显示成了object数字列显示成了object。df.describe()对数值型列输出count非空计数、mean均值、std标准差、min最小值、25%/50%/75%分位数、max最大值。一眼看出离群值是否存在量纲差异是否大到需要标准化。df.head(10)直接看前10行数据内容注意有没有明显的脏数据乱码、全角符号、多余空格、错位字段。dtype可能是你最该关注的东西Pandas每一列一定有一个数据类型。常见的包括dtype含义典型值int64整型数量、年份float64浮点型金额、比率object字符串/混合类型姓名、备注datetime64[ns]日期时间下单时间bool布尔型是否已支付“object”是最需要警惕的类型。它表示Pandas没能把这列识别为纯数字或纯日期很可能是因为这列里混入了少量特殊字符比如金额列里带了¥或者千分位逗号。遇到这种情况就要做类型转换了。df[销售额] df[销售额].astype(str).str.replace(¥, ).str.replace(,, ).astype(float)这一行做的事情是先把整列转成字符串用str.replace把¥和逗号去掉最后转成float。注意astype(str)那一步是必要的不然Pandas对数值列直接调用str.replace会报错。内存优化的小技巧数据量大的时候可以考虑把不需要精确到那么大范围的整型列降级。比如年龄列用int8就够-128到127但默认可能是int64内存相差8倍。用pd.to_numeric(column, downcastinteger)或直接astype(int8)可以显著降低内存占用。对一列这样优化可能看不出区别但几十列、上亿行时内存占用能少几个GB项目能不能跑完全都靠这点细节。3. 数据清洗缺失值、重复值和异常值的处理逻辑真实数据源没有一个干净的清洗占整个项目工作量的六到七成。这里把Pandas里最常见的三个清洗场景讲透。3.1 缺失值先看再决定填还是删df.isna().sum()这句代码输出每一列的缺失值数量。在处理前你要按列判断缺失的性质通常有几类情况客户备注、商品描述这类文本列缺失不影响统计留空或不处理都行。金额、数量这类关键数值列缺失直接影响汇总结果必须处理。连续性变化的时间序列中间缺了一个点可能需要插值。决策参考# 删除缺失占比过高的列比如超过50% df df.dropna(threshdf.shape[0] * 0.5, axis1) # 数值列用均值或中位数填充 df[销售额].fillna(df[销售额].median(), inplaceTrue) # 分类型列用众数填充 df[客户等级].fillna(df[客户等级].mode()[0], inplaceTrue) # 时间序列插值 df[销量].interpolate(methodlinear, inplaceTrue)具体选哪种核心原则是“不要引入偏差”。销售额这种有离群值存在的列均值容易被极端值拉高用中位数更稳健分类列填众数出现次数最多的值是最不引入额外信息的做法。提示inplaceTrue这个参数一度很流行但现在官方建议直接写成df df.fillna(...)返回新对象。原因是inplace在部分链式操作下会失效或者触发SettingWithCopyWarning而且Pandas后续版本有移除它的趋势。新项目里能不用就不用。3.2 重复值区分“完全重复”和“关键列重复”df.duplicated().sum() # 查看完全重复行数 df.drop_duplicates(inplaceTrue) # 删除完全重复行 df.drop_duplicates(subset[订单号], keepfirst, inplaceTrue) # 按指定列去重keep参数有讲究keepfirst保留重复项里的第一行keeplast保留最后一行keepFalse把重复的全部删掉。实际业务中如果同一个人在同一天下了两单不算重复但如果订单号完全一样说明数据源有问题重复记录会双倍计算营业额必须清洗。3.3 类型转换和字符串清洗前面提过astype这里补充一个更稳的写法。astype在遇到无法解析的内容时会直接报错这在自动化脚本里很致命。替代方案是pd.to_numeric有一个errors参数df[金额列] pd.to_numeric(df[金额列], errorscoerce)errorscoerce的作用能转成数字的就转转不了的变成NaN。之后你可以单独检查这些NaN的来源再决定是删掉还是手工修正。用这个方法比astype对脏数据的容忍度高很多。字符串列的清洗也不可少。比如客户姓名里混了全角空格、电话列里有横杠或括号可以用下面这组操作df[姓名] df[姓名].str.strip() # 去掉首尾空格 df[电话] df[电话].str.replace(r\D, , regexTrue) # 只保留数字str.strip()是最基础的操作但它只去首尾不管中间连续空格有需要时可以用str.replace(r\s, , regexTrue)把中间的空格也去掉。4. 数据筛选和切片布尔索引与loc/iloc的组合用法筛选操作看起来简单但Pandas里“按条件筛选”“按位置取值”“按行名列名取值”是三种不同的逻辑混用就会踩坑。4.1 loc和iloc的区别df.loc[0] # 按索引标签取行0是行索引的名字 df.iloc[0] # 按位置取行第一行初学者最容易迷糊的是loc用的是行索引的名字iloc用的是行的位置编号。如果行索引恰好是0、1、2这种整数序列两者结果相同但一旦索引被设置成日期、字符串或者倒序数字两者就完全不同了。df df.set_index(订单号) df.loc[DD001] # 按订单号取行 df.iloc[0] # 还是取第一行不管订单号是什么loc和iloc同时支持切片和行列同时选取df.loc[1:10, [客户名, 金额]] # 索引1到10取两列 df.iloc[0:10, 2:5] # 前10行第3到5列4.2 布尔索引是筛选的核心布尔索引的本质是“用一个True/False的数组作为行筛子”。比如筛选出金额大于1000的记录df[df[金额] 1000]df[金额] 1000这个表达式生成一个布尔序列传到方括号里之后Pandas只保留对应位置为True的行。多个条件组合时用且、|或、~非注意每个条件都要加括号df[(df[金额] 1000) (df[地区] 华东)] df[(df[金额] 1000) | (df[地区] 华北)] df[~(df[状态] 已取消)]很多新手在这里写成and和or直接报ValueError。因为Python的and会尝试把整个Series对象转成布尔值而一个Series的布尔值是有歧义的。4.3 isin和str.contains做模糊匹配isin用于一列的值是否落在给定列表里target_cities [北京, 上海, 广州] df[df[城市].isin(target_cities)]str.contains做子串匹配比如筛选所有包含“退款”的备注df[df[备注].str.contains(退款, naFalse)]注意naFalse这个参数如果备注列里有缺失值不填这个参数的话str.contains遇到NaN会返回NaN而在布尔索引里NaN会被当成True导致有缺失值的行被错误地选出来。这一点真没多少人注意过实际项目的脏数据里非常常见。4.4 切片时最容易犯的错链式赋值“筛选”和“修改”连在一起时会触发Pandas最著名的警告——SettingWithCopyWarning。# 这种写法有隐患 sub df[df[金额] 1000] sub[新列] 1这段代码运行时会弹黄色警告意思是“你修改的可能是副本不是原数据”结果可能没生效。正确做法是显式用loc一次性操作df.loc[df[金额] 1000, 新列] 1这个写法的含义是在金额 1000的行上给“新列”赋值为1。Pandas在此处不产生副本直接修改原DataFrame没有歧义不会警告。养成“影响原数据一律用loc表达”的习惯能省去很多排查时间。5. 核心运算groupby聚合、apply映射和pivot_table透视筛选只是第一步分析的核心在于汇总计算。Pandas的地位有一半是靠groupby和pivot_table撑起来的。5.1 groupby的基本逻辑groupby翻成大白话就是“按某个字段把行分组再对每组做聚合计算”。它有三个阶段拆分split、应用apply、合并combine。df.groupby(地区)[销售额].sum()这段代码的逻辑按地区分组对每组列销售额求和。结果是一个Series地区是索引销售额是值。多字段分组、多列聚合的写法df.groupby([地区, 商品类别])[销售额].agg([sum, mean, count, nunique])agg里传一个列表可以同时计算多个统计量。sum和mean常见count统计非空数量nunique统计去重后的唯一值个数。统计每个地区有多少个不同客户时nunique就派上用场了。5.2 自定义聚合逻辑agg和apply的区别如果内置的sum、mean、max满足不了需求比如你要算“每个地区销售额的中位数”“每个地区销售额大于1000的订单数”就得用自定义函数。def over_thousand_count(series): return (series 1000).sum() df.groupby(地区)[销售额].agg([sum, over_thousand_count])这个写法里over_thousand_count接收的是组内销售额这一整列Series返回一个标量。apply就灵活得多甚至可以接收整个分组DataFrame返回任意结构df.groupby(地区, group_keysFalse).apply( lambda group: group[group[销售额] group[销售额].max()] )这段代码的作用是返回每个地区销售额最高的订单记录。它接收的参数是每个分组形成的DataFrame返回的是该DataFrame筛选后的部分Pandas再把这些部分拼回一个整体。注意groupby.apply在Pandas 2.x版本里对分组键的处理有行为调整较新版本里group_keys参数默认值可能会让结果索引中包含分组键如果你发现结果里多了地区这一层索引显式设置group_keysFalse能去掉它。5.3 pivot_tableexcel数据透视表的Pandas版Excel里透视表人人会用Pandas里对应的是pivot_table。pivot pd.pivot_table( df, values销售额, index地区, columns商品类别, aggfuncsum, fill_value0, marginsTrue )每个参数的解释values要计算数值的那一列。index透视表的行字段。columns透视表的列字段。aggfunc聚合方式可以是字符串sum、内置函数np.sum、列表[sum, mean]。fill_value空值填充因为有的行列交叉点没有数据时是NaN透视表里显示为0更符合业务阅读习惯。marginsTrue额外显示“总计”行列相当于是Excel透视表里的“汇总”。输出结果就是行是地区、列是商品类别的矩阵。比如华东行、数码类列的交叉点就是华东地区数码类商品的销售额总和。这种转置格式非常适合写进周报、月报里直接展示。5.4 交叉表crosstab频次统计利器如果只做“计数”型的透视pd.crosstab更轻量pd.crosstab(df[地区], df[是否支付])输出是每个地区下“已支付/未支付”的订单数。pivot_table要三个参数才做得转的活crosstab直接给两个字段就行而且它还支持normalizeindex把每行转成占比格式方便快速看各地区的支付率差异。6. 数据合并merge、concat和join的选择场景业务数据很少只存在一张表里订单表、客户表、商品表、库存表分开存放很常见分析的时候需要把它们拼起来。数据合并有三个函数选错会很麻烦。6.1 mergeSQL join的Pandas版本merge的工作逻辑和SQL的join完全一致。常见场景是现在有一张订单表和一个商品目录表要按商品ID关联把商品名称和品类信息补到订单明细里。merged pd.merge( df_orders, df_products, on商品ID, howleft )how参数是最容易影响行数的地方how参数含义结果行数特征inner只保留两边匹配上的少于或等于左边表的行数left保留左表全部右表匹配不上填NaN等于左表的行数right保留右表全部左表匹配不上填NaN等于右表的行数outer两张表的并集大于等于任一边的行数项目里最常用的就是howleft因为它的思路是“以订单表为主其他表只做信息补充订单里的行不能被丢掉”。一旦有人用了inner那些在商品表里不存在的商品订单会被静默删除最终统计的金额凭空少了一截这种错误在有几十万行数据时极其隐蔽。6.2 merge时的一个杀手级坑一对多导致行数膨胀如果商品表里同一个商品ID出现了两条记录比如不同颜色、不同规格左连接后订单表的每一行会被复制成两行汇总金额直接翻倍。这恐怕是数据分析项目里最阴险的错误来源之一。预防方法merge之前先检查右表关联字段是否有重复dup_count df_products[商品ID].duplicated().sum() print(f发现重复商品ID数量: {dup_count}) if dup_count 0: df_products df_products.drop_duplicates(subset[商品ID])这几行检查代码建议写成一个函数任何一次merge之前都跑一次把“先查重再合并”变成肌肉记忆。6.3 concat按行还是按列拼接concat的典型场景是两张结构相同的表比如1月和2月的销售明细想纵向堆成一张表。df_all pd.concat([df_jan, df_feb], axis0, ignore_indexTrue)axis0表示沿着行方向拼接就是“上下堆叠”。axis1表示沿着列方向拼接就是“左右并排”。ignore_indexTrue表示拼接后重新生成一套连续的索引而不是保留原表各自的索引。纵向拼接时要注意列名必须完全一致否则Pandas会把列名不一致的部分产生NaN还会保留两套列。这个如果没注意后面groupby时非常容易出错。6.4 join按索引合并的简单写法join是merge的一个简化版本专用于“按索引”合并不指定on参数df_orders.set_index(客户ID).join(df_customers.set_index(客户ID), howleft)这种写法省掉了on参数的显式指定目标很明确以客户ID索引对齐。但可读性不如merge因为它把索引对齐的逻辑藏在背后。我个人的建议是除非是快速验证否则常规合并都写merge后面维护脚本的人一眼能看懂对齐逻辑。7. 链式操作和代码组织用pipe把处理流程串起来Pandas收集的API非常多每个函数单独调用很清晰一旦操作多了代码就变成了一层层嵌套的“俄罗斯套娃”别人读起来很难分清处理的先后顺序。链式操作和pipe可以很好地解决可读性问题。假设清洗流程有这几步df_clean ( df .drop_duplicates(subset[订单号]) .assign(销售额lambda x: pd.to_numeric(x[销售额], errorscoerce)) .dropna(subset[销售额]) )每个方法返回一个新DataFrame括号把它们按顺序连起来阅读顺序就是执行顺序。第一次接触可能会觉得怪但读多了会觉得很顺。更复杂的场景用pipe。pipe的作用是把自定义函数接入链式调用def fill_missing_by_median(data, columns): for col in columns: data[col] data[col].fillna(data[col].median()) return data df_clean ( df .pipe(fill_missing_by_median, columns[单价, 数量]) .query(数量 0) .assign(订单金额lambda x: x[单价] * x[数量]) )pipe会把前面一步处理好的DataFrame作为第一个参数自动传入fill_missing_by_mediancolumns是你显式传的第二个参数。这样一来每个清洗步骤都可以抽成独立函数既方便测试又方便复用。配合query做筛选代码会变得非常接近英语阅读习惯df_clean df_clean.query(数量 0 and 订单金额 100)query支持用字符串写条件内部再解析成布尔索引。它和df[...]的区别主要就是写法简洁特别适合一个条件里包含多个变量时使用。因为逻辑清晰维护起来心理负担小很多。8. 时间序列处理resample和dt访问器的实战用法日期列经过前面的parse_dates处理后已经是datetime64类型这一步能做什么按月汇总、按季度对比、提取星期几做分析。8.1 先设置索引再重采样resample是Pandas时间序列最强大的功能但要求时间列必须是行索引。df[下单时间] pd.to_datetime(df[下单时间]) df_time df.set_index(下单时间) monthly df_time[销售额].resample(M).sum()resample(M)表示按月聚合sum表示对每个月求和。如果按周呢resample(W)。按季度resample(Q)。按小时呢resample(h)。常用的频率别名代码含义D自然日W自然周M自然月Q自然季度Y自然年h小时T / min分钟这里有个坑M是自然月比如2月1日到2月29日MS也是月初但语义不同。老版本Pandas中M代表的是月末日期MS代表月初日期如果你发现自己聚合的标签日期总是跑到月末那天你可能想要的是MS。后来Pandas 2.2版本调整了部分频率对齐方式但核心区分还在使用时注意验证聚合标签是否符合预期即可。8.2 同一个月里不同日期的数据自动归并重采样不只是求和还可以配合agg做更多指标monthly_stats df_time[销售额].resample(M).agg([sum, mean, count, max])这段代码一次输出每个月的总销售额、日均销售额、有销售的天数、单日最高销售额。用于月度经营分析非常直接。8.3 dt访问器从日期里提取成分有时不需要重采样只想给每行增加“月份”“星期几”“是否工作日”等特征列df[月份] df[下单时间].dt.month df[星期几] df[下单时间].dt.dayofweek # 0周一, 6周日 df[小时] df[下单时间].dt.hour df[是否周末] df[下单时间].dt.dayofweek.isin([5, 6])dt访问器专门用于处理datetime64类型的列后面可以接year、month、day、hour、dayofweek、quarter等属性。比如想看看一周里哪几天订单集中直接df.groupby(星期几)[订单号].count()就出来了不用手动去翻Excel。8.4 时间范围筛选时间序列筛选用布尔索引也可以但Pandas提供了一组很顺手的方法df[ (df[下单时间] 2024-01-01) (df[下单时间] 2024-04-01) ]字符串日期可以直接和datetime列做比较Pandas会自动转型因此代码非常简洁。还有一种更语义化的写法是df[下单时间].between(2024-01-01, 2024-03-31, inclusiveleft)效果等价且inclusive能控制边界包含哪一侧不容易写错边界值。9. 最后的输出to_excel和to_csv的格式与编码细节分析做完了结果总要交付。df.to_csv()和df.to_excel()的格式细节直接影响别人打开文件的使用体验。9.1 to_csv的三个关键参数df.to_csv(结果.csv, indexFalse, encodingutf-8-sig, sep,)indexFalse不把行索引写进文件。如果你不设置这个文件第一列会多出Unnamed: 0或者原来的索引值别人看文件时得手动删掉非常不专业。encodingutf-8-sig这个编码比utf-8多一个BOM头作用很直接Excel打开CSV时不会出现中文乱码。用utf-8生成的CSV文件用Excel双击打开经常乱码换成utf-8-sig就正常。sep,默认就是逗号如果你的数据里本身包含逗号可以考虑用sep\t生成tsv文件或者保持逗号让Pandas自动加引号包裹。9.2 to_excel的sheet和格式参数with pd.ExcelWriter(分析结果.xlsx, engineopenpyxl) as writer: df_sales.to_excel(writer, sheet_name销售汇总, indexFalse) df_customer.to_excel(writer, sheet_name客户分析, indexFalse)一个Excel文件里写多个sheet用ExcelWriter是标准做法。engineopenpyxl用于处理.xlsx格式如果是老版.xls格式需要enginexlwt——不过现在xlwt已经停止维护建议全部统一用.xlsx格式。9.3 分列显示宽度优化对于要发给业务团队的Excel直接导出的列宽可能不太美观更恶心的是日期列显示成类似44832这样的数字看不到具体日期。可以用XlsxWriter或openpyxl在生成时做格式化with pd.ExcelWriter(分析结果.xlsx, engineopenpyxl) as writer: df_sales.to_excel(writer, sheet_name销售汇总, indexFalse) worksheet writer.sheets[销售汇总] worksheet.column_dimensions[A].width 20这个操作能控制列宽但坦白说若数据量不大用Pandas导出CSV再用Excel打开另存为xlsx然后手工调格式也不费劲。真正要追求自动化时再考虑openpyxl格式化。10. 完整案例从原始明细到月度汇总分析报告把前面所有的操作串起来用一个实际案例演示完整的处理流程。假设订单明细表订单明细_2024.xlsx里有以下列订单号客户ID商品类别销售额下单时间地区支付状态目标是输出一张“各地区各品类月度销售额汇总表”并保存为Excel。完整脚本如下import pandas as pd # 1. 读数据 df pd.read_excel( 订单明细_2024.xlsx, dtype{客户ID: str}, parse_dates[下单时间] ) # 2. 探查结构与缺失值 print(df.info()) print(df.isna().sum()) # 3. 清洗 df df.drop_duplicates(subset[订单号]).copy() df[销售额] pd.to_numeric(df[销售额], errorscoerce) df df.dropna(subset[销售额]) df df[df[支付状态] ! 已取消].copy() # 4. 特征提取 df[月份] df[下单时间].dt.month df[年月] df[下单时间].dt.to_period(M) # 5. 分组聚合 summary ( df.groupby([年月, 地区, 商品类别], as_indexFalse)[销售额] .agg([sum, count]) .reset_index() ) # 6. 透视表 pivot pd.pivot_table( df, values销售额, index[年月, 地区], columns商品类别, aggfuncsum, fill_value0, marginsTrue ) # 7. 输出 with pd.ExcelWriter(月度销售汇总.xlsx, engineopenpyxl) as writer: summary.to_excel(writer, sheet_name明细汇总, indexFalse) pivot.to_excel(writer, sheet_name透视总表)这个脚本里值得注意的细节df.drop_duplicates(subset[订单号]).copy()drop_duplicates返回的是视图还是副本存在不确定性显式加一个copy()是为了后续做任何修改时绝不触发SettingWithCopyWarning。df.groupby()[销售额].agg([sum, count])后加reset_index()是为了把分组键从索引里释放出来变成普通列。如果你后续还要把summary作为DataFrame做其他处理保留索引有时会带来麻烦释放成列更规范。透视表这步直接用清洗后的df而不是summary原因是透视表本身已经按需求做了按年月、地区、品类三个维度聚合不需要再对summary做一次转换。把这份脚本跑一遍你就可以拿着月度销售汇总.xlsx直接挂进周报模板里整个过程从原始数据到最终成果不到10秒。Pandas作为数据分析的核心工具真正掌握的标准不是会调用几个函数而是面对一张实际表格时能快速判断出“该清洗什么、该筛什么、该用什么维度聚合、最终要输出什么形状的成果”。上面这些操作如果都亲手敲一遍再把每一步的执行结果用print或者.head()看一遍你会发现Pandas的数据处理思维已经内化成一种直觉了。