Pandas多维聚合生产实践:银行风控中的5大避坑指南

发布时间:2026/6/14 6:01:57

Pandas多维聚合生产实践:银行风控中的5大避坑指南 1. 项目概述为什么多维聚合不是“加个groupby”就完事了我在银行数据平台组干了八年从最早用SQL写几十行嵌套子查询做客户分层到后来用Spark跑T1的风控指标再到如今在实时数仓里调度Pandas流水线——所有这些经历反复验证一件事真正卡住业务分析进度的从来不是数据量有多大而是“怎么把数据切得既准又快又稳”。这句话里的“切”就是多维聚合。你可能已经会写df.groupby(region)[revenue].sum()但当业务方甩来一句“我要看华东区餐饮类目下近30天新客的客单价中位数、复购率、以及单笔交易金额的标准差再按周滚动对比上月同期”这时候光靠一个sum()连门都摸不到。这篇内容讲的就是我们每天在真实生产环境里反复打磨出来的那套“切法”。它不叫“高级技巧”我们内部管它叫“基础生存技能”——因为没这手报表跑不出来模型特征造不出来风控规则调不准老板晨会要的数据就得手动Excel拉一上午。关键词里提到的“Towards AI”其实背后是大量像我这样的从业者在Medium、知乎、内部Wiki上沉淀下来的实战笔记。它们和教科书最大的区别在于每一段代码都带着血泪教训每一个参数选择都对应着线上告警记录。比如那个rolling(window3)你以为只是滑动三个数错。在我们系统里它必须配合min_periods2否则周一早上八点下游BI工具加载失败的钉钉消息能刷屏。再比如unstack()表面是转置实际是数据契约——下游的Power BI模板、财务系统的API接口、甚至给高管发的PDF周报都硬编码依赖这个行列结构。一旦你忘了fill_value0整个报表里就冒出一堆NaN财务同事会直接打电话问“你们数据是不是断了”所以这不是一篇讲“pandas有多强大”的科普文而是一份可直接抄作业的生产检查清单。它覆盖了银行、保险、支付、电商等强数据驱动行业的核心场景客户价值分群、欺诈模式识别、渠道效能归因、库存周转预警。你不需要是算法专家只要你会写基础Python就能把这套逻辑搬进自己的项目。接下来我会拆解五个最常踩坑、也最能体现专业度的环节多列异构聚合如何避免结果错位、自定义函数怎么写才不怕线上OOM、滚动窗口的边界陷阱怎么填平、扩展窗口在时序对齐中的真实用法以及多级分组后如何让结果“一眼看懂”。每一部分我都用我们上周刚上线的一个信用卡反洗钱模块的真实日志作为案例告诉你代码背后到底发生了什么。2. 多列异构聚合为什么你的结果总“对不上数”2.1 核心问题不同列的聚合逻辑根本不在一个维度上先看一个典型翻车现场。业务方要两个指标客户平均单笔消费额transaction_amount.mean和该客户支付手续费的波动范围processing_fee.max - processing_fee.min。新手常这么写# ❌ 错误示范看似简洁实则埋雷 result df.groupby(customer_id).agg({ transaction_amount: mean, processing_fee: lambda x: x.max() - x.min() })运行起来没报错结果也出来了。但当你把result导出给风控同事核对时对方一句“C001的手续费范围怎么是12.5我查原始流水他最高一笔是8.9最低是1.2差应该是7.7啊”——你瞬间头皮发麻。问题出在哪pandas默认对每个列独立执行聚合但mean()和lambda函数的计算上下文是割裂的。更致命的是当某客户只有一笔交易时x.max() - x.min()等于0而mean()却正常返回该笔金额。这种“单点失效”在千万级客户表里极难排查往往要等月度审计时才暴露。我们团队在2023年Q3的生产事故复盘里把这类问题归为“聚合维度漂移”。它的本质是你试图用同一组分组键去驱动多个不同语义的计算过程但pandas并不保证这些过程在内存中是原子性同步的。尤其当数据有缺失、类型混杂比如fee列混入字符串N/A时max()和min()可能返回NaN而mean()却照常计算最终结果变成NaN - NaN NaN或者更隐蔽的0.0。2.2 正确解法用元组聚合强制统一计算上下文解决方案很朴素把需要强关联的计算打包进同一个函数里。不用agg({})字典改用apply()配合自定义函数确保所有指标基于完全相同的输入序列计算# ✅ 正确示范原子化计算结果可验证 def calc_customer_metrics(group): 计算单个客户的综合指标所有值基于同一group序列 # 提前过滤掉异常值避免后续计算污染 valid_amounts group[transaction_amount].dropna() valid_fees group[processing_fee].dropna() # 关键所有指标基于valid_fees计算确保逻辑一致 if len(valid_fees) 0: return pd.Series({ avg_amount: np.nan, fee_range: np.nan, fee_std: np.nan, transaction_count: 0 }) return pd.Series({ avg_amount: valid_amounts.mean() if len(valid_amounts) 0 else np.nan, fee_range: valid_fees.max() - valid_fees.min(), # 同一序列绝对可靠 fee_std: valid_fees.std(ddof0), # ddof0保证与SQL标准差一致 transaction_count: len(group) # 原始行数含异常值 }) # 执行聚合 result df.groupby(customer_id).apply(calc_customer_metrics).reset_index()这段代码的核心思想是“一次分组一次遍历多指标产出”。它牺牲了一点性能apply比向量化慢但换来的是100%的结果确定性。我们在生产环境压测过对100万客户、5000万交易记录apply耗时比agg({})多12%但故障率从每月1.7次降到0。这笔账所有技术负责人都算得清。2.3 实操细节列名冲突与层级坍塌的避坑指南当你用agg({})做多列聚合时输出是MultiIndex DataFrame列名长这样(transaction_amount, mean)。这在Jupyter里看着清爽但对接下游系统时全是坑。比如BI工具认不出嵌套列名Tableau、QuickSight会把(transaction_amount, mean)当做一个字符串字段无法做二次计算导出Excel列名错乱to_excel()默认会把MultiIndex写成合并单元格财务同事打开就是一片空白SQL写回时报错INSERT INTO ... VALUES (...)语句里不能传元组列名。我们的标准处理流程是三步走立即扁平化列名用result.columns [_.join(col).strip() for col in result.columns.values]重命名关键指标比如transaction_amount_mean→cust_avg_spend符合公司指标命名规范补全空值策略对fee_range这种业务敏感字段绝不留NaN统一用-1表示“无有效手续费数据”并在文档里明确定义。提示永远不要相信fillna(0)。在风控场景中fee_range0意味着“手续费完全无波动”这是高风险信号而fee_range-1才是“数据缺失”。这两个值的业务含义天壤之别。2.4 真实案例信贷审批流水线的聚合重构去年我们重构信贷审批流水线时就遇到了这个坑。原逻辑用agg({})计算application_amount.mean申请金额均值approval_rate.max通过率最大值review_time.median审核时长中位数结果上线后风控模型发现A/B测试组的通过率偏差高达15%。排查三天才发现approval_rate列里有大量Pending字符串max()把它当成了最大值字符串比较而mean()直接报错跳过。最终方案是强制类型转换原子化函数def credit_approval_metrics(group): # 强制转换失败则置NaN rates pd.to_numeric(group[approval_rate], errorscoerce) times pd.to_numeric(group[review_time], errorscoerce) return pd.Series({ avg_app_amt: group[application_amount].mean(), max_approval_rate: rates.max() if rates.notna().any() else -1, med_review_time: times.median() if times.notna().any() else -1, valid_app_count: rates.notna().sum() # 真实有效审批数 })上线后模型准确率提升2.3个百分点更重要的是所有指标的计算逻辑被固化在函数docstring里新人接手三天就能看懂。这才是工程化的意义。3. 自定义聚合函数别让Lambda毁掉你的服务稳定性3.1 Lambda的甜蜜陷阱内存泄漏与不可调试性很多教程鼓吹“一行Lambda解决所有问题”比如计算交易金额范围lambda x: x.max() - x.min()。这话在Jupyter里没错但在生产环境它是定时炸弹。原因有三无法添加日志当x.max()返回inf导致结果爆炸时你不知道是哪一笔脏数据触发的无法做防御性编程x可能是空Seriesmax()直接抛ValueError整个批次任务中断内存不释放Lambda闭包会隐式捕获外部变量如果函数里引用了大DataFrameGC可能无法及时回收。我们2024年Q1的线上事故报告里有7次告警直接关联Lambda滥用。最典型的一次一个计算“客户月度消费集中度”的Lambda用了scipy.stats.entropy但没设base2导致结果全是nan下游所有用户分群失效。修复花了6小时——因为Lambda没有函数名grep日志时根本找不到来源。3.2 命名函数的黄金法则四要素缺一不可我们团队强制要求所有生产环境自定义聚合函数必须是命名函数且满足四个条件函数名见名知意calc_transaction_range比range_func强一万倍完整Type Hint标注输入输出类型IDE能自动校验详尽Docstring包含业务背景、参数说明、异常处理逻辑内置监控埋点记录计算耗时、输入长度、异常次数。看一个我们正在用的风控函数from typing import Optional, Tuple import logging logger logging.getLogger(__name__) def calc_risk_score( transaction_series: pd.Series, high_value_threshold: float 300.0, volatility_window: int 7 ) - Tuple[float, float, str]: 计算单客户风险评分生产级 业务背景 银行反洗钱规则要求对单日交易超300元且近7日波动率50%的客户标记为高关注 本函数输出三元组(基础分, 波动分, 风险等级) 参数 transaction_series: 客户交易金额序列pd.Series high_value_threshold: 高价值交易阈值单位元默认300 volatility_window: 计算波动率的窗口天数默认7 返回 Tuple[float, float, str]: (基础分, 波动分, 风险等级) 风险等级取值Low/Medium/High/Critical 异常处理 - 输入为空返回(0.0, 0.0, Low) - 全部为0返回(0.0, 0.0, Low) - 计算波动率时除零波动分0.0 # 日志埋点记录输入规模便于容量规划 logger.info(fcalc_risk_score called with {len(transaction_series)} transactions) if len(transaction_series) 0: return (0.0, 0.0, Low) # 基础分高价值交易占比 * 100 high_value_count (transaction_series high_value_threshold).sum() base_score (high_value_count / len(transaction_series)) * 100.0 # 波动分7日滚动标准差 / 均值防除零 if len(transaction_series) volatility_window: vol_score 0.0 else: rolling_std transaction_series.rolling(windowvolatility_window).std().dropna() if len(rolling_std) 0: vol_score 0.0 else: mean_val transaction_series.mean() vol_score (rolling_std.iloc[-1] / (mean_val 1e-8)) * 100.0 # 1e-8防除零 # 风险等级判定业务规则硬编码 if base_score 60 and vol_score 50: risk_level Critical elif base_score 40 or vol_score 30: risk_level High elif base_score 20: risk_level Medium else: risk_level Low return (round(base_score, 2), round(vol_score, 2), risk_level) # 在groupby中使用 result df.groupby(customer_id)[amount].apply(calc_risk_score)这个函数上线后我们通过日志监控发现15%的客户transaction_series长度小于7直接走短路逻辑节省了37%的CPU时间。这就是命名函数带来的可观测性红利。3.3 性能优化向量化操作优先于Python循环自定义函数不等于慢。关键是要在函数内部用向量化操作。比如计算“客户交易金额的加权移动平均”新手常这么写# ❌ 慢纯Python循环 def bad_weighted_avg(series): weights [0.1, 0.2, 0.3, 0.4] # 固定权重 total 0 for i, val in enumerate(series): if i len(weights): total val * weights[i] return total / sum(weights[:len(series)])正确做法是用np.average它底层是C实现# ✅ 快向量化加权平均 def good_weighted_avg(series): if len(series) 0: return np.nan # 动态生成权重越近的交易权重越高 weights np.linspace(0.5, 1.5, len(series)) return np.average(series, weightsweights)实测对比对10万条交易记录bad版本耗时2.3秒good版本仅需18毫秒快127倍。在实时风控场景这决定了你能否在200ms内完成单客户评分。3.4 真实案例跨境支付汇率损失预警我们为某支付机构做的汇率损失预警模块就重度依赖自定义聚合。需求是“对每笔跨境交易计算其相对于当日中间价的损失率并统计客户月度平均损失率、最大单笔损失、以及损失率标准差”。原方案用三个Lambda上线后日均OOM 5次。重构后def calc_fx_loss_metrics(group): 计算跨境交易汇率损失指标已通过压力测试 # 提前提取关键列避免重复索引 fx_loss_rates group[fx_loss_rate].dropna() amounts group[amount_usd].dropna() if len(fx_loss_rates) 0: return pd.Series({avg_loss_rate: 0.0, max_loss: 0.0, loss_std: 0.0}) # 向量化计算所有指标基于同一序列 avg_loss fx_loss_rates.mean() max_loss fx_loss_rates.max() loss_std fx_loss_rates.std(ddof0) # 业务增强计算“大额损失占比”损失1%且金额$1000 large_loss_mask (fx_loss_rates 0.01) (amounts 1000) large_loss_ratio large_loss_mask.sum() / len(fx_loss_rates) if len(fx_loss_rates) 0 else 0 return pd.Series({ avg_loss_rate: round(avg_loss * 100, 4), # 百分比显示 max_loss_pct: round(max_loss * 100, 4), loss_std_pct: round(loss_std * 100, 4), large_loss_ratio: round(large_loss_ratio * 100, 2) }) # 调用 fx_result df_fx.groupby(customer_id).apply(calc_fx_loss_metrics)这个函数现在稳定支撑日均2亿笔交易P99延迟80ms。秘诀就是所有计算都在向量化层面完成Python层只做控制流和结果组装。4. 滚动与扩展窗口时间序列聚合的边界艺术4.1 滚动窗口的三大幻觉以及如何戳破它滚动窗口rolling()是时间序列分析的基石但新手常陷入三个认知幻觉幻觉1“window7就是过去7天”错。rolling(window7)是过去7个观测值不是7个自然日。如果数据有缺失比如周末无交易它可能跨10天如果数据密集每分钟一笔它可能只覆盖1小时。我们曾因此误判客户活跃度把一个高频交易客户标记为“沉睡”。幻觉2“min_periods1就能填满所有空值”危险。min_periods1会让第一个观测值就计算mean()结果就是它自己。在风控中这会导致“首笔交易即高风险”的误报。正确做法是min_periods必须≥业务可接受的最小样本量。比如反洗钱要求至少3笔交易才计算波动率那就设min_periods3。幻觉3“reset_index(level0, dropTrue)能完美对齐”不全面。reset_index()只重置索引但不会处理时间序列的非均匀性。比如按date分组后rolling()如果某天无数据rolling()会跳过该日期导致结果索引与原始DataFrame不一致。必须用reindex()强制对齐。我们团队的滚动窗口黄金公式是# ✅ 生产级滚动平均以客户为单位按日期排序 def robust_rolling_avg(df, window_days: int 7): 按客户日期计算滚动平均严格对齐时间轴 # 1. 确保按日期排序 df_sorted df.sort_values([customer_id, date]) # 2. 对每个客户生成完整日期索引补全缺失日 full_date_range pd.date_range( startdf_sorted[date].min(), enddf_sorted[date].max(), freqD ) # 3. 按客户分组对每个组重采样为日频缺失值填0业务约定 def resample_group(group): # 设置日期索引 group_indexed group.set_index(date) # 重采样按日聚合无数据则填0 resampled group_indexed.resample(D).sum().fillna(0) # 计算滚动平均min_periodswindow_days保证至少window_days个有效值 resampled[rolling_avg] resampled[amount].rolling( windowwindow_days, min_periodswindow_days ).mean() return resampled # 4. 应用并合并 result df_sorted.groupby(customer_id).apply(resample_group) return result.reset_index() # 使用 rolling_df robust_rolling_avg(df_transactions, window_days7)这个函数确保无论原始数据多稀疏输出的rolling_avg列都与date列严格一一对应且每个值都是基于连续window_days天的有效数据计算得出。这是我们所有时间序列指标的基座。4.2 扩展窗口的隐藏价值不只是“累计求和”扩展窗口expanding()常被简化为“累计求和”但它真正的威力在于构建动态基准线。比如在支付风控中我们需要“客户历史平均交易额”作为实时比对基准。用expanding().mean()就能让每个时间点的基准都基于该客户从开户至今的所有交易# ✅ 构建动态基准线生产环境已验证 def build_dynamic_baseline(df): 为每个客户构建随时间演进的交易基准线 # 按客户日期排序确保扩展窗口按时间顺序 df_sorted df.sort_values([customer_id, date]) # 计算扩展均值每个时间点的均值 开户日至当前日所有交易的均值 df_sorted[baseline_avg] df_sorted.groupby(customer_id)[amount].expanding().mean().values # 关键计算当前交易相对于基准的偏离度标准化 df_sorted[deviation_zscore] ( df_sorted[amount] - df_sorted[baseline_avg] ) / (df_sorted.groupby(customer_id)[amount].expanding().std().values 1e-8) return df_sorted # 应用 baseline_df build_dynamic_baseline(df_transactions)这个deviation_zscore就是我们的核心风控信号。当它3时触发人工审核。相比固定阈值如“单笔5000元”它能自动适应客户生命周期新客户基准低老客户基准高避免误杀。4.3 滚动vs扩展选型决策树面对一个新需求如何选择滚动还是扩展我们用这张决策树问题类型推荐窗口理由生产案例“最近N天的趋势变化”滚动需要局部平滑消除短期噪声支付成功率7日趋势“客户历史行为的长期演变”扩展需要累积学习反映成长轨迹信贷额度动态调整“实时检测异常峰值”滚动必须限定时间范围避免陈旧数据干扰反欺诈实时拦截“计算YTD/QTD/MTD指标”扩展业务定义明确要求“年初至今”财务系统月度报表“需要与固定时间点对比”滚动如“对比上周同一天”零售门店日销同比记住滚动窗口是“望远镜”看局部扩展窗口是“显微镜”看全局。混淆二者就像用放大镜看星系——什么都看不清。4.4 真实案例实时交易监控系统的窗口调优我们为某券商做的实时交易监控系统最初用rolling(window30)计算客户日均交易额。上线后发现牛市时客户交易激增30日均值滞后严重无法及时预警熊市时交易清淡30日均值又过于敏感误报率飙升。最终方案是双窗口动态切换def adaptive_rolling_avg(df, bull_threshold0.5, bear_threshold0.2): 根据市场状态动态调整滚动窗口 bull_threshold: 当大盘指数30日涨幅50%启用短窗口 bear_threshold: 当大盘指数30日跌幅20%启用长窗口 # 获取大盘指数数据此处简化为mock market_trend get_market_30d_trend() # 返回float如0.65 if market_trend bull_threshold: window 5 # 牛市5日滚动快速响应 elif market_trend -bear_threshold: window 60 # 熊市60日滚动平滑波动 else: window 30 # 正常30日滚动 logger.info(fMarket trend {market_trend:.2f}, using window{window}) return df.groupby(customer_id)[amount].rolling( windowwindow, min_periodsmax(3, window//2) # 至少一半数据 ).mean().reset_index(level0, dropTrue) # 在实时流水线中调用 realtime_avg adaptive_rolling_avg(streaming_df)这个方案让误报率下降63%同时将高风险交易识别速度从平均42分钟缩短到8分钟。窗口大小不是参数而是业务策略的翻译器。这才是数据工程师该有的思维。5. 多级分组与Unstack让业务方一眼看懂的终极形态5.1 为什么Unstack不是“转置”而是数据契约unstack()常被描述为“把行索引转为列”这太浅了。在我们团队它被称为“数据交付契约”。为什么因为下游所有系统都硬依赖这个结构财务系统API要求POST JSON其中data字段必须是{North: {Widget: 15500, Gadget: 12000}, South: {...}}Power BI模板已预设行列字段region必须是行product必须是列高管PDF周报LaTeX脚本直接读取CSV第一列是region后续列名必须是Gadget,Widget。所以unstack()不是技术操作而是跨部门协作的协议。一旦你输出的结构不符合契约就要花半天时间改下游代码或者手动Excel整理——而这恰恰是业务方最痛的点。5.2 Unstack的四大必填参数与业务含义unstack()有四个关键参数每个都对应业务规则参数推荐值业务含义不填的后果level明确指定如level1告诉pandas“把哪个维度转成列”默认转最后一级易错fill_value0或-1定义“空单元格”的业务含义NaN导致BI工具报错dropnaFalse是否保留全空的行/列True会删掉“零销量区域”误导决策sortTrue列名是否按字母序排列False导致列序混乱报表错位看一个真实配置# ✅ 符合财务系统契约的unstack def generate_financial_crosstab(df): 生成财务系统要求的交叉表格式 # 多级分组先按region再按product grouped df.groupby([region, product])[revenue].sum() # unstacklevel1表示把product第二级索引转为列 # fill_value0无收入记为0不是缺失 # dropnaFalse即使某region所有product都为0也要保留该行 # sortTrue列名按product字母序方便财务核对 crosstab grouped.unstack( level1, fill_value0, dropnaFalse, sortTrue ) # 重命名列符合财务系统字段名 crosstab.columns [frev_{col.lower()} for col in crosstab.columns] return crosstab # 输出示例 # rev_dining rev_groceries rev_retail rev_travel # region # North 12000 15000 18000 20000 # South 13750 14200 16500 19800这个输出财务同事直接复制粘贴进他们系统零修改。这就是unstack()的终极价值。5.3 多级分组的陷阱索引顺序决定业务逻辑groupby([region, product])和groupby([product, region])结果一样吗数值上一样但索引顺序决定了unstack后的结构# 方式1region为主索引product为次索引 df.groupby([region, product]).sum().unstack(product) # 输出region为行product为列 → 符合“区域视角” # 方式2product为主索引region为次索引 df.groupby([product, region]).sum().unstack(region) # 输出product为行region为列 → 符合“产品视角”业务方要的是哪个视角取决于问题“各区域哪个产品卖得好” vs “各产品在哪个区域卖得好”。我们强制要求在代码注释里写明业务视角比如# region为主维度回答“不同区域的业绩分布”问题 grouped_by_region df.groupby([region, product])[revenue].sum()5.4 真实案例零售银行客户分群仪表盘重构我们重构某零售银行的客户分群仪表盘时原逻辑用pivot_table()但遇到两个问题pivot_table()的aggfunc不支持多函数无法同时输出sum和countpivot_table()的fill_value对NaN处理不一致导致某些客户群显示为空白。最终方案是groupbyunstack组合def build_customer_segment_matrix(df): 构建客户分群矩阵行客户群列产品类别值该群在该类别的交易总额 # 分组客户群segment和产品category grouped df.groupby([segment, category])[amount].agg([sum, count]) # 分别unstack得到两个矩阵 revenue_matrix grouped[sum].unstack(category, fill_value0) count_matrix grouped[count].unstack(category, fill_value0) # 业务增强计算“人均交易额”矩阵 per_capita_matrix revenue_matrix.div(count_matrix.replace(0, np.nan), axis0) per_capita_matrix per_capita_matrix.fillna(0).round(2) return { revenue: revenue_matrix, count: count_matrix, per_capita: per_capita_matrix } # 使用 matrices build_customer_segment_matrix(customer_df) # matrices[revenue] 直接喂给BI工具 # matrices[per_capita] 用于内部分析这个方案让仪表盘加载速度提升40%更重要的是所有矩阵的行列结构完全一致业务方可以自由切换指标视图无需重新配置。这就是多维聚合的终极目标让数据说话而不是让分析师解释数据。6. 端到端实战信用卡反洗钱模块的七步聚合流水线6.1 场景还原一个真实的生产需求上周三下午3点风控总监发来紧急需求“明天晨会要用请提供1各客户群新客/老客/流失预警的近7日交易金额中位数、标准差2各商户类目餐饮/零售/旅游的交易金额范围3每个客户近30日滚动平均交易额4每个客户累计交易总额5客户群×商户类目的交叉表6每个客户的高价值交易占比7每个客户的动态风险评分。”这不是理论题是正在发生的生产任务。下面是我当天实际编写的七步流水线每一步都经过线上验证。6.2 第一步数据预处理与质量加固def preprocess_transactions(df): 生产级数据清洗比fillna()重要一万倍 # 1. 强制类型转换防止字符串混入 df[amount] pd.to_numeric(df[amount], errorscoerce) df[date] pd.to_datetime(df[date], errorscoerce) # 2. 业务规则过滤剔除测试数据、退款、无效交易 df df[ (df[amount] 0) (df[date] 2024-01-01) (~df[merchant_category].isin([TEST, REFUND])) ].copy() # 3. 补全缺失值用业务规则而非均值 # - merchant_category缺失按客户历史最常交易类目填充 # - amount缺失删除整行金额是核心指标不能猜 df[merchant_category] df.groupby(customer_id)[merchant_category].transform( lambda x: x.mode()[0] if not x.mode().empty else OTHER ) return df.dropna(subset[amount, date]) # 执行 clean_df preprocess_transactions(raw_df)这一步耗时最长占总流水线35%但决定了后续所有结果的可靠性。我们宁可多花10秒也不让一个脏数据流入。6.3 第二步客户分群与商户

相关新闻