Pandas多维聚合实战:银行风控中的客户-产品-时间三维分析 1. 项目概述为什么“多维聚合”不是Pandas进阶技巧而是业务分析的生存技能我在银行风控部门干了七年从刚毕业写SQL查数的分析师到带三个人小团队做反欺诈模型的数据架构师。这七年里我亲手重构过四套核心报表系统也给二十多个业务部门做过数据赋能培训。最常被问到的问题不是“怎么建模”而是“老师这个指标能不能按客户产品时间三个维度一起算现在跑三次groupby再merge一跑就是四十分钟领导在催。”——这句话背后藏着的是真实世界里每天都在发生的效率损耗、逻辑错位和决策延迟。“Part 20: Data Manipulation in Multi-Dimensional Aggregation”这个标题听起来像教科书里的章节编号但在我日常工作中它对应的是一个具体、高频、高价值的动作把一张原始交易流水表在一次计算中同时回答五个不同角色的问题。财务要每个客户在每个产品线上的平均单笔金额和总手续费运营要识别出哪些客户在餐饮类目上交易波动最大max-min风控要追踪某个客户最近七天的消费均值是否突然跃升管理层要看南北方客户对不同产品的偏好矩阵而CEO办公室需要一份一页纸的客户健康度摘要包含总消费、客单价、交易频次、手续费占比甚至高价值交易占比。这些需求绝不是df.groupby(customer).mean()能覆盖的。它们要求你理解pandas聚合背后的计算图结构、内存布局逻辑和业务语义映射规则。关键词里提到的“Towards AI - Medium”其实暗示了一个重要事实这篇内容不是为纯学术研究写的而是为正在用Python处理真实业务数据的工程师、分析师、产品经理准备的。它不讲“什么是聚合”而是直击“为什么你的agg()调用慢了三倍”、“为什么unstack后列名变成元组你没法导出Excel”、“为什么rolling计算结果第一行是NaN却不敢删”。我见过太多人卡在这些细节上不是因为不会写代码而是没人告诉他们pandas的groupby不是黑箱它是一台精密的、可拆解、可调校的分析引擎。你今天花两小时搞懂agg({col: [mean, std]})生成的MultiIndex列结构明天就能省下每周五小时的手动整理报表时间。这不是炫技这是职业基本功。这篇文章的价值不在于教会你七个函数而在于帮你建立一套业务问题→聚合模式→代码实现→结果校验的完整思维链。比如当你看到“客户在餐饮类目的交易范围max-min”你应该立刻反应这是custom agg必须用lambda或命名函数不能用内置字符串它的业务含义是风险敞口评估不是统计描述输出必须保留原始分组键否则无法关联到客户画像表。这种条件反射式的判断力才是资深从业者和新手的本质区别。接下来的内容我会完全基于真实银行场景展开所有代码都经过生产环境验证所有坑都是我亲手踩过的——包括那个让整个ETL任务失败37次的reset_index(level0, dropTrue)陷阱。2. 核心设计思路为什么必须放弃“单列单聚合”的线性思维2.1 传统方式的三大硬伤性能、可维护性、业务失真很多分析师的初始方案很朴素对同一张表分别执行多次groupby操作再用pd.merge拼接。比如要算客户维度的“平均交易额”、“手续费最小值”、“交易次数”就写三段代码avg_amt df.groupby(customer_id)[amount].mean() min_fee df.groupby(customer_id)[fee].min() cnt_txn df.groupby(customer_id).size() result pd.merge(avg_amt, min_fee, left_indexTrue, right_indexTrue) result pd.merge(result, cnt_txn, left_indexTrue, right_indexTrue)这段代码在10万行数据上可能只慢0.3秒但在银行日均千万级交易的场景下它会暴露三个致命问题I/O与内存双重浪费每次groupby都要重新扫描整张DataFrame触发三次完整的哈希分组计算。pandas底层会为每次操作创建新的索引结构和中间数组内存占用呈线性增长。实测过对1亿行交易表做三次独立groupby峰值内存达12GB而用单次多聚合仅需4.1GB。逻辑割裂导致维护灾难当业务方突然要求“手续费最小值改成非零最小值排除免费交易”你得改三处代码——而且必须确保三处的过滤条件完全一致。去年我们有个项目因此上线后发现财务报表的手续费最小值用了fee 0但风控报表没加这个条件导致两个系统对同一客户的“最低成本”定义冲突引发跨部门扯皮。业务语义丢失独立计算无法保证分组键的严格对齐。如果某客户在某次交易中fee字段为空NaNmin()会返回NaN但size()仍会计数。合并后会出现“客户A有5笔交易但手续费最小值为空”的诡异结果。而单次聚合中pandas会自动对齐所有agg函数的输入序列天然保证数据一致性。提示pandas的agg()字典语法本质是构建一个“聚合计算图”。键是列名值是函数列表或字典它告诉引擎“对这一列同时执行这些操作”。引擎内部会一次性完成分组然后对每个分组块并行应用所有函数最后将结果按指定结构组装。这是性能差异的根本原因。2.2 多维聚合的底层逻辑从“平面表格”到“立方体思维”业务问题从来不是单维度的。银行看客户价值必然要交叉分析谁客户ID在什么时间月度/季度买了什么产品类别花了多少钱金额付了多少费手续费。这天然构成一个四维数据立方体Cube。传统SQL的GROUP BY只能切一刀如按客户而pandas的多级groupbyunstack让我们能自由旋转这个立方体。关键认知转变不要把groupby看作“分组操作”而要视为“定义分析坐标系”。groupby([customer_id, category])不是在分组是在声明“我的分析空间X轴是客户Y轴是产品类别”。后续所有agg、rolling、unstack都是在这个坐标系内进行运算。理解这点才能避免常见错误——比如有人试图对groupby([customer_id, category])的结果直接做rolling(7)这在数学上是无效的滚动窗口必须定义在有序的时间维度上而多级索引本身无序。正确做法是先sort_values(date)再groupby([customer_id, category])最后在分组内应用rolling。2.3 方案选型的黄金法则何时用内置函数何时必须自定义pandas内置agg函数sum、mean等经过高度优化Cython实现速度极快。但它们有明确边界只能处理标量输出且逻辑必须是通用统计学定义。一旦业务需求突破这个边界就必须转向自定义函数。我总结了一套现场决策树需求含“阈值”或“条件分支”→ 必须自定义如“高价值交易占比”金额300的交易数/总交易数、“活跃度评分”近30天交易次数×0.7 近90天总金额×0.3。内置函数无法嵌入if-else。需求含“序列依赖”或“权重”→ 必须自定义如“加权平均”新交易权重更高、“移动标准差”滚动窗口内计算std。内置rolling.std()虽存在但若需自定义窗口权重如指数衰减只能用apply()配合lambda。需求输出非标量→ 必须自定义如“交易金额分布直方图”返回numpy array、“首末交易时间差”返回timedelta。内置函数强制返回单个数字。需求需复用或审计→ 强烈建议命名函数lambda适合一行逻辑但业务规则往往需注释说明。比如def fraud_risk_score(series): 根据监管要求对单客户交易序列计算风险分前3笔交易均值×0.5 后3笔均值×0.5。六个月后新人接手代码一眼看懂意图无需翻文档。注意自定义函数性能比内置函数低3-5倍因Python解释器开销。所以策略是能用内置的绝不用自定义必须自定义时优先用vectorized操作np.xxx而非for循环。例如计算rangex.max()-x.min()比max(x)-min(x)快10倍因前者调用C底层后者是Python内置函数。3. 实操细节解析从代码到业务落地的七道关卡3.1 多列多函数聚合破解MultiIndex列名的“套娃”困局原始示例中df.groupby(merchant_category).agg({transaction_amount: [mean,median], processing_fee: [min,max]})输出的列是MultiIndex形如transaction_amount processing_fee mean median min max这在Jupyter里看着清爽但导出Excel或对接BI工具时90%的人会卡在第一步如何把这种嵌套列名展平成transaction_amount_mean这样的扁平字符串错误做法手动重命名result.columns [amt_mean, amt_median, fee_min, fee_max]。问题在于当列数增加到20且分组键变化时极易出错。正确解法用result.columns.map(_.join)。但注意这要求所有列名都是字符串。如果某列是数字如2024Q1join会报错。生产环境安全写法def flatten_columns(df): 安全展平MultiIndex列名兼容字符串和数字 if not isinstance(df.columns, pd.MultiIndex): return df # 将每层列名转为字符串再连接 flat_cols [] for col in df.columns: # 处理col可能是元组MultiIndex或单值普通Index if isinstance(col, tuple): parts [str(c) for c in col if c ! ] # 过滤空字符串 flat_cols.append(_.join(parts)) else: flat_cols.append(str(col)) df.columns flat_cols return df # 应用 result_flat flatten_columns(result) print(result_flat.columns.tolist()) # 输出: [transaction_amount_mean, transaction_amount_median, processing_fee_min, processing_fee_max]这个函数解决了三个实际痛点1避免_.join(col)对单层列名报错2过滤掉(, mean)中的空字符串3兼容数字列名如年份。我在某城商行部署时就因未处理数字列名导致季度报表导出时列名变成2024Q1_mean而BI工具认不出全表数据错位。3.2 自定义聚合函数从lambda到可审计的业务逻辑封装原始示例的lambda x: x.max() - x.min()简洁但无法满足生产需求。真实风控场景中“交易范围”需扩展为排除异常值剔除Top 1%和Bottom 1%的交易后再算range处理缺失值若分组内有效交易3笔返回NaN而非0添加业务标签range 500标记为“高波动”用于下游告警命名函数是唯一选择def robust_transaction_range(series, threshold_percentile1.0, min_valid_count3): 计算鲁棒交易范围剔除极端值后取max-min param series: 交易金额序列 param threshold_percentile: 剔除的百分位1.0表示剔除1% param min_valid_count: 有效交易最小数量不足则返回NaN return: float or NaN if len(series) min_valid_count: return np.nan # 剔除极端值 lower_bound np.percentile(series, threshold_percentile) upper_bound np.percentile(series, 100 - threshold_percentile) filtered series[(series lower_bound) (series upper_bound)] if len(filtered) min_valid_count: return np.nan return filtered.max() - filtered.min() # 使用 result df.groupby(merchant_category).agg({ transaction_amount: robust_transaction_range, processing_fee: lambda x: x.max() - x.min() # 简单场景仍可用lambda })这个函数的价值在于业务规则、参数、异常处理全部显式化。当合规部门审计时他们能直接看到threshold_percentile1.0知道这是按监管指引设置的当数据质量下降时min_valid_count保护了结果可靠性。而lambda函数半年后连你自己都忘了当初为什么设x.max()-x.min()。3.3 滚动窗口计算时间序列聚合的“三重陷阱”原始示例的rolling(window3).mean()看似简单但在银行时序分析中有三个必踩的坑陷阱一索引顺序未重置df_ts.groupby(category)[daily_revenue].rolling(window3).mean()返回的是Series其索引是MultiIndexcategory, date。直接赋值给新列会报错。正确做法是reset_index(level0, dropTrue)但要注意dropTrue会丢弃category索引导致结果错乱。安全写法# 正确保留原始索引结构 rolling_series df_ts.groupby(category)[daily_revenue].rolling(window3).mean() # 获取与原df相同长度的array df_ts[rolling_avg] rolling_series.droplevel(0).values # droplevel(0)移除category层陷阱二窗口内NaN传播若某客户在第1、2天无交易NaN第3天有交易rolling(3).mean()结果是NaN因NaN参与计算。但业务上我们希望“只要有1笔有效交易就计算”。解决方案用min_periods1参数。# 允许最少1个非空值即计算 df_ts[rolling_avg_safe] df_ts.groupby(category)[daily_revenue].rolling( window3, min_periods1 ).mean().droplevel(0).values陷阱三业务窗口 vs 技术窗口技术上window3是3个连续日期但业务上“7天滚动”应指自然周周一到周日。若数据有缺失如周末无交易技术窗口会漏掉。正确做法用ondate参数指定时间列并用freqD对齐# 确保按日历天数滚动而非按行数 df_ts_sorted df_ts.sort_values(date) df_ts_sorted[rolling_7day_calendar] df_ts_sorted.groupby(category).apply( lambda g: g.set_index(date)[daily_revenue].rolling(7D).mean() ).droplevel(0).values7D表示7个日历日自动处理缺失日期。这才是真正的业务滚动。3.4 扩展窗口计算累积指标的“起点哲学”expanding().sum()看似简单但关键在起点定义。原始示例从数据第一行开始累积但业务上累积销售额应从“客户开户日”开始而非数据表最早日期。若客户A在2024-01-01开户但数据从2023-12-01开始前30天累积值毫无意义。生产级写法需绑定业务起点# 假设df有first_txn_date列客户首笔交易日 def cumulative_from_start(group): # 按日期排序 group group.sort_values(date) # 找到首笔交易日后的所有记录 start_date group[first_txn_date].iloc[0] mask group[date] start_date # 对mask内数据做累积 cumsum group.loc[mask, daily_revenue].cumsum() # 补全非起始期为NaN result pd.Series(np.nan, indexgroup.index) result.loc[mask] cumsum.values return result df_ts[cumulative_from_opening] df_ts.groupby(customer_id).apply(cumulative_from_start)这个函数体现了“业务起点优先”原则。没有这个逻辑YTDYear-to-Date报表在客户生命周期早期会严重失真。3.5 多级分组与unstack从“数据透视”到“业务矩阵”的质变groupby([region,product]).mean().unstack()生成的矩阵是销售管理的黄金视图。但原始示例的unstack()默认展开最内层索引product而业务中常需展开外层region。pandas允许指定level参数# 展开region层product作行region作列 result_region_as_col df_sales.groupby([product,region])[revenue].mean().unstack(levelregion) # 输出 # region North South # product # Gadget 12000.0 13750.0 # Widget 15500.0 18000.0更关键的是fill_value参数。原始示例用unstack(fill_value0)但0在财务语境中是“零收入”而NaN才是“无数据”。填0会导致1求平均时把0计入分母2图表显示虚假的“零收入区域”。正确做法是fill_valuenp.nan并在下游用fillna(0)按需填充。此外unstack后常需重排序列。业务要求“North, East, South, West”顺序但pandas按字母序排。安全重排desired_order [North, East, South, West] # 只保留存在的列并按序排列 result result.reindex(columns[c for c in desired_order if c in result.columns])3.6 终极实战客户交易分析流水线的七步构建原始示例的end-to-end代码是教学范本但生产环境需强化三点错误处理、内存控制、结果验证。以下是我在某股份制银行落地的精简版def build_customer_analytics_pipeline(df_raw): 客户交易分析主流程生产级 param df_raw: 原始交易表含date, customer_id, category, amount, fee return: dict of DataFrames, key为分析名称 # 步骤1数据清洗生产必备 df df_raw.copy() df df.dropna(subset[date, customer_id, amount]) # 关键字段不能为空 df[date] pd.to_datetime(df[date]) # 强制转日期 df df[df[amount] 0] # 排除退款/负交易 # 步骤2多维聚合核心 multi_agg df.groupby([customer_id, category]).agg({ amount: [mean, median, count, lambda x: x.quantile(0.9) - x.quantile(0.1)], # 80%分位距 fee: [sum, lambda x: (x / df.loc[x.index, amount]).mean() * 100] # 平均费率% }) multi_agg flatten_columns(multi_agg) # 展平列名 # 步骤3滚动计算按客户时间 df_sorted df.sort_values([customer_id, date]) df_sorted[rolling_7day_amt] df_sorted.groupby(customer_id)[amount].rolling( window7, min_periods1 ).mean().droplevel(0).values # 步骤4累积计算按客户 df_sorted[cumulative_amt] df_sorted.groupby(customer_id)[amount].cumsum() # 步骤5交叉分析客户vs产品 crosstab df.groupby([customer_id, category])[amount].mean().unstack(fill_valuenp.nan) # 步骤6执行摘要面向高管 summary df.groupby(customer_id).agg({ amount: [sum, mean, count], fee: sum }) summary flatten_columns(summary) summary.columns [total_spend, avg_transaction, txn_count, total_fees] summary[fee_rate_pct] (summary[total_fees] / summary[total_spend] * 100).round(2) # 步骤7风险分层业务规则驱动 def risk_segment(series): high_val_cnt (series 300).sum() return pd.Series({ high_value_ratio: round(high_val_cnt / len(series) * 100, 1), high_value_count: high_val_cnt, regular_avg: series[series 300].mean() }) risk_df df.groupby(customer_id)[amount].apply(risk_segment) return { multi_dimensional: multi_agg, rolling_window: df_sorted[[customer_id, date, amount, rolling_7day_amt]], cumulative: df_sorted[[customer_id, date, amount, cumulative_amt]], crosstab: crosstab, executive_summary: summary, risk_segmentation: risk_df } # 调用 results build_customer_analytics_pipeline(df_transactions) print(Pipeline completed. Results keys:, list(results.keys()))这个函数封装了所有生产要素数据校验、错误容忍、列名标准化、内存友好的.values提取。它不是一个脚本而是一个可复用、可测试、可监控的分析组件。4. 实操过程详解手把手构建银行级多维聚合流水线4.1 环境准备与数据模拟还原真实银行数据特征生产环境从不给你干净数据。我们先构建一个逼近真实的模拟数据集包含银行交易典型特征时间不连续周末无交易、客户分层VIP/普通、产品类别零售/餐饮/旅行、手续费阶梯按金额分档、以及少量异常值。import pandas as pd import numpy as np from datetime import datetime, timedelta def generate_bank_transaction_data(n_customers500, n_days180, seed42): 生成高保真银行交易数据 特征1) 工作日交易密集周末稀疏2) VIP客户交易频次高、金额大3) 手续费按金额阶梯计费 np.random.seed(seed) # 客户分层VIP占10%交易频次是普通客户2倍平均金额1.5倍 customers [fC{str(i).zfill(3)} for i in range(1, n_customers1)] vip_mask np.random.choice([True, False], sizen_customers, p[0.1, 0.9]) customer_profiles pd.DataFrame({ customer_id: customers, is_vip: vip_mask, base_freq: np.where(vip_mask, 2.0, 1.0), # 基础交易频次倍数 base_amt_mult: np.where(vip_mask, 1.5, 1.0) # 金额倍数 }) # 时间范围过去180天但只生成工作日周一至周五 start_date datetime(2023, 1, 1) dates pd.date_range(startstart_date, periodsn_days, freqD) # 过滤出工作日 workdays dates[dates.weekday 5] # 产品类别及金额分布 categories [Groceries, Dining, Travel, Retail, Utilities] # 各类别基础金额中位数VIP客户在此基础上乘以base_amt_mult base_medians {Groceries: 85, Dining: 120, Travel: 450, Retail: 220, Utilities: 60} transactions [] for date in workdays: # 每日交易总数工作日多月末更多发薪日效应 daily_base_count 1000 if date.day in [25, 26, 27, 28]: # 月末高峰 daily_base_count * 1.8 if date.weekday() 0: # 周一稍高 daily_base_count * 1.2 # 随机选择客户VIP客户被选中概率更高 weights customer_profiles[base_freq].values selected_customers np.random.choice( customer_profiles[customer_id], sizeint(daily_base_count), pweights/weights.sum() ) for cust_id in selected_customers: profile customer_profiles[customer_profiles[customer_id]cust_id].iloc[0] # 随机选类别 category np.random.choice(categories) # 生成金额对数正态分布模拟真实偏态 median_amt base_medians[category] * profile[base_amt_mult] # lognormal参数muln(median)sigma控制离散度 mu np.log(median_amt) sigma 0.6 if category in [Travel, Dining] else 0.4 # 高波动类目sigma更大 amount np.random.lognormal(mu, sigma) # 手续费阶梯计费0-100: 1.5%, 100-500: 2.0%, 500: 2.5% if amount 100: fee_rate 0.015 elif amount 500: fee_rate 0.020 else: fee_rate 0.025 fee round(amount * fee_rate, 2) # 加入少量异常值0.5%概率 if np.random.random() 0.005: amount * np.random.choice([5, 10]) # 突发大额交易 transactions.append({ date: date, customer_id: cust_id, category: category, amount: round(amount, 2), fee: fee }) df pd.DataFrame(transactions) # 添加少量缺失值模拟数据采集故障 missing_idx np.random.choice(df.index, sizeint(len(df)*0.001), replaceFalse) df.loc[missing_idx, fee] np.nan return df # 生成10万行数据约3个月 df_bank generate_bank_transaction_data(n_customers500, n_days180, seed42) print(fGenerated {len(df_bank)} transactions) print(df_bank.head()) print(\nData quality check:) print(df_bank.isnull().sum())这个生成器的关键价值在于它复现了真实数据的“不完美性”。有缺失值、有异常值、有业务规则VIP分层、手续费阶梯、有时序模式工作日/月末。用它测试你的聚合代码比用原始示例的10行数据可靠100倍。4.2 多维聚合实战从“客户-产品”矩阵到动态仪表盘我们以“客户在各产品类别的平均交易额”为核心指标构建一个可直接喂给BI工具的宽表。# 步骤1基础多维聚合 print(Step 1: Basic multi-dimensional aggregation...) agg_basic df_bank.groupby([customer_id, category])[amount].agg([mean, count, std]).round(2) agg_basic flatten_columns(agg_basic) print(fBasic aggregation shape: {agg_basic.shape}) # 步骤2添加业务衍生指标 print(Step 2: Adding business metrics...) # 计算每个客户在各类别的交易占比相对于该客户总交易 customer_total df_bank.groupby(customer_id)[amount].sum() # 先按客户类别求和再除以客户总和 category_sum df_bank.groupby([customer_id, category])[amount].sum() # 用transform广播客户总和 agg_basic[pct_of_customer_total] ( category_sum / customer_total.loc[category_sum.index.get_level_values(0)] ).round(4) # 步骤3交叉表生成客户vs产品 print(Step 3: Building cross-tab matrix...) crosstab_mean df_bank.groupby([customer_id, category])[amount].mean().unstack(fill_valuenp.nan) crosstab_count df_bank.groupby([customer_id, category])[amount].count().unstack(fill_value0) # 步骤4添加行列汇总BI仪表盘常用 print(Step 4: Adding row/column totals...) # 列汇总各产品类别的全局均值 crosstab_mean.loc[ALL_CUSTOMERS] crosstab_mean.mean() crosstab_count.loc[ALL_CUSTOMERS] crosstab_count.sum() # 行汇总各客户的全局均值 crosstab_mean[ALL_CATEGORIES] crosstab_mean.mean(axis1) crosstab_count[ALL_CATEGORIES] crosstab_count.sum(axis1) # 步骤5保存为BI就绪格式 print(Step 5: Exporting to BI-ready format...) # 合并所有结果到一个dict bi_ready_data { customer_category_metrics: agg_basic, crosstab_mean: crosstab_mean, crosstab_count: crosstab_count, customer_summary: df_bank.groupby(customer_id).agg({ amount: [sum, mean, count], fee: sum }).round(2) } # 验证检查是否有客户在所有类别都无交易应为NaN print(\nValidation: Customers with all NaN in crosstab_mean) all_nan_customers crosstab_mean[crosstab_mean.isnull().all(axis1)].index.tolist() print(fFound {len(all_nan_customers)} such customers: {all_nan_customers[:3]}) # 导出示例实际中导出CSV/数据库 crosstab_mean.to_csv(customer_product_matrix.csv) print(Exported customer_product_matrix.csv)这个流程展示了生产级聚合的完整链条基础聚合 → 业务衍生 → 矩阵构建 → 汇总增强 → 质量验证。特别是pct_of_customer_total的计算用到了pandas的高级索引技巧category_sum.index.get_level_values(0)提取多级索引的第一层customer_id再用loc定位客户总和。这种写法比merge更高效内存占用更低。4.3 滚动与扩展窗口构建实时风险监控信号银行风控需要毫秒级响应但我们的聚合是批处理。如何用滚动窗口模拟近实时信号# 构建时间序列数据按日聚合 print(Building daily time-series for rolling analysis...) daily_data df_bank.groupby([date, customer_id]).agg({ amount: sum, fee: sum, category: count # 交易笔数 }).rename(columns{category: txn_count}).reset_index() # 步骤1客户级滚动均值7天 print(Calculating 7-day rolling average per customer...) # 按客户分组对日期排序再滚动 daily_sorted daily_data.sort_values([customer_id, date]) daily_sorted[rolling_7day_amt] daily_sorted.groupby(customer_id)[amount].rolling( window7, min_periods1 ).mean().droplevel(0).values # 步骤2添加业务信号是否突增当前值 过去7天均值*1.5 daily_sorted[is_surge] ( daily_sorted[amount] daily_sorted[rolling_7day_amt] * 1.5 ) (daily_sorted[rolling_7day_amt].notna()) # 步骤3扩展窗口计算客户生命周期价值CLV print(Calculating cumulative spend (CLV)...) daily_sorted[cumulative_spend] daily_sorted.groupby(customer_id)[amount].cumsum() # 步骤4构建风险信号矩阵客户x日期 print(Building risk signal matrix...) # pivot成宽表行客户列日期值是否突增 risk_matrix daily_sorted.pivot( indexcustomer_id, columnsdate, valuesis_surge ).fillna(False) # 步骤5计算每个客户的“突增频率”过去30天内突增天数/总天数 last_30_days daily_sorted[date].max() - pd.Timedelta(days30) recent_data daily_sorted[daily_sorted[date] last_30_days] surge_freq recent_data.groupby(customer_id)[is_surge].mean().round(3) print(f\nRisk signal summary:) print(fTotal customers: {len(surge_freq)}) print(fHigh-risk customers (surge_freq 0.2): {sum(surge_freq 0.2)}) print(fTop 5 surge frequencies:\n{surge_freq.nlargest(5)}) # 保存信号 surge_freq.to_csv(customer_surge_frequency.csv) print(Exported customer_surge_frequency.csv)这里的关键创新是将滚动窗口结果转化为布尔信号is_surge再通过pivot构建信号矩阵。这使得我们可以用简单的sum(axis1)计算每个客户的总突增天数或用corrwith()计算客户间突增行为的相关性。这种“信号化”思维是从业务指标到AI特征工程的桥梁。4.4 多级分组深度应用穿透式分析与根因定位当发现“南方地区餐饮类