当前位置: 首页 > news >正文

pandas多维聚合生产实践:滚动窗口、宽表生成与业务可解释性

1. 项目概述:为什么多维聚合不是“加个groupby”就能搞定的事

我在银行数据平台组干了八年,从最早用SQL写几十行嵌套子查询做客户分层,到后来带团队搭实时风险计算引擎,踩过的坑比写的代码还多。今天聊的这个主题——“多维聚合中的数据操作”,听起来像教科书里的一个章节标题,但实际在生产环境里,它直接决定着风控模型能不能按时上线、月度经营分析报告能不能准时发给CEO、甚至某次大促期间的实时交易监控面板会不会突然卡死。这不是炫技,是每天都在发生的生存问题。

核心关键词就三个:多维聚合、生产级、业务可解释性。注意,这里说的“多维”,不是指简单地groupby(['region', 'product'])——那只是二维;而是指同时处理时间维度(滚动/扩展窗口)、业务维度(客户+产品+渠道+风险等级)、统计维度(均值/中位数/极差/加权平均)这三类逻辑完全不同的“维”,还要让结果能被业务方一眼看懂、能进BI系统、能导出Excel、能喂给下游模型。很多新手一上来就猛敲.agg(),结果跑出来是个MultiIndex Series,列名是('amount', 'mean')这种嵌套元组,财务同事打开Excel直接懵:“这玩意儿怎么 pivot?”——这就是典型的“技术正确,业务失败”。

我见过太多团队把这部分当成“数据清洗收尾工作”,结果在UAT阶段被业务方反复打回:

  • “这个‘平均交易额’为什么和我们手工算的差0.3%?是不是没剔除退款单?”
  • “滚动7天均值,第一天是NaN,你们填0还是前向填充?规则文档在哪?”
  • “客户A在‘旅游’类别的交易中位数是285,但明细里明明有3笔500+的订单,中位数怎么不是500?”

这些问题背后,全是聚合逻辑与业务语义的错位。而这篇内容要解决的,就是如何把pandas里那些看似简单的.rolling().expanding().unstack(),真正变成可审计、可复现、可交付的生产级能力。它不讲理论推导,只讲我在三家银行、两个支付平台、一个保险科技公司落地时,被业务方指着鼻子问出来的实操细节。适合两类人:一是刚接手报表开发的数据工程师,二是想把分析脚本升级成自动化pipeline的分析师。如果你还在用for循环遍历客户算累计值,或者靠Excel手动补滚动窗口的空值——这篇文章能帮你省下至少200小时重复劳动。

2. 多维聚合的整体设计思路:先画清楚“业务问题地图”,再选技术路径

很多人一拿到需求就开写代码,结果写着写着发现:同一个客户在不同时间点的“高价值交易占比”定义不一致,滚动窗口和扩展窗口的边界条件打架,多级groupby后unstack出来的列顺序和BI工具要求的字段顺序对不上……最后推倒重来。根本原因在于,没把业务问题拆解成可计算的数学结构。我带团队做聚合方案设计时,强制执行三步法:

2.1 第一步:把业务问题翻译成“聚合四要素”

任何复杂的聚合需求,都能拆解为四个不可再分的原子要素:

  • 分组键(Grouping Keys):哪些字段决定“一组数据”?比如“按客户ID+商户类别分组”,这里“客户ID”是主键,“商户类别”是业务维度,二者缺一不可。
  • 聚合函数(Aggregation Functions):对每组数据计算什么?注意,这里必须明确是统计函数(如mean)还是业务函数(如“近30天交易金额极差”)。前者pandas内置,后者必须自定义。
  • 时间窗口(Time Window):是否涉及时间序列?如果是,是固定窗口(滚动)、增长窗口(扩展)、还是静态窗口(如“2024年Q1”)?窗口的起止点、对齐方式(按日/按周/按交易事件)、空值处理策略,必须书面确认。
  • 结果形态(Output Shape):最终要什么格式?是宽表(region为行、product为列)、长表(每行一个region-product组合)、还是嵌套JSON(供API调用)?这个决定了.unstack().reset_index().to_dict()等形态转换操作的顺序和参数。

举个真实案例:某信用卡中心提需求——“请输出每个客户近90天内,在‘餐饮’和‘零售’两类商户的交易金额中位数,并对比其与全量客户中位数的偏离度”。我们拆解:

  • 分组键:customer_id(必须) +merchant_category(限定为‘餐饮’‘零售’,需预过滤)
  • 聚合函数:median(统计函数) +(x - global_median) / global_median(业务函数,需两阶段计算)
  • 时间窗口:滚动90天(rolling(window='90D')),但注意:pandas的rolling默认按索引日期对齐,而交易日期可能有缺失,必须用min_periods=1并配合fillna(method='ffill')
  • 结果形态:宽表,customer_id为索引,dining_medianretail_mediandining_deviationretail_deviation为列

这四要素一旦定稿,技术方案就锁死了。后面所有代码都是填空题,而不是选择题。

2.2 第二步:警惕“维度爆炸”陷阱——不是所有组合都该一起groupby

新手常犯的错误是:看到需求里有“客户+产品+地区+时间”,就一股脑写groupby(['cust_id','prod','region','date'])。结果数据量从100万暴增到5000万行(笛卡尔积),内存直接爆掉。生产环境里,我们必须做维度裁剪:

  • 主维度(Primary Dimension):决定分析主体的字段,如customer_idmerchant_id,必须保留。
  • 过滤维度(Filtering Dimension):用于筛选数据子集的字段,如merchant_category in ['Dining','Retail'],应在groupby前用.query()或布尔索引完成,而非加入groupby。
  • 透视维度(Pivoting Dimension):需要展开成列的字段,如region,应通过.unstack()处理,而非放在groupby里。
  • 时间维度(Temporal Dimension):若需时间序列计算,优先用.rolling().expanding(),而非将日期作为groupby键(除非真要按日统计)。

我们曾有个项目,原始需求是“按客户+月份+产品线+风险等级统计交易额”。如果全放groupby,组合数超2亿。我们重构为:

  1. 先按customer_id分组,计算每个客户的月度汇总(resample('M').sum()
  2. 再对结果按product_linerisk_levelunstack(),生成宽表
  3. 最后用.apply()计算各指标
    内存占用从48GB降到3.2GB,运行时间从27分钟缩短到92秒。

2.3 第三步:生产环境的硬约束——性能、可维护性、可审计性

在实验室用10万行数据跑通的代码,放到生产环境处理10亿行交易流水,会暴露所有隐藏缺陷。我总结出三条铁律:

  • 性能铁律:所有聚合操作必须能在单机pandas上完成,禁用apply(lambda x: ...)遍历行(除非万不得已)。.agg()字典映射、.rolling().mean().expanding().sum()这些向量化操作是唯一选择。
  • 可维护性铁律:每个自定义函数必须有docstring说明业务含义,且函数名能直译业务术语。比如def calculate_fraud_risk_score(series):def func1(x):强一万倍。六月后你休假,同事要改这个逻辑,他得花10分钟猜你在算什么,还是10秒看懂?
  • 可审计性铁律:所有中间结果必须可追溯。比如计算“滚动7天均值”,不能只存最终结果,必须保留rolling_window_size=7, min_periods=4, closed='right'等参数,并在日志里记录。某次审计发现,因closed='left'导致窗口未包含当日数据,整个风控阈值偏移了12%,这就是血泪教训。

这套设计思路,不是为了显得高大上,而是为了让你写的每一行聚合代码,都能经得起业务方的追问、运维的压测、合规的检查。接下来,我们就按这个框架,把原文中的五个技术点,全部还原成生产现场的真实操作。

3. 核心细节解析与实操要点:从代码片段到生产级实现

原文展示了多个pandas聚合示例,但它们更像教学演示——数据干净、边界清晰、无异常值。而真实世界的数据,充满缺失、重复、类型错乱、时序错位。我把每个技术点拆解成“标准写法+生产加固+避坑指南”三层,全部基于我亲手调试过的银行生产代码。

3.1 多列多函数聚合:别让MultiIndex成为你的噩梦

原文代码:

result = df.groupby('merchant_category').agg({ 'transaction_amount': ['mean','median'], 'processing_fee': ['min','max'] })

标准写法(实验室版):这段代码本身没问题,但输出是MultiIndex DataFrame,列名为('transaction_amount', 'mean')这样的元组。业务方要导入Power BI,BI工具不认识元组列名,直接报错。

生产加固(我的做法)

  1. 列名扁平化:用pd.MultiIndex.droplevel(0)map('_'.join)生成可读列名
  2. 添加业务前缀:避免列名冲突,如'amt_mean'而非'mean'
  3. 空值安全处理.agg()遇到全NaN列会返回NaN,但业务上可能需要0或特殊标记
# 生产级写法 def safe_agg(df, group_col, agg_dict): """生产环境多列聚合:自动扁平化列名 + 空值处理""" # 步骤1:执行聚合 result = df.groupby(group_col).agg(agg_dict) # 步骤2:扁平化列名,格式为 'col_func' result.columns = ['_'.join(col).strip() for col in result.columns] # 步骤3:对数值列,将NaN替换为0(业务约定) numeric_cols = result.select_dtypes(include=[np.number]).columns result[numeric_cols] = result[numeric_cols].fillna(0) return result # 使用 result = safe_agg( df, 'merchant_category', { 'transaction_amount': ['mean', 'median'], 'processing_fee': ['min', 'max'] } ) # 输出列名:['transaction_amount_mean', 'transaction_amount_median', 'processing_fee_min', 'processing_fee_max']

提示:扁平化列名时,'_'.join(col)'.'.join(col)更安全,因为某些BI工具不支持点号列名。我们曾因列名含.导致Tableau连接失败,排查了3小时。

避坑指南

  • 坑1:聚合函数返回类型不一致。比如'count'返回int,'mean'返回float,混合使用会导致整列转为object类型。解决方案:统一用astype(float)或在agg字典中分开处理。
  • 坑2:分组键含空值groupby默认丢弃NaN分组键,但业务上“未知地区”可能需要单独统计。必须显式设置dropna=Falsedf.groupby('region', dropna=False)
  • 坑3:内存爆炸。当分组键基数极高(如1000万客户ID),.agg()会构建巨大中间表。此时改用daskpolars,或先采样验证逻辑。

3.2 自定义聚合函数:业务逻辑必须“可读、可测、可审计”

原文用lambda和简单函数演示,但生产环境绝不允许lambda——它无法被单元测试,无法加日志,无法debug。

标准写法(实验室版)

df.groupby('category').agg({'amount': lambda x: x.max() - x.min()})

生产加固(我的做法)

  1. 函数独立定义:放在utils/aggregations.py,便于复用和测试
  2. 强制类型注解:明确输入输出类型,IDE可提示错误
  3. 内置业务校验:如“极差不能为负”,抛出带业务上下文的异常
# utils/aggregations.py from typing import Union, Optional import numpy as np import pandas as pd def transaction_range(series: pd.Series) -> float: """ 计算交易金额极差(最大值-最小值) 业务规则:极差必须>=0,否则视为数据异常 """ if series.empty: return 0.0 max_val = series.max() min_val = series.min() if max_val < min_val: raise ValueError(f"数据异常:series.max()={max_val} < series.min()={min_val}") return float(max_val - min_val) # 在聚合中使用 result = df.groupby('category').agg({'amount': transaction_range})

单元测试示例(test_aggregations.py)

def test_transaction_range(): # 测试正常情况 s = pd.Series([100, 200, 150]) assert transaction_range(s) == 100.0 # 测试空序列 s_empty = pd.Series([]) assert transaction_range(s_empty) == 0.0 # 测试异常情况(触发业务校验) s_abnormal = pd.Series([50, 30]) # max<min with pytest.raises(ValueError, match="数据异常"): transaction_range(s_abnormal)

注意:所有自定义聚合函数必须接受pd.Series,返回标量(int/float/str)。返回pd.Serieslist会导致.agg()报错,这是新手高频错误。

避坑指南

  • 坑1:函数内部修改原Series。pandas传递的是视图,series.sort_values(inplace=True)会污染原始数据。永远用series.copy()
  • 坑2:忽略NaN处理series.max()遇到全NaN返回-inf,业务上应返回0或None。必须加skipna=True(默认)并检查结果。
  • 坑3:性能黑洞。避免在函数内调用len(series)series.tolist(),这会触发O(n)遍历。用series.size和向量化操作替代。

3.3 滚动窗口聚合:时间对齐才是真正的难点

原文示例用rolling(window=3),但生产环境的时间窗口,核心挑战从来不是计算,而是时间对齐

标准写法(实验室版)

df_ts['rolling_avg'] = df_ts.groupby('category')['daily_revenue'].rolling(window=3).mean()

生产加固(我的做法)

  1. 用日期频率替代固定数字window='3D'window=3可靠,避免因数据缺失导致窗口大小漂移
  2. 显式控制闭合方向closed='right'(默认)表示包含当前行,closed='both'包含首尾,业务必须明确
  3. 空值策略标准化min_periods=1保证首行有值,再用fillna(method='ffill')向前填充
def rolling_mean_by_date( df: pd.DataFrame, date_col: str, value_col: str, window: str = '7D', min_periods: int = 1, closed: str = 'right' ) -> pd.Series: """ 按日期频率的滚动均值(生产级) """ # 步骤1:确保date_col是datetime类型 df = df.copy() df[date_col] = pd.to_datetime(df[date_col]) # 步骤2:设为时间索引,按category分组滚动 result = ( df.set_index(date_col) .groupby('category')[value_col] .rolling(window=window, min_periods=min_periods, closed=closed) .mean() .reset_index(level=0, drop=True) # 丢弃category索引,保留date索引 ) # 步骤3:前向填充空值(业务约定:首日用当日值) return result.fillna(method='ffill') # 使用 df_ts['rolling_7day_avg'] = rolling_mean_by_date( df_ts, 'date', 'daily_revenue', window='7D' )

避坑指南

  • 坑1:索引丢失.rolling()后索引是MultiIndex(category+date),但业务需要date为唯一索引。必须用.reset_index(level=0, drop=True)
  • 坑2:时区混乱。交易数据常有时区(如UTC),而报表需本地时区(如Asia/Shanghai)。必须在set_index前统一转换:df[date_col] = df[date_col].dt.tz_convert('Asia/Shanghai')
  • 坑3:周末无数据window='7D'在周五计算时,会包含下周的周一(若数据未入库)。解决方案:用offset参数,如window='7D', offset='1D'跳过未来日期。

3.4 扩展窗口聚合:累计值不是“一直加”,而是“有起点的加”

原文用.expanding().sum(),但生产中“累计”必须明确定义起点——是“从第一笔交易开始”,还是“从开户日起”,或是“从风控模型上线日起”?

标准写法(实验室版)

df_ts['cumulative_sum'] = df_ts.groupby('category')['daily_revenue'].expanding().sum()

生产加固(我的做法)

  1. 起点可控:不依赖数据自然顺序,用min_date参数指定累计起始日
  2. 断点续算:当客户销户后重新开户,累计值应重置,而非继续累加
  3. 业务标识:累计值列名必须体现起点,如cumsum_since_20240101
def expanding_cumsum_with_start( df: pd.DataFrame, date_col: str, value_col: str, group_col: str, start_date: str = None, fill_na: float = 0.0 ) -> pd.Series: """ 带起始日期的扩展累计和 """ df = df.copy() df[date_col] = pd.to_datetime(df[date_col]) if start_date: start_dt = pd.to_datetime(start_date) # 过滤出start_date之后的数据 df = df[df[date_col] >= start_dt] # 按group_col分组,对value_col做扩展累计 result = ( df.sort_values([group_col, date_col]) .groupby(group_col)[value_col] .expanding() .sum() .reset_index(level=0, drop=True) ) # 合并回原df,缺失处填0 full_index = df.set_index([group_col, date_col]).index result = result.reindex(full_index, fill_value=fill_na) return result # 使用:从2024年1月1日起累计 df_ts['cumsum_since_20240101'] = expanding_cumsum_with_start( df_ts, 'date', 'daily_revenue', 'category', start_date='2024-01-01' )

注意:.expanding()默认按分组内顺序计算,所以必须先sort_values。曾有项目因未排序,导致累计值乱序,损失2天排查时间。

避坑指南

  • 坑1:数据重复。同一日期多条记录,.expanding()会逐条累加,但业务上应先sum()再累计。必须在expanding前用.groupby([group_col, date_col]).sum()去重。
  • 坑2:跨年累计year-to-date需每年重置,不能用.expanding(),而要用.groupby(['category', df[date_col].dt.year]).cumsum()
  • 坑3:精度丢失。累计值用float64,10亿次累加后误差可达0.01。金融场景必须用decimalInt64(单位:分)。

3.5 多级分组与Unstack:宽表不是目的,是业务沟通的语言

原文.unstack()输出矩阵,但生产中最大的问题是:列顺序错乱、缺失值填充策略不一致、行列标签业务含义模糊

标准写法(实验室版)

result = df_sales.groupby(['region','product'])['revenue'].mean().unstack()

生产加固(我的做法)

  1. 预定义列顺序:用categories参数确保product列按业务重要性排序(如['Widget','Gadget'])
  2. 缺失值业务填充fill_value=0可能误导(0代表“无交易”还是“交易额为0”?),改用fill_value=np.nan并加注释
  3. 行列标签业务化region列名改为销售大区product行名改为产品线
def safe_unstack( series: pd.Series, unstack_level: int = -1, fill_value: Union[float, None] = None, column_order: Optional[list] = None, index_name: str = None, columns_name: str = None ) -> pd.DataFrame: """ 生产级unstack:可控列序 + 业务标签 """ # 步骤1:unstack result = series.unstack(level=unstack_level, fill_value=fill_value) # 步骤2:按业务顺序重排列 if column_order and len(column_order) > 0: # 只保留存在的列,避免KeyError existing_cols = [c for c in column_order if c in result.columns] missing_cols = [c for c in column_order if c not in result.columns] result = result[existing_cols] # 对缺失列,添加全NaN列 for col in missing_cols: result[col] = np.nan # 步骤3:设置行列业务名称 if index_name: result.index.name = index_name if columns_name: result.columns.name = columns_name return result # 使用 result = ( df_sales .groupby(['region', 'product'])['revenue'] .mean() .pipe(safe_unstack, column_order=['Widget', 'Gadget'], index_name='销售大区', columns_name='产品线' ) )

避坑指南

  • 坑1:unstack后索引丢失.unstack()会把分组键之一变为列,另一键变为行索引。若需双索引,用.unstack(level=0)指定层级。
  • 坑2:内存翻倍。宽表比长表内存大3-5倍。大数据量时,优先用pivot_table并设置aggfunc='first'避免聚合。
  • 坑3:BI工具兼容性。某些BI工具不支持MultiIndex列,必须用result.columns = result.columns.get_level_values(1)提取二级列名。

4. 实操过程与核心环节实现:一个银行信用卡分析Pipeline的完整复现

现在,我们把前面所有加固点,整合成一个真实的生产Pipeline。场景:某银行信用卡中心,每日需生成《客户交易行为日报》,包含7个分析模块。我用真实代码(已脱敏)展示从原始数据到报表的全流程,所有参数均来自2024年Q2生产配置。

4.1 数据准备与质量加固

原始数据源是Kafka实时流,经Flink清洗后落库,但我们仍需二次校验:

# data_loader.py import pandas as pd import numpy as np from datetime import datetime, timedelta def load_daily_transactions(date_str: str) -> pd.DataFrame: """ 加载指定日期的交易数据(模拟数据库查询) 生产中此处为SQL查询或Parquet读取 """ # 模拟数据生成(实际从数据库读取) np.random.seed(hash(date_str) % 1000000) # 每日数据可重现 # 生成10万行模拟交易 n_rows = 100000 customers = [f'C{str(i).zfill(4)}' for i in np.random.randint(1, 5000, n_rows)] categories = np.random.choice(['Groceries','Dining','Travel','Retail','Utilities'], n_rows) amounts = np.random.lognormal(5, 0.8, n_rows).round(2) # 模拟右偏分布 dates = pd.date_range(date_str, periods=n_rows, freq='T')[:n_rows] # 每分钟一笔 df = pd.DataFrame({ 'date': dates, 'customer_id': customers, 'category': categories, 'amount': amounts, 'fee': (amounts * 0.025).round(2), 'is_refund': np.random.choice([True, False], n_rows, p=[0.02, 0.98]) }) # 关键质量加固:业务规则校验 # 规则1:交易金额必须>0 df = df[df['amount'] > 0] # 规则2:退款单金额为负,且fee=0 df.loc[df['is_refund'], 'amount'] = -df.loc[df['is_refund'], 'amount'] df.loc[df['is_refund'], 'fee'] = 0.0 # 规则3:日期必须在当日范围内(防数据延迟入库) target_date = pd.to_datetime(date_str) df = df[ (df['date'] >= target_date) & (df['date'] < target_date + pd.Timedelta(days=1)) ] return df # 加载2024-06-15数据 df_raw = load_daily_transactions('2024-06-15') print(f"原始数据加载完成:{len(df_raw)} 行,{df_raw['date'].min()} 至 {df_raw['date'].max()}") # 输出:原始数据加载完成:99842 行,2024-06-15 00:00:00 至 2024-06-15 23:59:00

4.2 分析模块1:多维统计(客户+品类)

目标:每个客户在各商户类别的交易均值、中位数、笔数、手续费范围。

# analysis/multi_dimensional_stats.py from utils.aggregations import safe_agg, transaction_range def analyze_customer_category_stats(df: pd.DataFrame) -> pd.DataFrame: """ 客户-品类多维统计(生产级) """ # 步骤1:预过滤(业务要求:仅统计非退款交易) df_filtered = df[~df['is_refund']].copy() # 步骤2:定义聚合字典(业务函数+内置函数混合) agg_dict = { 'amount': ['mean', 'median', 'count'], 'fee': ['min', 'max', transaction_range] # 复用自定义函数 } # 步骤3:执行聚合(自动扁平化+空值处理) result = safe_agg( df_filtered, ['customer_id', 'category'], agg_dict ) # 步骤4:业务列名重命名(提升可读性) rename_map = { 'amount_mean': 'avg_transaction_amt', 'amount_median': 'median_transaction_amt', 'amount_count': 'transaction_count', 'fee_min': 'min_processing_fee', 'fee_max': 'max_processing_fee', 'fee_transaction_range': 'fee_range' } result = result.rename(columns=rename_map) return result # 执行 stats_df = analyze_customer_category_stats(df_raw) print("客户-品类统计完成") print(stats_df.head())

4.3 分析模块2:滚动窗口(客户级7日均值)

目标:每个客户最近7天(含当日)的交易金额滚动均值,用于识别消费趋势突变。

# analysis/rolling_analysis.py from utils.time_utils import rolling_mean_by_date def analyze_customer_rolling_avg(df: pd.DataFrame) -> pd.DataFrame: """ 客户级7日滚动均值(生产级) """ # 步骤1:确保date为datetime df = df.copy() df['date'] = pd.to_datetime(df['date']) # 步骤2:按客户分组,计算滚动均值 # 注意:此处用'7D'而非7,避免因数据缺失导致窗口不足 rolling_series = rolling_mean_by_date( df, date_col='date', value_col='amount', group_col='customer_id', window='7D', min_periods=1, closed='right' ) # 步骤3:合并回原df,取当日值 df_result = df.copy() df_result['rolling_7day_avg'] = rolling_series.values # 步骤4:按客户聚合,取最新一条(即当日滚动均值) latest_rolling = ( df_result .sort_values(['customer_id', 'date']) .groupby('customer_id') .tail(1)[['customer_id', 'rolling_7day_avg']] .set_index('customer_id') ) return latest_rolling # 执行 rolling_df = analyze_customer_rolling_avg(df_raw) print("客户滚动均值完成") print(rolling_df.head())

4.4 分析模块3:扩展窗口(客户生命周期累计)

目标:每个客户自开户日起的累计交易额,用于计算客户LTV。

# analysis/expanding_analysis.py from utils.time_utils import expanding_cumsum_with_start def analyze_customer_cumulative_spend(df: pd.DataFrame, open_date: str) -> pd.DataFrame: """ 客户累计消费额(自开户日起) """ # 步骤1:加载客户开户日期(模拟从用户表关联) # 生产中此处为JOIN操作 customer_open_dates = { 'C0001': '2023-01-01', 'C0002': '2023-03-15', # ... 实际从dim_customer表获取 } # 步骤2:为每行标注有效起始日 df = df.copy() df['open_date'] = df['customer_id'].map(customer_open_dates).fillna('1970-01-01') # 步骤3:按客户分组,计算自开户日起的累计 result = expanding_cumsum_with_start( df, date_col='date', value_col='amount', group_col='customer_id', start_date=open_date, # 传入全局起始日 fill_na=0.0 ) # 步骤4:取每个客户的最新累计值 df_result = df.copy() df_result['cumulative_spend'] = result.values latest_cumsum = ( df_result .sort_values(['customer_id', 'date']) .groupby('customer_id') .tail(1)[['customer_id', 'cumulative_spend']] .set_index('customer_id') ) return latest_cumsum # 执行(假设全局起始日为2023-01-01) cumsum_df = analyze_customer_cumulative_spend(df_raw, '2023-01-01') print("客户累计消费完成") print(cumsum_df.head())

4.5 分析模块4:交叉分析(客户vs品类宽表)

目标:生成客户-品类交易均值宽表,供BI直接使用。

# analysis/crosstab_analysis.py from utils.pivot_utils import safe_unstack def analyze_customer_category_crosstab(df: pd.DataFrame) -> pd.DataFrame: """ 客户-品类交叉分析(宽表) """ # 步骤1:计算客户-品类均值 mean_series = df.groupby(['customer_id', 'category'])['amount'].mean() # 步骤2:unstack为宽表,指定品类顺序(业务要求:Groceries优先) result = safe_unstack( mean_series, column_order=['Groceries', 'Dining', 'Retail', 'Travel', 'Utilities'], index_name='客户ID', columns_name='商户类别' ) # 步骤3:业务填充:缺失值填0(代表该客户无此品类交易) result = result.fillna(0.0) return result # 执行 crosstab_df = analyze_customer_category_crosstab(df_raw) print("客户-品类宽表完成") print(crosstab_df.head())

4.6 分析模块5:风险分层(高价值交易识别)

目标:识别每个客户的高价值交易行为,用于反欺诈模型特征。

# analysis/risk_segmentation.py import numpy as np def analyze_risk_segmentation(df: pd.DataFrame, high_value_threshold: float = 300.0) -> pd.DataFrame: """ 风险分层分析:高价值交易识别 """ def risk_metrics(series: pd.Series) -> pd.Series: """计算单个客户的高价值交易指标""" total_count = len(series) if total_count == 0: return pd.Series({ 'high_value_count': 0, 'high_value_pct': 0.0, 'regular_avg': 0.0, 'high_value_avg': 0.0 }) # 高价值交易 high_value_mask = series > high_value_threshold high_value_count = high_value
http://www.jsqmd.com/news/1196783/

相关文章:

  • 2026御网杯网络安全赛线下题解(部分)
  • 2026实力之选:铭嘉源装饰公司——常州装修市场的专业服务品牌 - 企业推荐官【官方】
  • Anthropic的研究撕开了AI对齐的底裤-龍德明宇
  • 2026年7月最新烟台江诗丹顿官方售后客服电话及服务网点地址查询 - 江诗丹顿官方服务中心
  • Sqribble文档自动化原理:模板驱动的结构化排版系统
  • 欧米茄中国官方售后服务中心|电话和完整地址权威信息声明(2026年7月最新) - 欧米茄服务中心
  • Axios POST请求传参实战:从基础到高级场景全解析
  • 计算机毕业设计之基于springboot动漫网站的设计与实现
  • 数字IC前端设计进阶指南:从Verilog到FPGA原型验证的实战路径
  • 2026年河北煤矿支护网供应商挑选攻略 瑞邦丝网等优质企业梳理 - 每天一杯纯牛奶
  • 单链表逻辑结构梳理
  • 鸿蒙原生开发手记:徒步迹 - ArkTS 语法快速入门
  • 大模型GEO结构化排版:3个格式逻辑让内容优先被提取,零代码提25%引用率附格式对照表
  • 限时返场开源组件集成实践:风险评估与Spring Boot安全集成指南
  • 用AI做用户分群:RFM模型的智能化升级与自动聚类
  • 从AIGC到AIGS,企业AI框架必须补上一层“业务语义底座
  • 山东大学程序设计思维-记事本:从零实现一个支持粘滞选择与查找的文本编辑器
  • 宇舶中国官方售后服务中心|服务热线及全部维修地址权威信息公示(2026年7月最新) - 亨得利官方服务中心
  • 道心通明:一个觉醒者的宇宙观
  • 【C/C++ 实用工具】从原理到实战:主流内存检测工具深度剖析
  • 基于STM32单片机的智能全自动洗衣机蓝牙/WiFi APP设计DIY-T157
  • 恶意代码沙箱检测架构:在隔离环境里放毒看反应
  • 量化实盘异常处理流程:断线、部分成交、重复委托与人工接管
  • 热电效应三兄弟:从赛贝克到帕尔帖,揭秘半导体制冷片背后的物理世界
  • 基于STC89C52与DHT11的智能家居环境调控系统设计与实现
  • 心跳监控有什么用?云哨兵有一台“任务打卡机”
  • 【C语言】深入探究字母大小写转换的三种底层逻辑
  • STM32平衡小车进阶调试(调参篇)
  • 模板驱动型文档自动化:零代码实现动态内容填充与品牌一致性
  • 混沌初开:模糊算法与生命智能