本节摘要:报表室最常要的那张表——行是分组、列是分组、格子里是聚合值——不必"先聚合再变身"两步走,pivot_table 一步端出;纯数频次的场合,crosstab 更短。本节讲两个函数的核心参数与 margins 口径,并说明它们与 4.1、5.2 的血脉关系。承接 5.2 的长宽变身,为摆盘区收官。
上线前最后一次晨会,运营抛来一句:"给我一张行是渠道、列是月份、格子里是总额的表,外加合计行列。"按部就班的做法是 groupby 聚合、再 unstack 展开、再补合计——三步,三处可以出错。pivot_table 把这三步打包成一个调用:聚合口径、长宽形态、合计边栏,全是参数。它是 4.1 的 groupby 与 5.2 的 pivot 的合体产物,也是日常交付频率最高的出表工具。往前承接两节的手艺,往后,你在 8.5 的流水线里会看到它以成品身份出场。
**pd.pivot_table(df, index, columns, values, aggfunc='mean', fill_value=None, margins=False, margins_name='All', dropna=True)**:index 与 columns 各领一组维度,values 是进格子的数值列,aggfunc 是聚合口径(可给函数名、函数列表或字典);fill_value 填空格;margins=True 加合计行列,边栏名字用 margins_name 改(比如改成"合计")。**pd.crosstab(index, columns, normalize=False, margins=False)**:专攻频次交叉表,本质是计数版透视表;normalize 三种口径——'index' 按行归一成占比、'columns' 按列、True 全表占比。
import pandas as pd df = pd.DataFrame({ "渠道": ["直营", "直营", "分销", "分销", "直播"], "月份": ["5月", "6月", "5月", "6月", "6月"], "金额": [120.0, 150.0, 88.0, 95.0, 180.0], }) report = pd.pivot_table( df, index="渠道", columns="月份", values="金额", aggfunc="sum", fill_value=0, margins=True, margins_name="合计", ) print(report) # 月份 5月 6月 合计 # 渠道 # 分销 88.0 95.0 183.0 # 直播 0.0 180.0 180.0 # 直营 120.0 150.0 270.0 # 合计 208.0 425.0 633.0
场景:分析"渠道与退款状态"的交叉分布——先看频数,再看行占比,一步一句。
df2 = pd.DataFrame({ "渠道": ["直营", "直营", "直营", "分销", "分销", "直播", "直播", "直播"], "状态": ["完成", "退款", "完成", "完成", "退款", "退款", "完成", "完成"], }) # 频数交叉表 print(pd.crosstab(df2["渠道"], df2["状态"], margins=True)) # 状态 完成 退款 All # 渠道 # 分销 1 1 2 # 直播 2 1 3 # 直营 2 1 3 # All 5 3 8 # 行占比:各渠道内部的退款率 rate = pd.crosstab(df2["渠道"], df2["状态"], normalize="index").round(2) print(rate["退款"]) # 渠道 # 分销 0.50 # 直播 0.33 # 直营 0.33
占比口径的选择就是结论口径的选择:按行归一看"各渠道内部退款率",按列归一看"退款单的渠道构成"——同一张频数表,两种讲法,汇报前想清楚要回答哪一问。
数值对了,报表还要"能看"。三件小事常被忽略:列序——月份列默认按字典序排,"10月"会跑到"5月"前面,转成 3.4 的时间周期再排就对了;空格——fill_value 补零之前想清楚,零与"没有发生"在业务上是不是一回事;合计口径——margins 的合计是重新聚合的,与手工把各列相加在含 NaN 时会有出入,交付前抽一行核对。这些收尾不影响数字对错,却决定报表在汇报现场的命运。
**翻车一:默认聚合是均值。**pivot_table 的 aggfunc 默认 mean,忘改就得到"平均金额"的报表——要总额必须显式 aggfunc="sum"。这可能是全册出现率最高的一处"默认值事故"。**翻车二:margins 与 fill_value 的口径纠缠。**空格先被 fill_value 填成零、合计再把零算进去,边栏与明细加减能对上,但"有数据的格子数"这类统计就失真了——开 margins 的报表,建议边栏用与明细一致的聚合口径,必要时手工验一行合计。**翻车三:crosstab 只认Series 不认列名列表混搭。**传列名容易踩作用域问题,稳妥做法像示例那样显式取 df2["渠道"];要多级交叉就传多个 Series 组成的列表。**翻车四:类别维度丢空组。**分组键是 category 类型时,没出现的类别默认被丢掉——pivot_table 与 crosstab 里显式传 dropna=False 或 observed 参数,空组才肯露面。
它俩与 4.1 加 5.2 的组合完全等价:groupby(["渠道", "月份"]).sum().unstack() 得到同一张表——透视表胜在一句写完,groupby 组合胜在聚合口径更自由(比如同时要多个统计量再分别展开)。复杂的多层表头、条件格式,交给导出环节的样式工具处理;高频复用的报表,把参数封装成函数,口径写在函数名与注释里,比每次现场拼参数可靠。
摆盘区三道菜至此出齐:合、变、报,一桌席成型。下一章转向随取随用的调味架——map、正则与抽样,见 6.1。