多维聚合数据操纵:从SQL分组到决策就绪的立方体变形 1. 这不是“加个GROUP BY”就能搞定的事多维聚合中的数据变形真相你有没有遇到过这样的场景业务方甩来一张报表需求——“按地区、按产品线、按季度统计销售额、毛利、复购率再叠加同比和环比”你信心满满写完SQL跑出来一看维度交叉后行数爆炸空值遍地SUM(CASE WHEN...)嵌套三层还漏了某个组合最后不得不导出Excel手动透视这不是你SQL没学好而是你还没真正踩进多维聚合的数据操纵深水区。Part 20 这个标题看着像教科书章节编号但它直指一个被严重低估的硬核能力Data Manipulation in Multi-Dimensional Aggregation——在多维聚合语境下对数据进行有目的、可控制、可复现的主动变形操作。它不是简单的分组求和而是像捏陶土一样在“地区×产品×时间”这个三维立方体上切、削、拉、压、旋转、填充、补洞。核心关键词就三个多维Multi-Dimensional、聚合Aggregation、操纵Manipulation。这三者缺一不可。少了“多维”就是单表GROUP BY少了“聚合”就是普通JOIN或FILTER少了“操纵”那就只是被动展示结果。它解决的是真实商业分析中那个最痛的点原始聚合结果无法直接服务于下游决策动作——比如销售总监要对比华东区A类新品Q3 vs Q2的毛利结构变化但数据库里只有按“大区品类季度”三级分组的总毛利没有“结构占比”没有“变动归因”更没有“异常标记”。这篇文章就是为你写的无论你是刚能写COUNT(*)的初级分析师还是天天和Pandas pivot_table搏斗的数据工程师只要你需要把聚合结果变成“能用、好用、敢用”的决策燃料你就必须吃透这一关。我干这行十二年带过三十多个BI项目90%以上的返工和口径争议根源都在这一环没做扎实。2. 为什么不能只靠SQL GROUP BY多维聚合操纵的本质与设计逻辑2.1 多维聚合不是“多加几个字段”而是一个立方体空间建模很多人把“按A、B、C分组”理解为SQL里GROUP BY a, b, c这么简单。这是致命误解。真正的多维聚合本质是在构建一个OLAP立方体OLAP Cube。想象一个真实的物理立方体X轴是地区华东、华北、华南Y轴是产品线硬件、软件、服务Z轴是时间2023Q1、2023Q2、2023Q3。每个顶点如[华东, 硬件, 2023Q2]就是一个唯一的单元格Cell里面存放着该组合下的聚合值如销售额1250万。这个立方体有三个关键属性维度Dimensions、度量Measures、层次Hierarchies。维度就是坐标轴地区、产品、时间度量就是你要算的数值销售额、订单数层次则是维度内部的上下级关系比如“时间”维度包含“年→季度→月→日”的层次“地区”包含“大区→省→市”的层次。而“Data Manipulation”的起点恰恰是从这个立方体模型出发的。你不是在操作一堆扁平的行而是在操作一个有结构、有关系、有路径的立体空间。所以当业务说“我要看华东区硬件产品在Q2的销售额以及它占华东区总销售额的比例”你脑子里第一个反应不应该是“写个子查询”而应该是“我在[地区华东]这个切片上沿着[产品线]维度做一次‘下钻Drill-down’然后在[华东]这个子立方体里计算[硬件]单元格的值占整个[华东]切片所有产品线单元格之和的比例”。这个思维转换是区分“会写SQL”和“懂多维分析”的分水岭。我见过太多人用几十行嵌套CASE WHEN硬生生把一个本该用两行MDX或DAX就能表达的“父级占比”逻辑写得面目全非最后连自己都看不懂。2.2 “Manipulation”的四大核心动作切、比、补、标在立方体模型里“Manipulation”绝非泛泛而谈它有四个明确、可落地、可编程的核心动作每一个都对应着真实业务场景的刚需切片Slicing与切块Dicing这是最基础也最容易被忽视的动作。“切片”是固定一个维度取值观察其他维度的变化比如“固定时间2023Q2看各地区、各产品线的销售额”。这对应SQL里的WHERE time 2023Q2。“切块”则是同时固定多个维度的取值范围形成一个子立方体比如“看华东和华南地区、硬件和软件产品、2023Q2和Q3的数据”。这对应WHERE region IN (华东,华南) AND product IN (硬件,软件) AND time IN (2023Q2,2023Q3)。关键在于切片/切块不是为了过滤数据而是为了定义后续所有操纵动作的作用域。很多错误就源于作用域没定清楚比如想算“华东区各产品的Q2占比”却忘了先切片到华东区导致分母变成了全国总额。比较Comparison这是驱动决策的核心。它远不止于“本期vs上期”。在多维空间里比较有严格的路径依赖时间序列比较同维度组合下不同时间点的度量值对比Q2 vs Q1。这要求时间维度有明确的层次和顺序。结构占比比较同一时间点下某维度成员占其父级或全集的比例硬件销售额 / 华东区总销售额。这要求能动态识别“父级”。对标比较与指定基准如行业均值、目标值、标杆区域的差异华东Q2实际/目标112%。这要求外部基准能与立方体坐标对齐。变动归因比较分解总量变动的原因Q2比Q1增长1000万其中地区贡献300万产品贡献500万价格贡献200万。这需要更高级的加权计算。填充与补全Filling Completing现实世界的数据是稀疏的。一个新成立的“西南区”在2023Q1可能没有任何销售记录数据库里就根本没有[西南, *, 2023Q1]这条记录。但报表里必须显示“0”。这就是稀疏立方体Sparse Cube的稠密化Densification。Manipulation必须能主动“造出”那些本不存在的单元格并赋予合理默认值0、NULL、前向填充值等。这在SQL里需要CROSS JOIN生成所有可能组合再LEFT JOIN原数据过程繁琐且易错。在Pandas里reindex()配合fill_value是标准解法但前提是索引必须是MultiIndex且层级对齐。标注与标记Annotation Flagging这是让数据“开口说话”的关键一步。聚合结果本身是冰冷的数字Manipulation要给它打上业务语义标签异常标记CASE WHEN sales LAG(sales) OVER (PARTITION BY region, product ORDER BY time) * 1.5 THEN 暴增 ELSE 正常 END状态标记根据多个度量组合判断毛利0 AND 复购率30% → “高毛利低粘性”等级标记将连续数值映射为离散等级销售额0-100万→C级100-500万→B级500万→A级这四大动作构成了Manipulation的完整能力图谱。任何脱离这个图谱的“技巧”比如单纯用UNION ALL拼接不同粒度的结果都是在用蛮力绕开问题本质后患无穷。2.3 方案选型为什么放弃纯SQL拥抱混合技术栈面对上述复杂需求纯SQL方案很快就会崩溃。原因很实在SQL是为描述“静态集合”而生不是为描述“动态空间操作”而生。当你需要在一个查询里同时完成“切片→填充→计算父级占比→打异常标”时SQL的语法会变得极其臃肿。我曾维护过一个报表其核心SQL长达800行包含7层嵌套子查询、12个窗口函数、3个CROSS JOIN每次业务方加一个新维度整个查询就要重写。这不是技术问题是范式错配。因此我们团队在Part 20实践中坚定采用混合技术栈Hybrid Stack底层数据准备层用SQL或Spark SQL完成最耗资源的原始聚合GROUP BY region, product, time和基础切片。目标是产出一个结构清晰、无冗余、已去重的“宽表基底”比如fact_sales_agg字段为region_id, product_id, quarter, sales_amt, gross_profit。中层操纵层用Python Pandas或R dplyr作为主力操纵引擎。理由非常硬核Pandas的DataFrame天然支持MultiIndexpivot_table()、stack()/unstack()、groupby().apply()、reindex()等方法是对立方体进行“切、比、补、标”的完美抽象。一行df.groupby([region, product]).sales_amt.transform(sum)就能拿到每个组合的“区域-产品”粒度销售额再除以df.groupby(region).sales_amt.transform(sum)父级占比瞬间完成。代码可读性、可维护性、可测试性远超等效SQL。上层呈现层用BI工具如Tableau、Power BI或自研Web应用消费中层输出的结构化结果。BI工具强大的可视化和交互能力正好承接Manipulation后的丰富语义。这个分层不是为了炫技而是基于每个工具的比较优势Comparative Advantage。SQL擅长海量数据的并行扫描和聚合Pandas擅长灵活、迭代、面向对象的数据变形BI工具擅长用户交互和视觉传达。强行让一个工具干所有活就像让挖掘机去绣花效率低下且效果糟糕。我亲眼见过一个团队为了“统一技术栈”坚持用纯SQL实现所有Manipulation结果开发周期延长3倍上线后性能下降70%最终不得不推倒重来。3. 核心细节拆解从原始聚合到决策就绪数据的七步实操3.1 第一步定义清晰、无歧义的维度与度量字典一切Manipulation的起点是一份被所有人认可的、版本化的数据字典。这不是文档工作是技术基建。我们为Part 20项目制定了如下核心字典维度名称层次结构示例唯一标识符业务含义特殊规则region大区→省→市region_id(INT)销售地理覆盖范围region_id0表示“未分配”大区层级需预计算parent_region_idproduct_line一级品类→二级品类→SKUproduct_id(VARCHAR)公司销售的产品分类product_idALL为虚拟汇总码一级品类需在ETL中固化time_quarter年→季度quarter_id(CHAR(6), e.g., 2023Q2)财务报告周期必须使用ISO标准季度编码禁止使用自然月提示维度ID必须是稳定、无业务含义的代理键Surrogate Key。我吃过亏早期用region_name作为分组字段结果市场部把“华东区”改名为“华东及华中大区”所有历史报表口径瞬间失效。现在一律用region_id名字变更只影响维度表的name字段不影响事实表关联和聚合逻辑。度量Measures同样需要明确定义sales_amt确认收入单位人民币元精度2位小数不含税。gross_profitsales_amt-cost_of_goods_sold必须与sales_amt在同一会计期间确认。order_count去重订单数DISTINCT order_id非行数。这份字典是我们所有Manipulation操作的“宪法”。任何对聚合逻辑的讨论都必须回归到字典定义。它杜绝了“我以为的销售额”和“你理解的销售额”之间的扯皮。3.2 第二步构建“最小完备”原始聚合宽表不要试图在第一步就产出所有维度组合。我们的原则是只聚合到业务逻辑上最细、最不可再分的粒度Atomic Granularity。对于销售分析这个粒度通常是[region_id, product_id, quarter_id]。我们用Spark SQL执行-- Part 20: Base Aggregation Query INSERT OVERWRITE TABLE fact_sales_agg SELECT COALESCE(r.region_id, 0) AS region_id, COALESCE(p.product_id, UNKNOWN) AS product_id, CONCAT(YEAR(o.order_date), Q, QUARTER(o.order_date)) AS quarter_id, SUM(o.sales_amount) AS sales_amt, SUM(o.sales_amount - o.cost_amount) AS gross_profit, COUNT(DISTINCT o.order_id) AS order_count FROM ods_orders o LEFT JOIN dim_region r ON o.region_code r.region_code LEFT JOIN dim_product p ON o.product_sku p.sku_code WHERE o.order_status CONFIRMED AND o.order_date 2022-01-01 GROUP BY COALESCE(r.region_id, 0), COALESCE(p.product_id, UNKNOWN), CONCAT(YEAR(o.order_date), Q, QUARTER(o.order_date));关键细节COALESCE处理NULL确保维度ID总有值避免后续GROUP BY产生意外的NULL组合。CONCAT生成标准季度ID统一格式避免2023-Q2、Q2-2023等混乱。WHERE条件前置在聚合前过滤掉无效订单极大提升性能。不JOIN维度表的名称字段只保留ID。名称、层级等描述性信息留到Pandas层通过map()或merge()动态注入保证灵活性。这张宽表就是我们Manipulation的唯一数据源。它的行数就是这个三维立方体中所有“非零”单元格的数量。我们称它为“最小完备”表因为它包含了所有后续操作所需的原子信息且没有冗余。3.3 第三步Pandas中构建MultiIndex立方体骨架加载宽表后第一件事不是计算而是构建立方体的骨架Skeleton。这一步决定了后续所有Manipulation的稳定性和可扩展性。import pandas as pd import numpy as np # 1. 加载基础聚合表 df_base spark.read.table(fact_sales_agg).toPandas() # 2. 构建MultiIndex骨架所有可能的维度组合 # 获取各维度的唯一值列表含ALL汇总码 regions list(df_base[region_id].unique()) [ALL] products list(df_base[product_id].unique()) [ALL] quarters list(df_base[quarter_id].unique()) # 创建完整的笛卡尔积索引 idx_full pd.MultiIndex.from_product( [regions, products, quarters], names[region_id, product_id, quarter_id] ) # 3. 将基础数据设置为MultiIndex DataFrame df_cube df_base.set_index([region_id, product_id, quarter_id]) # 4. 用full index重新索引实现稠密化缺失值填0 df_cube df_cube.reindex(idx_full, fill_value0).reset_index()这段代码完成了三件大事from_product生成了[region_id, product_id, quarter_id]所有可能的组合包括[ALL, ALL, 2023Q2]这种全汇总。set_indexreindex将稀疏的原始数据“拉伸”成一个稠密的立方体骨架。原来没有[5, PROD-001, 2023Q1]记录没关系reindex会自动创建这一行sales_amt等字段填0。reset_index把MultiIndex变回普通列方便后续groupby等操作。注意fill_value0是安全的因为我们处理的是销售额、订单数等可加性度量Additive Measures。如果是平均值、比率等半可加性Semi-additive或不可加性Non-additive度量这里就不能简单填0而要用np.nan并在后续计算中显式处理缺失值。这是新手常踩的坑——用0填充毛利率导致“无销售区域”的毛利率被算成0%严重误导决策。3.4 第四步执行核心Manipulation——“切、比、补、标”现在立方体骨架已就位我们开始注入灵魂。以下代码全部基于df_cube这个稠密DataFrame# 1. 【切片】固定时间分析Q2 df_q2 df_cube[df_cube[quarter_id] 2023Q2].copy() # 2. 【比较-结构占比】计算各产品线在各地区的销售额占比 # 步骤a先计算每个地区的总销售额父级聚合 region_total df_q2.groupby(region_id)[sales_amt].transform(sum) # 步骤b计算占比注意处理分母为0 df_q2[sales_pct_in_region] np.where( region_total 0, df_q2[sales_amt] / region_total, 0.0 ) # 3. 【比较-时间序列】计算Q2 vs Q1的环比增长率 # 先获取Q1数据 df_q1 df_cube[df_cube[quarter_id] 2023Q1].copy() # 重命名Q1的度量列便于合并 df_q1 df_q1.rename(columns{ sales_amt: sales_amt_q1, gross_profit: gross_profit_q1, order_count: order_count_q1 }) # 与Q2数据按region_id, product_id合并 df_compare pd.merge( df_q2, df_q1[[region_id, product_id, sales_amt_q1, gross_profit_q1, order_count_q1]], on[region_id, product_id], howleft ) # 计算环比处理Q1为0的情况 df_compare[sales_mom_pct] np.where( df_compare[sales_amt_q1] 0, (df_compare[sales_amt] - df_compare[sales_amt_q1]) / df_compare[sales_amt_q1], np.nan # Q1无数据环比无意义 ) # 4. 【标注-异常标记】标记销售额环比增长50%的产品 df_compare[sales_mom_flag] np.where( df_compare[sales_mom_pct] 0.5, 暴增, np.where(df_compare[sales_mom_pct] -0.3, 暴跌, 平稳) ) # 5. 【补全-父级汇总】添加ALL维度的汇总行 # 按region_idALL汇总所有产品和季度 all_region df_cube.groupby([product_id, quarter_id])[ [sales_amt, gross_profit, order_count] ].sum().reset_index() all_region[region_id] ALL # 按product_idALL汇总所有地区和季度 all_product df_cube.groupby([region_id, quarter_id])[ [sales_amt, gross_profit, order_count] ].sum().reset_index() all_product[product_id] ALL # 合并所有基础数据和汇总数据 df_final pd.concat([ df_compare, all_region, all_product ], ignore_indexTrue)这段代码展示了Manipulation的威力结构占比用transform(sum)在分组内广播父级总和一行代码搞定无需子查询。时间序列比较用merge替代SQL的JOIN逻辑清晰且howleft保证了Q2有数据而Q1无数据的组合不会丢失。异常标注np.where链式调用语义一目了然。父级汇总groupby().sum()后concat轻松生成ALL行为BI工具的“钻取”功能提供数据基础。3.5 第五步处理半可加性度量——毛利率的特殊战术毛利率gross_profit / sales_amt是典型的半可加性度量它不能跨时间相加Q1毛利率Q2毛利率≠H1毛利率也不能跨地区相加。直接对gross_profit和sales_amt分别求和再相除是常见错误。正确做法是在最细粒度上计算再按需聚合。我们在Part 20中采用了“分子分母分离聚合后计算”的策略# 在df_base原始聚合上先计算每行的毛利率避免0除 df_base[gp_rate_raw] np.where( df_base[sales_amt] 0, df_base[gross_profit] / df_base[sales_amt], np.nan ) # 当需要计算“华东区Q2的毛利率”时 # 步骤1取出华东区Q2的所有明细行即使原始宽表是聚合的我们也保留了明细ID的线索 # 步骤2对这些行的gross_profit求和对sales_amt求和 # 步骤3用总毛利 / 总销售额 # 这在Pandas中就是 df_east_q2 df_base[(df_base[region_id] 5) (df_base[quarter_id] 2023Q2)] east_q2_gp_total df_east_q2[gross_profit].sum() east_q2_sales_total df_east_q2[sales_amt].sum() east_q2_gp_rate east_q2_gp_total / east_q2_sales_total if east_q2_sales_total 0 else np.nan # 如果必须在宽表上操作就用weighted average近似 # (sum(gp_rate_raw * sales_amt) / sum(sales_amt)) df_cube[weighted_gp_rate] df_cube[gp_rate_raw] * df_cube[sales_amt] # 然后在groupby时用sum(weighted_gp_rate) / sum(sales_amt)这个例子说明Manipulation不是一套死板的公式而是需要深入理解业务度量的数学性质。没有银弹只有针对不同度量的定制化战术。4. 实操过程全记录从环境搭建到生产部署的完整流水线4.1 环境与依赖轻量、稳定、可复现我们摒弃了复杂的虚拟环境管理采用最务实的方案Python版本3.9.16LTS长期支持版避免新特性带来的兼容性风险核心库pandas1.5.31.5.x系列是最后一个支持Python 3.7且API稳定的版本避免1.4.x的bug和2.x的breaking changenumpy1.23.5与pandas 1.5.3完美匹配pyarrow11.0.0大幅提升Pandas读写Parquet的性能尤其对大宽表部署方式所有代码和依赖打包为一个>FROM python:3.9.16-slim # 安装系统依赖 RUN apt-get update apt-get install -y \ libglib2.0-0 \ libsm6 \ libxext6 \ rm -rf /var/lib/apt/lists/* # 复制并安装Python依赖 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制代码 COPY src/ /app/src/ WORKDIR /app # 入口脚本 CMD [python, src/main.py]requirements.txt内容精简到极致pandas1.5.3 numpy1.23.5 pyarrow11.0.0 pyspark3.4.0 # 用于连接Spark集群读取宽表实操心得我们曾经在测试环境用pandas2.0.0结果发现reindex()在处理超大MultiIndex时内存暴涨300%且transform()行为有细微变化导致一个关键报表的占比计算偏差0.02%。线上环境绝对不用非LTS版本。稳定压倒一切。4.2 核心脚本main.py一个文件五个阶段整个Manipulation流程被封装在一个清晰的main.py中分为五个阶段每个阶段都有明确的输入、输出和日志# src/main.py import logging from datetime import datetime import pandas as pd import numpy as np from pyspark.sql import SparkSession # 配置日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(/var/log/manipulation/part20.log), logging.StreamHandler() ] ) logger logging.getLogger(__name__) def stage_1_load_data(spark: SparkSession) - pd.DataFrame: 阶段1从Spark读取基础聚合宽表 logger.info(Stage 1: Loading base aggregation table...) df spark.read.table(fact_sales_agg).toPandas() logger.info(fLoaded {len(df)} rows from fact_sales_agg) return df def stage_2_build_cube(df_base: pd.DataFrame) - pd.DataFrame: 阶段2构建稠密MultiIndex立方体骨架 logger.info(Stage 2: Building dense cube skeleton...) # ... (同3.3节代码) logger.info(fCube skeleton built with {len(df_cube)} rows) return df_cube def stage_3_apply_manipulation(df_cube: pd.DataFrame) - pd.DataFrame: 阶段3执行核心Manipulation逻辑 logger.info(Stage 3: Applying core manipulations (slice, compare, fill, flag)...) # ... (同3.4节代码) logger.info(Core manipulations applied successfully) return df_final def stage_4_validate_output(df_final: pd.DataFrame): 阶段4关键业务规则校验 logger.info(Stage 4: Running business rule validations...) # 校验1所有sales_pct_in_region应在[0,1]区间 pct_outliers df_final[ (df_final[sales_pct_in_region] 0) | (df_final[sales_pct_in_region] 1) ] if len(pct_outliers) 0: raise ValueError(fFound {len(pct_outliers)} rows with invalid sales_pct_in_region) # 校验2ALL汇总行的sales_amt应等于其下属所有行的sum all_region_sum df_final[df_final[region_id] ALL][sales_amt].sum() non_all_sum df_final[df_final[region_id] ! ALL][sales_amt].sum() if abs(all_region_sum - non_all_sum) 1e-6: raise ValueError(Region ALL total does not match sum of non-ALL regions) logger.info(All business validations passed) def stage_5_save_output(df_final: pd.DataFrame): 阶段5保存结果到目标表 logger.info(Stage 5: Saving final result to target table...) # 使用PyArrow高效写入Parquet df_final.to_parquet( /data/output/part20_final.parquet, enginepyarrow, compressionsnappy, indexFalse ) logger.info(Final result saved successfully) if __name__ __main__: start_time datetime.now() logger.info(fPart 20 Data Manipulation STARTED at {start_time}) try: # 初始化Spark spark SparkSession.builder \ .appName(Part20-Manipulation) \ .getOrCreate() # 执行五个阶段 df_base stage_1_load_data(spark) df_cube stage_2_build_cube(df_base) df_final stage_3_apply_manipulation(df_cube) stage_4_validate_output(df_final) stage_5_save_output(df_final) end_time datetime.now() duration end_time - start_time logger.info(fPart 20 Data Manipulation COMPLETED in {duration.total_seconds():.2f} seconds) except Exception as e: logger.error(fPart 20 Data Manipulation FAILED: {str(e)}, exc_infoTrue) raise finally: if spark in locals(): spark.stop()这个脚本的设计哲学是可调试、可监控、可审计。每个阶段都有日志记录起止时间和关键指标stage_4_validate_output是质量防火墙任何业务规则违反都会中断流程绝不让脏数据流入下游try/except/finally确保Spark资源被正确释放。我们在线上运行时会将日志实时推送至ELK任何ERROR级别的日志都会触发企业微信告警。4.3 生产调度Airflow DAG的健壮性设计我们使用Apache Airflow 2.6.3进行调度。DAG定义的关键在于失败重试与依赖隔离# dags/part20_dag.py from airflow import DAG from airflow.operators.python import PythonOperator from airflow.operators.bash import BashOperator from airflow.providers.apache.spark.operators.spark_submit import SparkSubmitOperator from datetime import datetime, timedelta default_args { owner: data-engineering, depends_on_past: False, start_date: datetime(2023, 1, 1), email_on_failure: True, email_on_retry: False, retries: 2, # 仅重试2次避免无限循环 retry_delay: timedelta(minutes5), execution_timeout: timedelta(hours2), # 防止任务卡死 } dag DAG( part20_data_manipulation, default_argsdefault_args, descriptionPart 20: Multi-Dimensional Aggregation Manipulation, schedule_interval0 3 * * 1, # 每周一凌晨3点执行 catchupFalse, tags[data-manipulation, sales-analysis], ) # 任务1检查上游宽表是否已就绪防依赖断裂 check_upstream BashOperator( task_idcheck_upstream_table, bash_commandspark-sql -e SELECT COUNT(*) FROM fact_sales_agg WHERE quarter_id \{{ macros.ds_format(ds, \%Y-%m-%d\, \%YQ%q\) }}\ | grep -q 0 exit 1 || echo Upstream ready, dagdag, ) # 任务2运行Manipulation主脚本Docker容器 run_manipulation BashOperator( task_idrun_manipulation_script, bash_commanddocker run --rm -v /data:/data -v /var/log:/var/log>问题现象可能根因排查步骤解决方案报表中出现大量NaN或Inf1. 分母为0的除法运算2.np.where条件判断逻辑错误分支未覆盖所有情况3.reindex(fill_value0)误用于非可加性度量1. 在stage_3_apply_manipulation中对所有/运算前后加print(fDEBUG: denom{denom}, num{num})2. 用df_final.isna().sum()和df_final.is