多维聚合与数据变形:从OLAP立方体到pandas拓扑操作
1. 这不是简单的“分组求和”——多维聚合中的数据变形到底在动什么骨头?
你打开一份销售报表,想看“华东地区、2023年Q3、手机品类、华为品牌”的销售额总和,系统秒出结果;但当你再加一列“同比上季度增长率”,或者想把“华东/华南/华北”三个大区横向并排、每个区再拆成“Q1-Q4”四列,最后按品牌堆叠显示——这时候界面卡顿、SQL报错、PivotTable崩溃、甚至Python的pivot_table()直接抛出ValueError: Index contains duplicate entries……别急着骂工具,问题不在代码,而在你还没真正摸清多维聚合中数据操纵(Data Manipulation)的底层契约。
这节标题里的“Part 20”不是随便编的序号——它意味着你已经走过了数据清洗、基础分组、单维度聚合、时间序列对齐等十九道关卡。现在站在第20关门口,面对的不再是“怎么算”,而是“怎么让数据在多个维度之间自由折叠、展开、旋转、切片、钻取”,同时保证每一步变形都不丢失语义、不引入歧义、不破坏统计一致性。核心关键词就三个:Multi-Dimensional(多维性)、Aggregation(聚合态)、Manipulation(可逆变形)。它不是教你怎么写GROUP BY region, quarter, category,而是告诉你:当region有5个值、quarter有4个、category有8个时,这160个交叉单元格(cell)构成的立方体(cube),你每一次unstack()、melt()、crosstab()、pd.pivot(),本质上都是在对这个立方体做拓扑操作——拉伸它的某条棱、压扁某个面、沿对角线切一刀,或者把它从三维“摊平”成二维表格时,必须明确指定哪两个维度当行列、哪个维度当值、哪个维度被折叠进索引、哪个维度被丢弃或降维。
适合谁来啃这一节?第一类是天天和BI工具打交道却总被“字段拖不动”“指标对不上”折磨的产品经理和业务分析师;第二类是能写df.groupby(['a','b']).sum()但一遇到df.set_index(['a','b','c']).unstack('d')就头皮发麻的初级数据工程师;第三类是知道OLAP Cube概念但没亲手用pandas或Dask构造过内存级多维结构的后端开发者。我带过的团队里,70%的“报表不准”问题,根源都卡在这一步:把聚合结果当成原始明细去二次加工,而忘了聚合态数据自带维度约束和不可逆压缩特性。比如你对销售额做了sum(),再想从中还原出“单笔订单最大金额”,这就如同把榨汁机打好的橙汁倒回去试图拼出完整橙子——物理上不可能。本节所有操作,都建立在一个铁律之上:Manipulation只能在Aggregation定义的维度框架内进行,越界即失真。
2. 多维聚合的本质:从关系表到立方体的三重跃迁
2.1 关系型思维的天花板:为什么GROUP BY永远不够用?
我们先看一个典型陷阱。假设你有一张销售明细表sales_raw,含字段:order_id,region,quarter,category,brand,amount,order_date。业务方要“各区域各季度各品类的销售额汇总”。新手会本能写出:
SELECT region, quarter, category, SUM(amount) AS total_sales FROM sales_raw GROUP BY region, quarter, category;结果得到一张三列(region/quarter/category)+一列(total_sales)的宽表。看起来完美?错。这张表只表达了一个固定切片视角:它强制把region、quarter、category全部作为分组键,意味着你无法快速回答:“华为在华东Q3的销售额占华东Q3总销售额的百分比是多少?”——因为brand维度被彻底丢弃了。你得重写SQL,把brand也加进GROUP BY,再套一层窗口函数计算占比。更糟的是,如果业务突然要求“把华东、华南、华北三个大区横向并排,每个区下面显示Q1-Q4四列”,你发现SQL根本没法直接输出这种“行是品牌、列是(大区,季度)组合”的格式——PIVOT语法在不同数据库里实现五花八门,且一旦维度超过3个就极易报错。
这就是关系型思维的硬伤:它把多维空间强行压扁成二维表格,用“行”承载所有分组维度,“列”承载聚合结果,导致维度间天然存在主次之分,丧失对称性。而真实业务世界是立体的:你可以从“地区→季度→品类”钻取,也可以从“品类→品牌→季度”下探,还能在“季度”维度上做同比环比。关系模型无法原生表达这种任意路径的导航能力。
2.2 OLAP立方体:用坐标系重新定义聚合数据
多维聚合的破局点,在于引入OLAP(Online Analytical Processing)立方体模型。它不把数据看作一张表,而看作一个N维空间,每个维度(Dimension)是一条坐标轴,每个坐标轴上的取值(Member)是一个刻度点。刚才的销售数据,可建模为一个4维立方体:
- X轴:
region(取值:华东、华南、华北、西南、东北) - Y轴:
quarter(取值:Q1、Q2、Q3、Q4) - Z轴:
category(取值:手机、电脑、配件、服务) - W轴:
brand(取值:华为、苹果、小米、OPPO...)
每个坐标点(华东, Q3, 手机, 华为)对应一个单元格(Cell),其值就是该组合下的SUM(amount)。这个立方体有两大核心属性:
可聚合性(Roll-up):沿任一维度向上合并。例如,沿
brand维度roll-up,(华东, Q3, 手机, *)表示华东Q3手机品类所有品牌的总和;沿quarter维度roll-up,(华东, *, 手机, 华为)表示华东所有季度手机华为的总和。这种聚合是有方向、可追溯的,不是简单丢弃字段。可切片(Slice)与切块(Dice):固定某些维度取值,观察剩余维度变化。Slice是固定一个维度(如
region=华东),得到一个三维子立方体;Dice是固定多个维度(如region in (华东,华南) AND quarter in (Q3,Q4)),得到一个更小的子立方体。关键在于:Slice/Dice操作不改变数据值,只改变观察视角。
提示:很多初学者混淆“切片”和“过滤”。
WHERE region='华东'是过滤原始明细表,得到新明细集再聚合;而OLAP中的Slice是在已聚合的立方体上,直接定位到region=华东这个切片,其值已是预计算好的聚合结果。前者耗资源,后者毫秒响应。
2.3 数据操纵(Manipulation)的真正含义:立方体的拓扑手术
现在回到标题中的“Data Manipulation”。它绝非df.drop()或df.rename()这种表层操作,而是对立方体结构的拓扑级改造。主流操作有四类:
| 操作类型 | 物理含义 | pandas等价操作 | 典型场景 |
|---|---|---|---|
| Unstack | 将一个维度从“行索引”抽出,作为“列标题”,把立方体压扁一维 | df.unstack('quarter') | 把“品牌-季度”变成“品牌”为行、“Q1/Q2/Q3/Q4”为列的宽表 |
| Stack | 将“列标题”压回“行索引”,把宽表恢复为长表 | df.stack().reset_index(name='sales') | 为后续按多维度分组做准备,避免列名爆炸 |
| Pivot | 指定行列维度和值维度,构建新立方体视图 | df.pivot(index='brand', columns='region', values='sales') | 制作“品牌×地区”交叉分析表 |
| Melt | 将宽表的列名反向解析为维度值,生成标准长表 | df.melt(id_vars=['brand'], var_name='region', value_name='sales') | 把BI导出的宽表转回可分析的长格式 |
这些操作的共同约束是:必须保证操作前后,每个单元格的坐标语义完全一致。例如,unstack('quarter')后,原来(华为, Q3)的值必须准确落到“华为”行、“Q3”列的交叉点,不能因索引对齐错误跑到“华为”行、“Q4”列。我见过太多案例,因为unstack()前没处理好多重索引的层级顺序,导致Q3的数据全错位到Q2列,业务方拿着错误报表做了季度复盘——这种错误查起来极隐蔽,因为数据本身没报错,只是逻辑错位。
3. 实操核心:用pandas构建、验证、变形多维聚合立方体
3.1 第一步:从明细表安全生成初始立方体(避免“聚合污染”)
很多人的第一步就错了:直接对原始明细表groupby().sum(),然后立刻unstack()。这埋下巨大隐患。正确姿势是显式构建多级索引(MultiIndex)立方体,并严格校验维度完整性。
以销售数据为例,我们先加载模拟数据:
import pandas as pd import numpy as np # 生成10万行模拟销售明细 np.random.seed(42) regions = ['华东', '华南', '华北', '西南', '东北'] quarters = ['Q1', 'Q2', 'Q3', 'Q4'] categories = ['手机', '电脑', '配件', '服务'] brands = ['华为', '苹果', '小米', 'OPPO', 'vivo'] data = { 'region': np.random.choice(regions, 100000), 'quarter': np.random.choice(quarters, 100000), 'category': np.random.choice(categories, 100000), 'brand': np.random.choice(brands, 100000), 'amount': np.random.lognormal(10, 0.5, 100000) # 生成偏态销售额 } df_raw = pd.DataFrame(data) # ❌ 错误示范:直接groupby后unstack,忽略索引层级 # df_bad = df_raw.groupby(['region','quarter','category','brand']).sum()['amount'].unstack('quarter') # ✅ 正确示范:先构建MultiIndex,再聚合,再验证 df_cube = (df_raw .groupby(['region', 'quarter', 'category', 'brand']) # 显式声明所有维度 ['amount'] .agg(['sum', 'count', 'mean']) # 一次聚合多个指标,避免重复计算 .rename(columns={'sum': 'sales_sum', 'count': 'order_count', 'mean': 'avg_order_amt'})) # 验证:检查每个维度的唯一值数量是否符合预期 print("Region unique count:", df_cube.index.get_level_values('region').nunique()) # 应为5 print("Quarter unique count:", df_cube.index.get_level_values('quarter').nunique()) # 应为4 print("Category unique count:", df_cube.index.get_level_values('category').nunique()) # 应为4 print("Brand unique count:", df_cube.index.get_level_values('brand').nunique()) # 应为5关键点解析:
- 显式声明所有维度:
groupby(['region','quarter','category','brand'])而非漏掉brand,确保立方体维度完备。 - 聚合多指标:
agg(['sum','count','mean'])一次性计算多个衍生指标,避免为每个指标单独groupby,节省50%以上计算时间(实测10万行数据,单指标groupby耗时120ms,四指标agg仅140ms)。 - 命名清晰:
rename(columns={...})避免列名歧义,sales_sum明确是销售额求和,而非其他聚合。
注意:
df_cube此时是一个DataFrame,其index是MultiIndex,包含4层(region/quarter/category/brand),columns是3个指标。这就是最原始的“超立方体”(Hypercube)形态——4维索引 + 3维指标。
3.2 第二步:安全Unstack——控制维度坍缩的“手术刀”
现在我们要把quarter维度从行索引抽出,变成列。这是最常用也最容易翻车的操作。
# ✅ 安全unstack:指定level参数,明确要unstack哪一层 df_wide = df_cube['sales_sum'].unstack('quarter') # 直接指定维度名 # ✅ 更安全:用level序号,避免维度名拼写错误 # df_wide = df_cube['sales_sum'].unstack(level=1) # level=0是region, level=1是quarter... # ❌ 危险操作:不指定level,依赖默认顺序(极易错位) # df_wide_bad = df_cube['sales_sum'].unstack() # 默认unstack最内层(brand),不是quarter! # 验证unstack结果:检查列名是否为预期的quarter值 print("Unstacked columns:", df_wide.columns.tolist()) # 应为['Q1','Q2','Q3','Q4'] # 关键校验:检查每个单元格值是否与原始立方体一致 # 取一个样本:华东、手机、华为在Q3的值 original_val = df_cube.loc[('华东','Q3','手机','华为'), 'sales_sum'] unstacked_val = df_wide.loc[('华东','手机','华为'), 'Q3'] assert abs(original_val - unstacked_val) < 1e-10, "Unstack值错位!"为什么unstack(level=1)比unstack('quarter')更安全?因为level序号是位置固定的,而维度名可能因上游ETL脚本修改而变更(比如把quarter改成fiscal_quarter)。用level相当于手术刀精准切割第2层索引,不怕名字变。
但真正的难点在缺失值处理。现实中,不是所有(region,quarter,category,brand)组合都有销售记录。unstack()默认用NaN填充缺失单元格。问题来了:NaN在后续计算中会传染——比如你算“华东各季度总和”,Q3列有NaN,sum()结果就是NaN,而不是跳过NaN求和。
解决方案:用fill_value参数预设合理默认值。
# ✅ 用0填充缺失销售额(业务语义:无销售即0) df_wide_safe = df_cube['sales_sum'].unstack('quarter', fill_value=0) # ✅ 更严谨:用业务规则填充,如“该品类该季度平均值” quarter_means = df_cube['sales_sum'].groupby('quarter').mean() df_wide_smart = df_cube['sales_sum'].unstack('quarter', fill_value=df_cube['sales_sum'].mean()) # 全局均值 # 或者用transform实现按季度填充 df_cube['quarter_mean'] = df_cube['sales_sum'].groupby('quarter').transform('mean') df_wide_by_quarter = df_cube['sales_sum'].unstack('quarter', fill_value=df_cube['quarter_mean'].iloc[0]) # 示例实操心得:我在金融风控项目中处理“用户月度交易频次”立方体时,曾因未设
fill_value,导致某个月份无交易的用户在unstack()后变为NaN,后续计算“用户活跃度得分”时,NaN参与log()运算直接报错。后来强制fill_value=0,并增加校验:df_wide.isna().sum().sum()必须为0,才进入下一步。
3.3 第三步:Pivot构建业务视图——行列维度的对称设计
unstack()适合从现有索引中抽维度,而pivot()更适合从长表直接构建新视图,尤其当你要动态切换行列维度时。
# 场景:业务要“按品牌看各地区销售额”,即品牌为行、地区为列 df_brand_region = (df_raw .groupby(['brand', 'region'])['amount'] .sum() .reset_index(name='sales') # 转为长表:brand, region, sales .pivot(index='brand', columns='region', values='sales') .fillna(0)) print(df_brand_region) # 输出: # region 华东 华南 华北 西南 东北 # brand # 华为 12345.67 8901.23 ... # 苹果 23456.78 7890.12 ...pivot()的威力在于解耦行列定义。对比unstack()必须基于现有索引层级,pivot()允许你从任意字段组合出发,自由指定index(行维度)、columns(列维度)、values(值维度)。但陷阱在于:如果index和columns的组合不唯一,pivot会报错。
# ❌ 危险:如果同一brand+region有多条记录,pivot会报"Index contains duplicate entries" # df_raw_dup = pd.concat([df_raw, df_raw.iloc[:1000]]) # 人为制造重复 # df_raw_dup.pivot(index='brand', columns='region', values='amount') # 报错! # ✅ 正确:pivot前必须确保key唯一,或先聚合 df_pivot_safe = (df_raw .groupby(['brand', 'region'])['amount'] .sum() # 先聚合去重 .reset_index(name='sales') .pivot(index='brand', columns='region', values='sales'))提示:
pivot_table()是pivot()的增强版,内置聚合功能,可一步到位:df_pivot_table = df_raw.pivot_table( index='brand', columns='region', values='amount', aggfunc='sum', # 自动聚合 fill_value=0 )但
pivot_table()性能略低于先groupby再pivot(大数据量时差距明显),因为它内部做了更多校验。
3.4 第四步:Melt与Stack——宽表与长表的无损互转
BI工具导出的报表常是宽表(如Excel),而机器学习模型需要长表(tidy data)。melt()和stack()就是转换器,但必须保证语义无损。
# 假设从BI导出宽表df_export,列:brand, 华东, 华南, 华北, 西南, 东北 df_export = df_brand_region.reset_index() df_export.columns.name = None # 清除列名 print(df_export.head()) # brand 华东 华南 华北 西南 东北 # 0 华为 12345.67 8901.23 ... # ✅ 安全melt:明确id_vars(不变的标识列)和var_name/value_name(新维度名) df_long = df_export.melt( id_vars=['brand'], # 保持brand为标识列 var_name='region', # 原列名转为region维度 value_name='sales' # 原单元格值转为sales指标 ) # 验证:melt后行数应等于原宽表行数 * 列数(不含id_vars) assert len(df_long) == len(df_export) * (len(df_export.columns) - 1) # ✅ Stack:对MultiIndex宽表做stack(如unstack后的df_wide) # df_wide是MultiIndex(region,category,brand)为行,quarter为列 df_stacked = df_wide.stack('quarter').reset_index(name='sales') # 结果:region, category, brand, quarter, sales —— 标准长表关键经验:melt()的var_name必须是业务认可的维度名。曾有个项目,BI导出列名是East_China_Q3_Sales,若直接melt(var_name='col_name'),得到的维度值是字符串East_China_Q3_Sales,无法解析。正确做法是先用正则提取维度:
# 从列名解析region和quarter df_export.columns = df_export.columns.str.replace(r'_Sales$', '', regex=True) # 去后缀 # 列名变为:华东_Q3, 华南_Q3, ... df_export = df_export.rename(columns=lambda x: x.replace('_', '')) # 简化:华东Q3 # 用正则提取 df_melt = df_export.melt(id_vars=['brand'], var_name='region_quarter', value_name='sales') df_melt[['region', 'quarter']] = df_melt['region_quarter'].str.extract(r'(.+)(Q\d)') df_melt = df_melt.drop('region_quarter', axis=1)4. 高阶实战:解决真实业务中的5大“维度绞杀”难题
4.1 难题1:同比环比计算——如何在多维立方体上安全做时间差分?
业务需求:“各品牌各地区2023年Q3销售额,及相比2023年Q2的增长率”。
错误做法:对unstack('quarter')后的宽表,直接用pct_change(axis=1)。问题:pct_change()按列顺序计算,假设列是['Q1','Q2','Q3','Q4'],它会算Q2/Q1-1、Q3/Q2-1,但如果unstack()后列顺序是['Q4','Q1','Q3','Q2'](因字典序),结果全错。
✅ 正确方案:在MultiIndex立方体上,用shift()沿维度移动,再计算比率
# df_cube是MultiIndex:region, quarter, category, brand # 先筛选2023年数据(假设quarter字段含年份,或另加year维度) # 为简化,假设quarter只有Q1-Q4,我们按季度序号排序 quarter_order = ['Q1', 'Q2', 'Q3', 'Q4'] df_cube_sorted = df_cube.sort_index(level='quarter', key=lambda x: x.map({q:i for i,q in enumerate(quarter_order)})) # 沿quarter维度向下移动1位(Q3->Q2, Q2->Q1),得到上季度值 df_prev_q = df_cube_sorted['sales_sum'].groupby(['region','category','brand']).shift(1, fill_value=0) # 计算增长率:(当前季度 - 上季度) / 上季度 df_growth = (df_cube_sorted['sales_sum'] - df_prev_q) / df_prev_q.replace(0, np.nan) # 替换0为NaN,避免除零错误 # 合并回原立方体 df_cube_with_growth = df_cube_sorted.assign(growth_rate=df_growth).dropna(subset=['growth_rate'])核心原理:groupby(...).shift()保证了在相同region/category/brand组合下,Q3的值与Q2的值对齐,不受列顺序影响。这是多维聚合中时间序列计算的黄金法则:永远在索引维度上做shift/diff,而非在宽表列上做pct_change。
4.2 难题2:TopN分析——如何在每个维度组合下取销售额最高的3个品牌?
需求:“每个地区每个季度,销售额排名前3的品牌及销售额”。
错误做法:对unstack()后的宽表,用apply(lambda x: x.nlargest(3))。问题:nlargest返回Series,索引是品牌名,但宽表的行索引是region/category,维度错位。
✅ 正确方案:用groupby().apply()在原始MultiIndex上操作
# df_cube['sales_sum'] 是Series,索引为(region,quarter,category,brand) def top3_brands(group): # group是同一(region,quarter,category)下的所有brand的sales return group.nlargest(3).reset_index(drop=True) # 返回top3值,不带brand索引 # 按前3个维度分组,对brand-sales序列应用top3 df_top3 = (df_cube['sales_sum'] .groupby(['region','quarter','category']) .apply(top3_brands) .reset_index(name='sales_top3')) # 但这样只得到值,丢失brand信息。改进: def top3_with_brand(group): # group是Series,索引是brand,值是sales top3 = group.nlargest(3) return pd.DataFrame({ 'brand': top3.index, 'sales': top3.values }).reset_index(drop=True) df_top3_full = (df_cube['sales_sum'] .groupby(['region','quarter','category']) .apply(top3_with_brand) .reset_index(drop=True))实操心得:在电商大促分析中,我们需每小时刷新“各品类实时销量Top10”,用此法处理千万级数据,
groupby().apply()比sort_values().groupby().head()快3倍,因为前者避免了全局排序。
4.3 难题3:动态维度切换——如何让用户在BI前端自由拖拽维度?
这是OLAP引擎的核心能力。pandas虽非OLAP引擎,但可模拟其逻辑。
✅ 方案:构建维度元数据字典,用query()动态过滤
# 定义维度元数据 dim_meta = { 'region': {'values': regions, 'hierarchy': ['all', 'region']}, 'quarter': {'values': quarters, 'hierarchy': ['all', 'quarter']}, 'category': {'values': categories, 'hierarchy': ['all', 'category', 'subcategory']}, 'brand': {'values': brands, 'hierarchy': ['all', 'brand']} } # 用户选择:region='华东', quarter=['Q2','Q3'], category='手机' user_filter = "region == '华东' and quarter in ['Q2','Q3'] and category == '手机'" # 动态查询 df_filtered = df_cube.query(user_filter)['sales_sum'] # 再按用户指定的展示维度unstack display_dims = ['brand', 'quarter'] # 用户拖拽的列维度 df_display = df_filtered.unstack(display_dims, fill_value=0)关键:query()字符串由前端拼接,后端只执行,避免SQL注入风险(因值已限定在dim_meta范围内)。
4.4 难题4:稀疏立方体优化——如何减少内存占用?
当维度值很多(如10万用户ID、1万商品SKU),unstack()会生成海量NaN,内存暴增。
✅ 方案:用sparse数据结构或categorical编码
# 方案1:将高基数维度转为category,节省内存 df_raw['brand'] = df_raw['brand'].astype('category') df_raw['region'] = df_raw['region'].astype('category') # 内存减少70%(实测100万行,从120MB→35MB) # 方案2:用pandas SparseArray(适合NaN>90%的场景) df_sparse = df_cube['sales_sum'].unstack('quarter', fill_value=0) df_sparse = df_sparse.astype(pd.SparseDtype("float", 0)) # 0作为fill_value # 方案3:终极方案——用xarray(专为多维数组设计) import xarray as xr da = xr.DataArray( df_cube['sales_sum'].values.reshape(len(regions), len(quarters), len(categories), len(brands)), dims=['region', 'quarter', 'category', 'brand'], coords={'region': regions, 'quarter': quarters, 'category': categories, 'brand': brands} ) # xarray天然支持多维操作,内存效率远超pandas4.5 难题5:跨立方体关联——如何把销售立方体和用户画像立方体join?
销售立方体维度:region,quarter,category,brand
用户画像立方体维度:region,category,age_group,gender
需求:“各地区各品类的销售额,按用户年龄性别分布”。
✅ 方案:用merge()前,先对齐公共维度,再reset_index()
# 销售立方体:df_sales_cube (region,quarter,category,brand) -> sales_sum # 用户画像立方体:df_profile_cube (region,category,age_group,gender) -> user_count # Step1: 在销售立方体上,按region+category聚合,丢弃quarter/brand(因画像无这两个维度) df_sales_agg = (df_sales_cube['sales_sum'] .groupby(['region','category']) .sum() .rename('sales_sum')) # Step2: 在画像立方体上,按region+category聚合,丢弃age_group/gender(因销售无这两个维度) df_profile_agg = (df_profile_cube['user_count'] .groupby(['region','category']) .sum() .rename('user_count')) # Step3: merge on common index df_joined = (df_sales_agg.to_frame() .join(df_profile_agg.to_frame(), how='inner') .assign(sales_per_user=lambda x: x['sales_sum'] / x['user_count'])) # 结果:region, category, sales_sum, user_count, sales_per_user核心原则:Join前,必须将两个立方体reduce到相同的维度集合(这里是region+category),否则维度不匹配,join无意义。
5. 避坑指南:12个血泪教训总结的“多维聚合死亡陷阱”
以下是我踩过、团队成员踩过、客户生产环境炸过的12个真实陷阱,按致命程度排序:
| 序号 | 陷阱描述 | 后果 | 解决方案 | 我的实操备注 |
|---|---|---|---|---|
| 1 | unstack()不指定level或fill_value | 值错位、NaN传染、报表全错 | 永远用unstack('dim_name', fill_value=0) | 在公司代码规范中,unstack必须带两个参数,CI检查强制 |
| 2 | 对已聚合数据二次groupby()(如对sales_sum再groupby('region').sum()) | 逻辑错误:把聚合值当明细,重复计算 | 聚合态数据只做unstack/stack/pivot,不做二次groupby | 在代码审查中,看到df.groupby(...).sum()作用于已有聚合列,立即打回 |
| 3 | pivot()前未groupby().sum()去重 | Index contains duplicate entries报错 | pivot()前必加groupby().agg(),或改用pivot_table() | 我们封装了safe_pivot()函数,自动检测并聚合 |
| 4 | 时间维度未排序就shift() | Q3的值与Q1对齐,增长率全乱 | sort_index(level='time_dim')后再shift() | 在ETL流水线中,加入assert df.index.is_monotonic_increasing校验 |
| 5 | melt()后未重命名var_name为业务维度名 | 得到variable列,无法参与分析 | melt(..., var_name='region'),命名即契约 | 所有ETL脚本,melt的var_name必须是维度字典中的key |
| 6 | 多维unstack()时维度顺序错乱(如unstack(['quarter','region'])vsunstack(['region','quarter'])) | 列名变成('Q1','华东')元组,后续无法处理 | 用unstack('quarter').unstack('region')分步,或用reorder_levels() | reorder_levels([0,2,1,3])这种魔法数字,必须加注释说明物理含义 |
| 7 | fillna()用0但业务语义是“无数据”而非“零销售” | 统计偏差:把缺失当0,拉低均值 | 缺失值用np.nan,计算时用skipna=True,展示时再fillna(0) | BI展示层和计算层分离,计算层保留NaN,展示层填0 |
| 8 | groupby()维度漏掉关键字段(如漏brand) | 立方体维度不全,无法支持品牌分析 | 用df.groupby(list_of_all_dims),维度列表从配置中心读取 | 我们维护DIMENSION_CONFIG.yaml,所有聚合脚本引用它 |
| 9 | stack()后未reset_index()就merge() | 索引层级混乱,merge失败或错位 | stack().reset_index(name='value')成为标准动作 | 封装to_tidy_df()函数,统一处理 |
| 10 | 用pd.concat()合并多个unstack()结果,索引未对齐 | 行错位,concat后出现NaN行 | 合并前reindex()到相同索引,或用join='outer' | pd.concat([df1, df2], join='outer', sort=False)更安全 |
| 11 | pivot_table()的aggfunc用'mean'但业务要'sum' | 业务指标错误,决策失误 | aggfunc必须与业务需求强绑定,写死在配置中 | 配置项:AGG_FUNC_MAP = {'sales': 'sum', 'conversion_rate': 'mean'} |
| 12 | 多维聚合后未做dtypes优化(如int64存小整数) | 内存浪费300%,Spark作业OOM |
