我上一篇文章里写过一句话: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_name="2024年", header=0, usecols="A:G", dtype={"客户ID": str}, parse_dates=["下单时间"], skiprows=0 )这几个参数值得解释一下:
sheet_name:如果一个Excel文件有多个sheet,默认只读第一个。传sheet的名称或索引(0表示第一个)都可以。header:指定哪一行作为列名。默认0(第一行),如果你的表格前几行是标题、说明文字,就要改成合适的值,比如header=2。usecols:只读需要的列,传列名列表或"B:F"这样的范围。数据源列特别多的时候,先用usecols做裁剪,能明显加快读取速度、减少内存占用。dtype:强制指定列的数据类型。最常见的坑是客户ID、订单号这种“以0开头的数字”,如果不指定dtype=str,读进来会被当成整型,前导的0直接被吞掉,而且那列会显示成科学计数法。parse_dates:把字符串形式的日期列解析成真正的日期类型,后续才能做时间序列排序、按月聚合等操作。
read_csv的用法和read_excel大致相同,差异点在分隔符和编码上。sep默认是英文逗号,如果你拿到的是tab分隔或者分号分隔的文件,要显式指定sep='\t';中文CSV在Windows下经常遇到乱码,指定encoding='utf-8'或encoding='gbk'就好,任选一个直到不乱码。
还有一个容易被忽略的参数是nrows。项目刚启动时,数据源有几千万行,你只想先看数据结构,用df = pd.read_csv("big.csv", nrows=1000)读1000行预览,比傻等全量读取快得多。
分块读取大文件的思路
如果你的操作目标是“统计全量数据”,但文件大到内存装不下,Pandas支持分块读取:
chunk_iter = pd.read_csv("超大文件.csv", chunksize=500000) result = [] for chunk in chunk_iter: result.append(chunk.groupby("类别")["金额"].sum()) final = pd.concat(result).groupby(level=0).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, downcast="integer")或直接astype("int8")可以显著降低内存占用。对一列这样优化可能看不出区别,但几十列、上亿行时,内存占用能少几个GB,项目能不能跑完全都靠这点细节。
3. 数据清洗:缺失值、重复值和异常值的处理逻辑
真实数据源没有一个干净的,清洗占整个项目工作量的六到七成。这里把Pandas里最常见的三个清洗场景讲透。
3.1 缺失值:先看再决定填还是删
df.isna().sum()这句代码输出每一列的缺失值数量。在处理前你要按列判断缺失的性质,通常有几类情况:
- 客户备注、商品描述这类文本列缺失,不影响统计,留空或不处理都行。
- 金额、数量这类关键数值列缺失,直接影响汇总结果,必须处理。
- 连续性变化的时间序列中间缺了一个点,可能需要插值。
决策参考:
# 删除缺失占比过高的列(比如超过50%) df = df.dropna(thresh=df.shape[0] * 0.5, axis=1) # 数值列用均值或中位数填充 df["销售额"].fillna(df["销售额"].median(), inplace=True) # 分类型列用众数填充 df["客户等级"].fillna(df["客户等级"].mode()[0], inplace=True) # 时间序列插值 df["销量"].interpolate(method="linear", inplace=True)具体选哪种,核心原则是“不要引入偏差”。销售额这种有离群值存在的列,均值容易被极端值拉高,用中位数更稳健;分类列填众数(出现次数最多的值)是最不引入额外信息的做法。
提示:
inplace=True这个参数一度很流行,但现在官方建议直接写成df = df.fillna(...)返回新对象。原因是inplace在部分链式操作下会失效或者触发SettingWithCopyWarning,而且Pandas后续版本有移除它的趋势。新项目里,能不用就不用。
3.2 重复值:区分“完全重复”和“关键列重复”
df.duplicated().sum() # 查看完全重复行数 df.drop_duplicates(inplace=True) # 删除完全重复行 df.drop_duplicates(subset=["订单号"], keep="first", inplace=True) # 按指定列去重keep参数有讲究:keep="first"保留重复项里的第一行,keep="last"保留最后一行,keep=False把重复的全部删掉。实际业务中,如果同一个人在同一天下了两单,不算重复;但如果订单号完全一样,说明数据源有问题,重复记录会双倍计算营业额,必须清洗。
3.3 类型转换和字符串清洗
前面提过astype,这里补充一个更稳的写法。astype在遇到无法解析的内容时会直接报错,这在自动化脚本里很致命。替代方案是pd.to_numeric,有一个errors参数:
df["金额列"] = pd.to_numeric(df["金额列"], errors="coerce")errors="coerce"的作用:能转成数字的就转,转不了的变成NaN。之后你可以单独检查这些NaN的来源,再决定是删掉还是手工修正。用这个方法比astype对脏数据的容忍度高很多。
字符串列的清洗也不可少。比如客户姓名里混了全角空格、电话列里有横杠或括号,可以用下面这组操作:
df["姓名"] = df["姓名"].str.strip() # 去掉首尾空格 df["电话"] = df["电话"].str.replace(r"\D", "", regex=True) # 只保留数字str.strip()是最基础的操作,但它只去首尾,不管中间连续空格,有需要时可以用str.replace(r"\s+", "", regex=True)把中间的空格也去掉。
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("退款", na=False)]注意na=False这个参数:如果备注列里有缺失值,不填这个参数的话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_keys=False).apply( lambda group: group[group["销售额"] == group["销售额"].max()] )这段代码的作用是返回每个地区销售额最高的订单记录。它接收的参数是每个分组形成的DataFrame,返回的是该DataFrame筛选后的部分,Pandas再把这些部分拼回一个整体。
注意:
groupby.apply在Pandas 2.x版本里对分组键的处理有行为调整,较新版本里group_keys参数默认值可能会让结果索引中包含分组键,如果你发现结果里多了地区这一层索引,显式设置group_keys=False能去掉它。
5.3 pivot_table:excel数据透视表的Pandas版
Excel里透视表人人会用,Pandas里对应的是pivot_table。
pivot = pd.pivot_table( df, values="销售额", index="地区", columns="商品类别", aggfunc="sum", fill_value=0, margins=True )每个参数的解释:
values:要计算数值的那一列。index:透视表的行字段。columns:透视表的列字段。aggfunc:聚合方式,可以是字符串("sum")、内置函数(np.sum)、列表(["sum", "mean"])。fill_value:空值填充,因为有的行列交叉点没有数据时是NaN,透视表里显示为0更符合业务阅读习惯。margins=True:额外显示“总计”行列,相当于是Excel透视表里的“汇总”。
输出结果就是行是地区、列是商品类别的矩阵。比如华东行、数码类列的交叉点,就是华东地区数码类商品的销售额总和。这种转置格式非常适合写进周报、月报里直接展示。
5.4 交叉表crosstab:频次统计利器
如果只做“计数”型的透视,pd.crosstab更轻量:
pd.crosstab(df["地区"], df["是否支付"])输出是每个地区下“已支付/未支付”的订单数。pivot_table要三个参数才做得转的活,crosstab直接给两个字段就行,而且它还支持normalize="index"把每行转成占比格式,方便快速看各地区的支付率差异。
6. 数据合并:merge、concat和join的选择场景
业务数据很少只存在一张表里,订单表、客户表、商品表、库存表分开存放很常见,分析的时候需要把它们拼起来。数据合并有三个函数,选错会很麻烦。
6.1 merge:SQL join的Pandas版本
merge的工作逻辑和SQL的join完全一致。常见场景是:现在有一张订单表和一个商品目录表,要按商品ID关联,把商品名称和品类信息补到订单明细里。
merged = pd.merge( df_orders, df_products, on="商品ID", how="left" )how参数是最容易影响行数的地方:
| how参数 | 含义 | 结果行数特征 |
|---|---|---|
| inner | 只保留两边匹配上的 | 少于或等于左边表的行数 |
| left | 保留左表全部,右表匹配不上填NaN | 等于左表的行数 |
| right | 保留右表全部,左表匹配不上填NaN | 等于右表的行数 |
| outer | 两张表的并集 | 大于等于任一边的行数 |
项目里最常用的就是how="left",因为它的思路是“以订单表为主,其他表只做信息补充,订单里的行不能被丢掉”。一旦有人用了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], axis=0, ignore_index=True)axis=0表示沿着行方向拼接,就是“上下堆叠”。axis=1表示沿着列方向拼接,就是“左右并排”。ignore_index=True表示拼接后重新生成一套连续的索引,而不是保留原表各自的索引。
纵向拼接时要注意列名必须完全一致,否则Pandas会把列名不一致的部分产生NaN,还会保留两套列。这个如果没注意,后面groupby时非常容易出错。
6.4 join:按索引合并的简单写法
join是merge的一个简化版本,专用于“按索引”合并,不指定on参数:
df_orders.set_index("客户ID").join(df_customers.set_index("客户ID"), how="left")这种写法省掉了on参数的显式指定,目标很明确:以客户ID索引对齐。但可读性不如merge,因为它把索引对齐的逻辑藏在背后。我个人的建议是,除非是快速验证,否则常规合并都写merge,后面维护脚本的人一眼能看懂对齐逻辑。
7. 链式操作和代码组织:用pipe把处理流程串起来
Pandas收集的API非常多,每个函数单独调用很清晰,一旦操作多了,代码就变成了一层层嵌套的“俄罗斯套娃”,别人读起来很难分清处理的先后顺序。链式操作和pipe可以很好地解决可读性问题。
假设清洗流程有这几步:
df_clean = ( df .drop_duplicates(subset=["订单号"]) .assign(销售额=lambda x: pd.to_numeric(x["销售额"], errors="coerce")) .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_median,columns是你显式传的第二个参数。这样一来,每个清洗步骤都可以抽成独立函数,既方便测试,又方便复用。
配合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", inclusive="left"),效果等价,且inclusive能控制边界包含哪一侧,不容易写错边界值。
9. 最后的输出:to_excel和to_csv的格式与编码细节
分析做完了,结果总要交付。df.to_csv()和df.to_excel()的格式细节直接影响别人打开文件的使用体验。
9.1 to_csv的三个关键参数
df.to_csv("结果.csv", index=False, encoding="utf-8-sig", sep=",")index=False:不把行索引写进文件。如果你不设置这个,文件第一列会多出Unnamed: 0或者原来的索引值,别人看文件时得手动删掉,非常不专业。encoding="utf-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", engine="openpyxl") as writer: df_sales.to_excel(writer, sheet_name="销售汇总", index=False) df_customer.to_excel(writer, sheet_name="客户分析", index=False)一个Excel文件里写多个sheet,用ExcelWriter是标准做法。engine="openpyxl"用于处理.xlsx格式,如果是老版.xls格式,需要engine="xlwt"——不过现在xlwt已经停止维护,建议全部统一用.xlsx格式。
9.3 分列显示宽度优化
对于要发给业务团队的Excel,直接导出的列宽可能不太美观,更恶心的是日期列显示成类似44832这样的数字,看不到具体日期。可以用XlsxWriter或openpyxl在生成时做格式化:
with pd.ExcelWriter("分析结果.xlsx", engine="openpyxl") as writer: df_sales.to_excel(writer, sheet_name="销售汇总", index=False) 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["销售额"], errors="coerce") df = df.dropna(subset=["销售额"]) df = df[df["支付状态"] != "已取消"].copy() # 4. 特征提取 df["月份"] = df["下单时间"].dt.month df["年月"] = df["下单时间"].dt.to_period("M") # 5. 分组聚合 summary = ( df.groupby(["年月", "地区", "商品类别"], as_index=False)["销售额"] .agg(["sum", "count"]) .reset_index() ) # 6. 透视表 pivot = pd.pivot_table( df, values="销售额", index=["年月", "地区"], columns="商品类别", aggfunc="sum", fill_value=0, margins=True ) # 7. 输出 with pd.ExcelWriter("月度销售汇总.xlsx", engine="openpyxl") as writer: summary.to_excel(writer, sheet_name="明细汇总", index=False) 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的数据处理思维已经内化成一种直觉了。