Pandas多维聚合实战:构建可交互OLAP立方体 1. 项目概述当数据不再是一张“平铺直叙”的表格你有没有遇到过这样的场景销售部门要按季度、按区域、按产品大类看毛利同时还要对比去年同期财务团队需要把成本拆解到“部门-项目-费用类型-发生月份”四个维度再筛选出超预算的组合甚至一个简单的用户行为分析都要交叉统计“新老用户 × 设备类型 × 页面路径深度 × 当日活跃时段”。这时候Excel 的透视表点到第三层就开始卡顿SQL 里写个 GROUP BY 加上 CASE WHEN 嵌套三层自己都快看不懂了——这已经不是“汇总”问题而是多维聚合Multi-Dimensional Aggregation的实战现场。本篇标题中的 “Part 20: Data Manipulation in Multi-Dimensional Aggregation”绝非教科书里抽象的“高维数组”概念它直指现代数据分析中一个最硬核、也最容易被低估的环节如何在保留原始数据颗粒度的前提下自由、高效、可复现地对多个维度进行任意组合、切片、钻取与比较。核心关键词——多维聚合、数据操作、维度建模、OLAP思维、分组聚合优化——全部围绕一个现实目标让数据从“静态报表”变成“可交互的决策仪表盘”。它适合三类人一是刚从单表 GROUP BY 走出来的 SQL 工程师正被业务方层出不穷的“再加一列维度”的需求压得喘不过气二是用 Pandas 做分析但总在 pivot_table 和 groupby 之间反复横跳、搞不清 index 层级和 unstack 逻辑的新手三是正在搭建 BI 系统、却对底层数据模型如何支撑“拖拽式分析”感到模糊的产品或数仓工程师。这不是讲理论是讲怎么在真实代码里把“按 A、B、C 三个字段分组求和”这件事做得既快又稳还能随时加 D 维度、删 B 维度、换聚合函数不改一行核心逻辑。2. 多维聚合的本质为什么不能只靠 GROUP BY 堆叠2.1 传统 GROUP BY 的“线性陷阱”与维度爆炸很多人初学多维聚合第一反应就是 SQL 里的GROUP BY a, b, c。这没错但问题在于GROUP BY 是“一次性声明”它不支持动态增减维度也不保存中间状态。举个具体例子假设你有一张 1000 万行的订单明细表包含order_id,product_id,region,sales_rep,order_date,amount字段。业务第一次要“各区域各销售代表的月度销售额”你写SELECT region, sales_rep, DATE_TRUNC(month, order_date) AS month, SUM(amount) AS total_amount FROM orders GROUP BY region, sales_rep, DATE_TRUNC(month, order_date);结果返回 5000 行。第二天业务说“再加个产品大类维度我要看华东区张三卖的手机类产品的月度趋势。” 你不得不重写整个查询把product_category需关联产品表加进 SELECT 和 GROUP BYWHERE 条件也要加过滤。第三天又要“排除退货订单”第四天要“计算同比增幅”……每一次需求变更都是全量重跑全量重写。更致命的是维度组合本身会指数级膨胀。如果region有 5 个值sales_rep有 20 个product_category有 8 个month有 24 个两年理论上最大组合数是 5×20×8×24 19,200 行。但实际数据稀疏——比如西北区可能只有 2 个销售代表卖手机其他组合根本不存在。传统 GROUP BY 强制计算所有笛卡尔积大量空结果浪费 CPU 和内存。我在一家电商公司做过实测一张 800 万行的交易日志用GROUP BY user_id, item_id, category_id, hour4 个高基数字段PostgreSQL 执行时间从 12 秒飙升到 3 分钟而其中 92% 的结果行是 NULL 或 0。这就是“线性陷阱”——它把维度当作并列的、不可分割的标签忽略了维度之间天然的层次关系Hierarchy和语义关系Drill-down/ Roll-up。区域下面有城市城市下面有门店日期有年、季、月、日、小时产品有类目、子类目、品牌、SKU。这些不是平级的字符串而是有父子结构的树状网络。强行用 GROUP BY 平铺等于让数据库替你做“人工树遍历”效率必然低下。2.2 多维聚合的正确打开方式立方体Cube思维真正的多维聚合其底层模型是OLAPOnline Analytical Processing立方体。你可以把它想象成一个 N 维的乐高积木每个维度Dimension是一条轴比如 X 轴是“时间”Y 轴是“地理”Z 轴是“产品”。每一个坐标点如 [2024-Q2, 华东, 手机]就是一个单元格Cell里面存放着该组合下的聚合值如销售额、订单数。关键在于这个立方体不是一次性“堆”出来的而是通过预计算Pre-aggregation和按需计算On-demand Computation的混合策略构建。预计算是指对高频、稳定的维度组合如“年-区域-产品大类”提前算好并存入物化视图或宽表按需计算则是利用现代引擎如 ClickHouse、Doris、甚至优化后的 Pandas的向量化执行能力在内存中快速完成低基数维度的实时聚合。这种思维转变直接决定了你的方案是“能跑通”还是“能扛住每天 500 次不同维度组合的自助分析”。我见过太多团队花三个月搭完 BI 系统结果业务方抱怨“换个维度就要等半分钟”根源就在于后端数据服务层还停留在“一个需求一个 SQL”的手工模式没建立起真正的立方体抽象。所以“Data Manipulation in Multi-Dimensional Aggregation” 的第一课不是学语法而是建立这个立方体心智模型维度是轴指标是值操作是沿轴移动、缩放、切片。2.3 核心技术栈选型为什么 Python/Pandas 是本篇的绝对主角既然目标是“数据操作Data Manipulation”那工具链的选择就至关重要。有人会问为什么不直接讲 SQL或者 StarRocks答案很实在Pandas 是连接“业务语义”和“工程实现”的最佳翻译器。SQL 强大但它本质是声明式语言你告诉数据库“要什么”但很难精确控制“怎么算”。而多维聚合的难点恰恰在“怎么算”——比如如何处理缺失维度的默认填充如何让“华东”下钻到“上海”时自动继承父级的过滤条件如何把“月度销售额”和“月度订单数”两个指标放在同一张宽表里且保证它们的分组逻辑完全一致这些细节SQL 很难优雅表达。Pandas 则不同它的groupby对象是一个活的、可迭代的、带状态的“聚合上下文”。你可以.agg({amount: sum, order_id: count})一次定义多个指标可以用.apply(lambda x: (x[amount].sum() / x[amount].count()))写自定义逻辑更关键的是pandas.MultiIndex天然支持多层索引.unstack()就是把某个维度“转成列”.stack()就是“转回行”这正是 OLAP 中“旋转Pivot”操作的代码映射。我在为一家 SaaS 公司重构客户分析模块时把原来 17 个硬编码的 SQL 报表统一收口到一个 Pandas Pipeline输入是原始事件流 DataFrame输出是一个MultiIndex的DataFrame索引是(customer_tier, acquisition_channel, quarter)列是[revenue, active_days, support_tickets]。业务方只要调用.xs(Enterprise, levelcustomer_tier)就能切片出企业客户数据.unstack(quarter)就能生成横向对比表。整个过程代码不到 200 行维护成本降了 80%。所以本篇聚焦 Pandas并非因为它“简单”而是因为它把多维聚合中最棘手的“状态管理”和“逻辑复用”问题用最贴近人类思维的方式解决了。后续章节的所有实操都将基于此展开。3. 核心操作详解从原始数据到可交互立方体的四步法3.1 第一步维度标准化与层级建模Dimension Modeling多维聚合的第一道坎往往不是计算而是数据清洗和维度对齐。原始数据里“区域”字段可能是 “华东”, “East China”, “EC”, “Shanghai, Jiangsu, Zhejiang” 四种写法混在一起“产品类目”可能是 “手机”, “Mobile Phone”, “Smartphone”, “iPhone 15 Pro” 这种粒度混乱的混合体。如果不先统一后续所有聚合都是空中楼阁。我的经验是必须建立一个独立的维度表Dimension Table哪怕只是内存里的一个字典或 DataFrame。以“地理维度”为例我通常会构建一个dim_geoDataFramegeo_idregion_nameregion_levelparent_idfull_path1中国countrynull中国2华东region1中国/华东3上海city2中国/华东/上海4浦东新区district3中国/华东/上海/浦东新区这个表的关键在于full_path字段——它用斜杠拼接了完整的层级路径。有了它任何原始数据中的“上海”、“EC”、“Shanghai”都可以通过模糊匹配或映射表统一归到geo_id3。更重要的是parent_id和full_path支持了下钻Drill-down和上卷Roll-up当你想从“华东”下钻到“上海”只需dim_geo[dim_geo[parent_id]2]想从“上海”上卷到“华东”查dim_geo.loc[3, parent_id]就能得到 2再查dim_geo.loc[2, region_name]就是“华东”。这比在 SQL 里写一堆CASE WHEN region IN (Shanghai, Nanjing, ...) THEN East China清晰、可维护一万倍。在 Pandas 里我习惯用pd.merge()把原始订单表和dim_geo表关联生成order_enriched其中新增geo_id,region_name,city_name等标准化字段。这一步看似繁琐但它是后续所有操作稳定性的基石。我踩过的最大坑就是在一次大促复盘中因为“渠道”维度没做标准化把 “微信公众号”、“WeChat Official Account”、“WX OA” 当作三个独立渠道统计导致总流量虚高 37%最后花了两天才定位到问题。记住维度建模不是为了好看是为了让“华东”永远等于“华东”而不是某次 SQL 里写对、某次写错的字符串。3.2 第二步构建多层索引MultiIndex与基础聚合完成维度标准化后真正的聚合就开始了。Pandas 的groupby是核心但关键在于如何组织 groupby 的 keys。新手常犯的错误是直接df.groupby([region, city, product_category])这会产生一个扁平的、无结构的索引。而我们要的是一个有层级的、可导航的索引。正确做法是先用pd.MultiIndex.from_frame()或直接在groupby中传入一个列表然后立刻.agg()# 假设 order_enriched 已包含标准化字段 base_agg ( order_enriched .groupby([region_name, city_name, product_category, order_month]) .agg( total_revenue(amount, sum), order_count(order_id, count), avg_order_value(amount, mean), unique_customers(user_id, nunique) ) .round(2) # 数值统一保留两位小数 )这段代码执行后base_agg的索引.index就是一个MultiIndex其names是[region_name, city_name, product_category, order_month]每一层都清晰可辨。你可以用base_agg.index.get_level_values(region_name)单独提取某一层用base_agg.xs(华东, levelregion_name)快速切片用base_agg.unstack(order_month)把月份转成列。这里有个重要技巧聚合函数的命名必须明确体现指标含义和计算逻辑。我坚持用total_revenue而不是sum_amount因为业务方只关心“营收”不关心底层是 sum 还是 avg用unique_customers而不是nunique_user_id因为nunique是 Pandas 内部术语业务方看不懂。这看似是命名规范实则是降低协作成本的关键。另外.round(2)不是可选项——浮点数精度问题在多维聚合中会被放大。我曾在一个金融项目中因为没做 roundsum()后的数值和 Excel 里手动加总差了 0.0000001业务方死活不信这是精度问题折腾了一整天。所以数值规范化是多维聚合的“出厂设置”。3.3 第三步动态切片、钻取与旋转Slicing, Drilling, Pivoting有了MultiIndex的base_agg你就拥有了一个轻量级的内存立方体。接下来的所有操作都是在这个立方体上“做手术”。我们分三类典型场景来看场景一切片Slicing——固定某些维度查看子集这是最常用的操作。比如老板问“只看手机类产品的数据。” 你不需要重跑聚合只需mobile_data base_agg.xs(手机, levelproduct_category) # mobile_data 的索引现在只剩 [region_name, city_name, order_month]如果要同时固定两个维度比如“华东 手机”用元组east_mobile base_agg.xs((华东, 手机), level[region_name, product_category])提示.xs()方法的drop_levelFalse参数很有用。默认它会把被切掉的维度从索引中移除但有时你想保留空壳以便后续对齐就设为False。场景二下钻Drilling-down——增加一个更细粒度的维度比如当前聚合是按“城市”做的现在要看到“城市”下的“区县”。前提是你的原始数据里有district_name字段且已标准化。操作很简单# 先在原始 enriched 表里加一层 district detailed_agg ( order_enriched .groupby([region_name, city_name, district_name, product_category, order_month]) .agg({ ... }) # 聚合逻辑同上 ) # 然后如果你只想看“上海”下的区县就 xs 切片 shanghai_districts detailed_agg.xs(上海, levelcity_name)注意这不是在base_agg上“添加”维度而是回到更细粒度的原始数据重新聚合。这是多维聚合的铁律上卷可以丢弃信息下钻必须回归源头。试图在base_agg城市级上“模拟”出区县级只会得到错误的平均值。场景三旋转Pivoting——改变展示形态服务不同场景BI 看板喜欢宽表每列一个月份运营日报喜欢长表每行一个时间点。Pandas 的.unstack()和.stack()就是为此而生。比如把order_month从行索引转成列monthly_wide base_agg.unstack(order_month) # 结果索引是 [region_name, city_name, product_category]列是 MultiIndex: [(total_revenue, 2024-01), (total_revenue, 2024-02), ...] # 如果只想看 revenue可以再 .droplevel(0, axis1) 去掉指标层 revenue_only monthly_wide[total_revenue]反过来如果宽表要变回长表用.stack()long_format revenue_only.stack(order_month).reset_index(nametotal_revenue)这四步法标准化 → 多层索引聚合 → 切片钻取 → 旋转构成了一个闭环。它不依赖任何外部数据库全部在内存中完成响应速度极快。我在一个实时监控项目中用这套方法处理每秒 5000 条的 IoT 设备上报数据从原始数据到生成 6 个维度、12 个指标的聚合视图平均耗时 180ms完全满足大屏秒级刷新要求。3.4 第四步指标衍生与跨维度计算Cross-Dimensional Calculation多维聚合的终极价值不在于“算得准”而在于“算得巧”。很多业务洞察来自于不同维度组合下的指标对比。比如“华东区手机品类的销售额占全国手机总销售额的比例”这就涉及跨区域、跨品类的分母计算。在 Pandas 里这通过.groupby(...).transform()或.agg()配合pd.concat()实现。以计算“区域占比”为例# 步骤1先算出全国手机的总销售额分母 national_mobile_total ( base_agg .xs(手机, levelproduct_category) .sum()[total_revenue] # 这里 sum() 是对所有 region/city/month 求和 ) # 步骤2为每个区域/城市/月份计算其占全国手机销售额的比例 base_agg[mobile_share_pct] ( base_agg.xs(手机, levelproduct_category)[total_revenue] / national_mobile_total * 100 ).round(2)更复杂的场景是“同比计算”。假设你要在base_agg里新增一列yoy_growth表示“本月销售额 vs 去年同月”。这需要把去年的数据“对齐”到今年的索引上。Pandas 的shift()在时间序列上很好用但多维索引下要用swaplevel和reindex# 假设 order_month 是 datetime 类型先确保索引按时间排序 base_agg_sorted base_agg.sort_index(levelorder_month) # 创建一个“去年同月”的索引把 order_month 减去 12 个月 last_year_idx base_agg_sorted.index.set_levels( base_agg_sorted.index.get_level_values(order_month) - pd.DateOffset(months12), levelorder_month ) # 用去年的索引去 reindex 今年的数据得到去年同月的值 last_year_values base_agg_sorted.reindex(last_year_idx)[total_revenue] # 计算同比增长率 base_agg_sorted[yoy_growth] ( (base_agg_sorted[total_revenue] - last_year_values) / last_year_values * 100 ).round(2)这段代码的精妙之处在于reindex()—— 它不是简单地“找去年的值”而是严格按索引的层级结构把去年的值“粘贴”到今年对应的位置上。即使某个城市去年没有卖手机reindex()也会返回 NaN避免了错误的 0 填充。这种跨维度计算能力是传统 GROUP BY 无法企及的。它让数据操作从“静态汇总”升级为“动态洞察”。4. 性能优化与避坑指南让多维聚合真正落地的 7 个实战心得4.1 内存是第一瓶颈如何优雅地处理千万级数据Pandas 在内存中操作当数据量超过 500 万行聚合开始明显变慢1000 万行以上groupby可能触发 OOMOut of Memory。这不是 Pandas 的缺陷而是设计使然。我的应对策略是“分而治之 智能缓存”。首先绝不让原始大表直接参与groupby。我会用pd.read_csv(..., chunksize50000)分块读取对每一块做轻量聚合比如只算region,product_category,hour三级再把所有块的聚合结果pd.concat()起来最后对这个“小聚合表”做最终的多维聚合。实测下来处理 2000 万行订单日志分块聚合比全量加载快 3.2 倍内存峰值降低 65%。其次善用categorical数据类型。对于高基数但取值固定的维度如region_name,product_category在读取时就指定dtype{region_name: category}。Pandas 会把字符串映射为整数 ID内存占用能减少 70% 以上groupby速度提升 2-3 倍。我在一个物流轨迹分析项目中把delivery_status取值只有 5 个pending, picked_up, in_transit, delivered, failed设为 categorygroupby时间从 4.8 秒降到 1.3 秒。最后果断放弃“全量立方体”幻想。不是所有维度组合都需要预计算。用value_counts()统计各维度的唯一值数量优先为低基数1000、高查询频次的组合做预聚合。比如region × product_category只有 5×840 种组合值得预存而user_id × order_month有百万级必须按需计算。这是经验也是取舍。4.2 索引层级的“隐形陷阱”顺序、缺失与重复MultiIndex是利器也是雷区。第一个陷阱是层级顺序。groupby([A,B,C])和groupby([C,B,A])产生的索引结构完全不同前者A是最外层后者C是最外层。这直接影响.xs()和.unstack()的行为。我的原则是按业务逻辑的“主次”排顺序通常是“宏观→微观”如 [region, city, district, product_category, product_subcategory]。这样切片“华东”时拿到的就是所有华东下的城市、区县、品类符合直觉。第二个陷阱是缺失值NaN。如果原始数据中city_name有空值groupby会把它们单独聚合成一个NaN维度。这通常不是你想要的。解决方案是在groupby前dropna()或用fillna(Unknown)统一占位。第三个陷阱是重复索引。当groupby的 keys 不能唯一标识一行时比如漏了某个维度Pandas 会报ValueError: Index has duplicate keys。排查方法是df.duplicated(subset[A,B,C]).sum()看有多少重复行。我的习惯是在构建MultiIndex前先运行df.drop_duplicates(subset[A,B,C], keepfirst)宁可丢数据也不要让聚合逻辑崩溃。这些细节文档里不会写但线上事故十有八九出在这里。4.3 聚合函数的“魔鬼细节”sum、count、nunique 的真实差异新手常以为sum()就是加法count()就是计数nunique()就是去重计数。但在多维聚合中它们的行为差异巨大直接影响业务口径。sum(amount)会忽略amount列的 NaN这是对的但count(amount)统计的是amount列非空值的数量而count()不带参数统计的是整行非空值的数量两者可能差很多。nunique(user_id)是去重用户数但如果user_id有大量 NaNnunique()默认会把 NaN 当作一个值去重导致结果虚高。解决方案是显式指定dropnaTrue(user_id, pd.NamedAgg(columnuser_id, aggfunclambda x: x.nunique(dropnaTrue)))。更隐蔽的坑是mean()。mean(amount)会自动忽略 NaN但如果你先sum(amount) / count(order_id)分母count(order_id)包含了所有行而分子sum(amount)只加了非空 amount结果就不对。所以永远优先使用 Pandas 内置的聚合函数名sum, mean, nunique而不是自己写 lambda除非你 100% 理解其 NaN 处理逻辑。我在一个广告效果分析中因为误用了count()而不是size()size()统计所有行包括 NaN导致 CTR点击率计算错误差点误导了千万级的投放预算调整。4.4 与 BI 工具的“最后一公里”如何导出为标准 OLAP 格式辛苦构建的内存立方体最终要服务于 Tableau、Power BI 或自研前端。它们期望的数据格式通常是“宽表”或“星型模型”。Pandas 导出的关键是.reset_index()的时机和方式。不要在base_agg上直接reset_index()那会丢失所有层级信息。正确流程是先用.unstack()把你需要的维度转成列再reset_index()。例如要导出给 BI 用的“区域-月份”宽表bi_ready ( base_agg .xs(手机, levelproduct_category) # 先切片 .unstack(order_month) # 月份转列 .droplevel(0, axis1) # 去掉指标层让列名变成 2024-01, 2024-02... .reset_index() # 把 region_name, city_name 变成普通列 .rename(columns{region_name: Region, city_name: City}) # 用业务友好的列名 ) bi_ready.to_csv(bi_input.csv, indexFalse)如果前端需要星型模型事实表 多个维度表那就分别导出base_agg.reset_index()作为事实表所有维度列 指标列dim_geo,dim_product等作为维度表。这里有个黄金法则事实表里永远只存维度 ID如geo_id,product_id不存维度名称如region_name维度名称全部放在单独的维度表里。这保证了数据一致性——修改“华东”的名称只需改维度表一行事实表自动生效。我在一个跨国项目中因为事实表里直接存了region_name当德国团队把 Europe 改成 EMEA 时历史数据里的 Europe 无法自动更新只能全量重刷损失了 12 小时的分析窗口。这个教训刻骨铭心。4.5 常见问题速查表从报错到解决方案的 5 分钟定位问题现象可能原因快速定位命令解决方案KeyError: xxxgroupby的 keys 中字段名拼写错误或该字段在 DataFrame 中不存在print(df.columns.tolist())检查字段名大小写、空格、特殊字符用df.rename(columns{old:new})统一ValueError: Index has duplicate keysgroupby的 keys 组合不能唯一标识每一行df.duplicated(subset[A,B,C]).sum()df.drop_duplicates(subset[A,B,C], keepfirst)AttributeError: Series object has no attribute xs对Series单列聚合结果误用了DataFrame方法type(base_agg)确保base_agg是DataFrame如果是Series先.to_frame()Performance is slow on large data未使用category类型或未分块处理df.memory_usage(deepTrue).sum()对高基数维度列astype(category)用chunksize分块NaN values in final result原始数据有缺失且聚合函数未正确处理df[[A,B,C]].isna().sum()fillna()或dropna()预处理聚合时用aggfunclambda x: x.sum(skipnaTrue)这张表是我过去三年在 12 个项目中被问得最多的问题总结。它不追求全面只解决“此刻卡住你、让你没法继续往下走”的问题。记住调试多维聚合80% 的时间花在数据探查上而不是写代码上。df.head(),df.info(),df.describe()这三个命令是你最忠实的战友。5. 实战案例复盘从零搭建一个电商销售多维分析看板5.1 业务背景与原始数据结构我们以一家中型跨境电商的真实需求为例。他们有三张核心表orders订单主表含order_id,user_id,order_date,statusorder_items订单明细含order_id,product_id,quantity,priceproducts商品主数据含product_id,category,brand,country_of_origin。业务方的核心诉求是实时查看“国家-品类-品牌”三个维度的销售额、订单数、客单价并支持按日期范围筛选、按国家下钻到城市、计算各品牌在品类内的市占率。原始数据量orders120 万行order_items350 万行products8 万行。挑战在于order_items是明细粒度必须先关联聚合country_of_origin在products表需要merge“城市”维度不在任何表中需从user_id关联的用户地址表获取但我们这次先聚焦主维度。5.2 代码实现一个可运行、可复用的完整 Pipeline以下是我在生产环境部署的简化版 Pipeline已去除公司敏感信息保留全部核心逻辑import pandas as pd import numpy as np from datetime import datetime, timedelta # 1. 数据加载与关联模拟 # orders pd.read_csv(orders.csv) # order_items pd.read_csv(order_items.csv) # products pd.read_csv(products.csv) # 为演示我们生成模拟数据 np.random.seed(42) orders pd.DataFrame({ order_id: [fORD_{i} for i in range(100000)], user_id: np.random.choice([fU_{i} for i in range(10000)], 100000), order_date: pd.date_range(2023-01-01, periods100000, freq10T).strftime(%Y-%m-%d), status: np.random.choice([completed, cancelled], 100000, p[0.95, 0.05]) }) order_items pd.DataFrame({ order_id: np.random.choice(orders[order_id], 250000), product_id: np.random.choice([fP_{i} for i in range(5000)], 250000), quantity: np.random.randint(1, 5, 250000), price: np.random.uniform(10, 500, 250000) }) products pd.DataFrame({ product_id: [fP_{i} for i in range(5000)], category: np.random.choice([Electronics, Clothing, Home Kitchen, Beauty], 5000), brand: np.random.choice([Apple, Samsung, Nike, Uniqlo, Dyson, Loreal], 5000), country_of_origin: np.random.choice([USA, China, Japan, Germany, France], 5000) }) # 2. 维度标准化构建 dim_country国家维度 dim_country pd.DataFrame({ country_code: [USA, China, Japan, Germany, France], country_name: [United States, China, Japan, Germany, France], continent: [North America, Asia, Asia, Europe, Europe] }).set_index(country_code) # 3. 数据融合从明细到宽表 fact_sales ( order_items .merge(orders, onorder_id, howleft) .merge(products, onproduct_id, howleft) .merge(dim_country, left_oncountry_of_origin, right_indexTrue, howleft) .query(status completed) # 只算完成订单 .assign( revenuelambda x: x[quantity] * x[price], order_monthlambda x: pd.to_datetime(x[order_date]).dt.to_period(