简介:天猫大数据竞赛资金流入流出预测项目是一份面向金融数据分析与时间序列建模学习者、竞赛参与者的完整实践资料包。项目融合自回归积分滑动平均模型与线性回归模型,针对用户资金申购赎回行为进行预测,并对中秋、国庆等特殊节假日进行特征调优,提升预测准确度。压缩包共14个文件、约46KB,包含7个Python脚本、2个SQL脚本、2个R脚本,以及文档与说明文件,分别对应数据处理、模型构建、建表查询与项目解读等环节,结构清晰。针对节假日效应的专项调优显著提高了模型在实际业务场景中的鲁棒性,已有41人学习下载。通过源码与说明可掌握从数据预处理、特征工程到模型训练评估的完整流程,还可直接复用节假日效应处理策略,为电商平台资金管理和投资决策提供参考。
1. 资金流入流出预测:为什么 ARIMA 与线性回归的组合值得复现
一到中秋和国庆,很多平台的货币基金类产品就会迎来申购赎回的双重脉冲,节前申购猛增、节后赎回集中。平日跑得还算顺的时间序列模型,在这两个节点上经常翻车,偏偏这类特殊时间点的预测准确度直接影响资金管理决策。我复现这个资金流入流出预测项目时最大的感触是:ARIMA 模型负责趋势惯性,线性回归模型负责节假日修正,两者接力比任何单一模型都稳。压缩包里从 ODPS 建表、日频聚合、ARIMA 定阶到线性回归特征调优的全流程脚本都有,适合竞赛入门者、金融数据分析和时序预测从业者照着步骤复现,也能帮熟手快速补齐节假日特征处理的实操细节。
2. 拆包与数据准备:从压缩包目录到 ODPS 建表和第一版特征
2.1 先认文件:哪个脚本管哪一段
拿到压缩包,第一件事不是急着跑代码,而是先建立对脚本结构的认知。解压后主目录是 Pur_Red_forecast-master,命名对应 Pur(申购)和 Red(赎回)两端预测。工程里实际出现的文件有 create_table.sql、example code for ODPS.sql、PRF1.py 到 PRF8.py、PRF5.r,以及 README、说明文件和一份附赠资源文档。
按脚本编号推测的任务切分是这样的:PRF1.py 负责读取原始流水并做清洗,PRF2.py 负责聚合出日频资金流时间序列,PRF3.py 构建基础特征和滞后特征,PRF4.py 跑线性回归,PRF6.py 处理节假日窗口,PRF7.py 做模型对比和评估,PRF8.py 最终输出预测结果。PRF5.r 是 R 语言版本的主模型脚本,说明原始方案里存在 Python 和 R 双语言验证的痕迹。这种文件名排序方式有个好处:每一步只需要改上一个文件的输出路径,链条清晰,不用在单个脚本里维护几百行耦合逻辑。
我复现时的启动顺序比较简单:先执行 create_table.sql 建表,再在竞赛指定的云端数仓环境(代码注释里叫 ODPS)跑 SQL 做数据聚合,最后回到本地 Python 环境跑模型脚本。README 只给了简明说明,附赠资源相当于补充的项目背景和调参记录,建议通读一遍再动代码,能省不少事。
2.2 create_table.sql:数据表结构和分区设计
竞赛环境提供的是一个分布式数仓,create_table.sql 就在这套环境里执行。建表语句直接体现字段和分区设计:
CREATE TABLE IF NOT EXISTS fund_flow_daily ( ds STRING COMMENT '业务日期,yyyy-MM-dd', user_id BIGINT COMMENT '用户标识', purchase_amt DOUBLE COMMENT '申购金额', redeem_amt DOUBLE COMMENT '赎回金额', balance DOUBLE COMMENT '当前账户余额' ) PARTITIONED BY (dt STRING COMMENT '数据加载日期分区') LIFECYCLE 366;这里有两个细节值得留意。第一,ds 是业务日期,dt 是数据加载日期,两者分开,避免重复加载覆盖历史业务日。第二,LIFECYCLE 366 是数仓的保留机制,一年后自动清理旧分区,既控制存储成本又防止跨年数据污染训练集。如果本地用 MySQL 或 ClickHouse,分区写法不同,但分层逻辑相同:把业务时间和写入时间拆成两个字段,是所有后续回测和防未来数据泄露的基础。
实际使用里,输入的表往往不是这种整理过的明细,而是更大粒度的申购赎回流水。所以 create_table.sql 建好之后,通常要再跑一遍类型转换,确保金额字段没有字符串中混入逗号,时间字段没有漏掉补零的月份。这个环节看起来琐碎,但做不好会直接影响后面的聚合结果。
2.3 第一版日频聚合:从流水到时间序列
原始表是亿行级别的用户流水,模型吃不下这么细的粒度,需要先按天聚合。example code for ODPS.sql 里给出的是标准聚合逻辑:
SELECT ds, CAST(SUM(purchase_amt) AS DOUBLE) AS total_purchase, CAST(SUM(redeem_amt) AS DOUBLE) AS total_redeem, COUNT(DISTINCT user_id) AS active_users FROM fund_flow_daily WHERE dt = '${bizdate}' AND ds >= TO_CHAR(DATEADD(TO_DATE('${bizdate}', 'yyyy-mm-dd'), -365, 'dd'), 'yyyy-mm-dd') GROUP BY ds ORDER BY ds;这段 SQL 的核心是按业务日期做日频汇总,同时把活跃用户数也捞出来。active_users 这个字段在后面特征调优时很有用,因为申购赎回总额受用户基数影响很大,节假日用户量本身就涨,不看活跃用户数就容易把“用户变多”误判成“人均购买力变强”。DATEADD 里的 -365 表示取一年窗口,用来控制训练集长度,避免全量历史数据导致的分布漂移。
跑完这段 SQL,得到的就是一张三列日频表。我一般落地成 CSV 在 Pandas 里继续处理。顺带提醒,分布式数仓的 SUM 对 DOUBLE 类型有精度截断,别在 SQL 里做太多浮点乘法,把数值运算留到 Python 里更稳。后面避坑章节还会展开这个点。
3. ARIMA 建模:参数选择、平稳性检验和预测闭环
3.1 为什么要用 ARIMA 而不是直接上 LSTM
在处理日频资金流入流出时序时,很多同学第一反应是上 LSTM 或者 XGBoost。但竞赛基线方案常用 ARIMA 是有道理的:资金流数据量虽然大,日频序列只有几百个点,深度模型在小样本上容易过拟合;ARIMA 的差分和滑动平均正好覆盖资金流的随机游走特征,且参数少、可解释性强。
ARIMA 由自回归、差分和滑动平均三部分组成,p 是自回归阶数,d 是差分次数,q 是滑动平均阶数。它的核心假设是:当前值可以由过去 p 个值和过去 q 个噪声的线性组合来解释。这对日常资金流来说是成立的,因为用户的申购赎回习惯天然有惯性,工资日、还款日前后资金流动规律性明显。
3.2 平稳性检验和定阶:先定 d 再定 p、q
ARIMA 建模的常规套路是先检验平稳性,如果 ADF 检验的 p 值大于 0.05,就先做一阶差分。PRF2.py 里的实现基本是这个逻辑:
import pandas as pd from statsmodels.tsa.stattools import adfuller df = pd.read_csv('daily_flow.csv', parse_dates=['ds']) series = df.set_index('ds')['total_purchase'] # 第一步:ADF 检验判断是否平稳 adf_res = adfuller(series.dropna(), autolag='AIC') print('p-value before diff:', adf_res[1]) d = 0 diff_series = series if adf_res[1] > 0.05: diff_series = series.diff().dropna() d = 1 # 差分后再检验一次,确认已经平稳 print('p-value after diff:', adfuller(diff_series, autolag='AIC')[1])这段代码执行完,差分阶数 d 已经确定。接下来看 ACF 和 PACF 图确定 p 和 q:如果 PACF 在滞后 1 阶后截尾,说明自回归部分取 p=1;如果 ACF 在滞后 1 阶后截尾,说明滑动平均部分取 q=1。实际数据往往没有教科书那么干净,所以更稳妥的办法是网格搜索 AIC 值。
import warnings from statsmodels.tsa.arima.model import ARIMA best_aic = float('inf') best_order = None with warnings.catch_warnings(): warnings.filterwarnings('ignore') for p in range(0, 4): for q in range(0, 4): try: model = ARIMA(series, order=(p, d, q)) fit = model.fit() if fit.aic < best_aic: best_aic = fit.aic best_order = (p, d, q) except Exception: continue print('best_order:', best_order, 'AIC:', best_aic)逻辑说明:AIC 是赤池信息准则,它同时惩罚拟合优度和参数数量,避免模型无意义地变复杂。网格搜索范围取 p、q 都在 0 到 3,对日频资金流足够,超过 3 阶容易进入过拟合。脚本里同时有 PRF2.py 和 PRF5.r,说明 Python 和 R 各跑了一遍同样的搜索,两边交叉验证定阶结果。
3.3 预测回测:滚动窗口比单次拟合更有参考价值
竞赛评估通常要求预测未来若干天,而一次拟合全样本再预测未来,很难看出模型在不同时间段的表现差异。常见做法是把历史数据切成滚动窗口,比如用前 60 天训练、预测后 7 天,窗口每 7 天往前挪一次,收集每个窗口的误差。
import numpy as np from sklearn.metrics import mean_absolute_error def rolling_arima(series, train_len=60, horizon=7): errors = [] for start in range(0, len(series) - train_len - horizon, horizon): train = series.iloc[start:start + train_len] test = series.iloc[start + train_len:start + train_len + horizon] model = ARIMA(train, order=best_order).fit() pred = model.forecast(horizon) errors.append(mean_absolute_error(test, pred)) return np.mean(errors) mae = rolling_arima(series) print('rolling MAE:', mae)这段回测有两个参数要调:train_len 决定每次训练用多长历史,horizon 是前向预测跨度。对资金流场景,60 天训练、7 天预测是常见起点。如果训练窗口太短,模型学不到月度周期;如果太长,历史权重可能冲淡最近的节假日脉冲。回测结果出来后,结合误差分布再决定是否需要引入线性回归修正,这就自然过渡到第 4 章的特征调优逻辑。
4. 线性回归与特征调优:把节假日效应塞进特征里
4.1 为什么 ARIMA 不够,还要线性回归
ARIMA 擅长捕捉长期趋势和自相关,但它对突发性事件几乎无感知,比如中秋节前一天的申购暴增,在 ARIMA 看来就是一个莫名误差。线性回归的优势在于:只要把节假日标志、节前倒数天数这类信息作为特征显式喂进去,模型就能学到“中秋前一天申购上升明显”这样的规律。
竞赛方案里用的不是单变量回归,而是把 ARIMA 的预测结果当作一个特征,与节假日特征、滞后特征、星期特征一起送入线性回归做融合。这种基模型加修正器的思路,比单独跑任一模型都稳,这也是特征调优的核心意义。
4.2 特征构造:时间和节假日特征一览
PRF3.py 主要做特征工程。对日频资金流数据,我一般构造这几类特征:时间特征如星期几、是否月初、是否月末、当月第几天;滞后特征如前 1 天、前 7 天、前 14 天的申购赎回值;平滑特征如过去 7 天移动平均、过去 30 天移动平均;节假日特征如是否节假日、节前倒数第几天、节后第几天、节假日类型。
import pandas as pd df = pd.read_csv('daily_flow.csv', parse_dates=['ds']) df['dayofweek'] = df['ds'].dt.dayofweek df['day'] = df['ds'].dt.day df['is_month_start'] = (df['ds'].dt.day <= 3).astype(int) df['is_month_end'] = (df['ds'].dt.day >= 28).astype(int) df['lag_1'] = df['total_purchase'].shift(1) df['lag_7'] = df['total_purchase'].shift(7) df['ma_7'] = df['total_purchase'].rolling(7).mean()日期特征里要注意 is_month_start 和 is_month_end 的区间设定。有的月份 28 号就是最后一天,所以直接用“28 号之后”作为月末标志,比判断当月最后一天更简单,预测时也不需要知道下个月日历。
节假日特征需要手动定义日期列表,这是整个项目里最依赖业务经验的部分:
holidays_2023 = { 'mid_autumn': ['2023-09-29', '2023-09-30', '2023-10-01'], 'national': ['2023-10-01', '2023-10-02', '2023-10-03', '2023-10-04', '2023-10-05', '2023-10-06'], }特征落地时按距离节日起点还有几天构造计数,比如节前第 5 天到节前第 1 天分别标记为 1 到 5,节日当天标记 0,节后第 1 天到第 3 天标记 -1 到 -3。这样模型得到的不是简单哑变量,而是能学习节前申购上升、节后赎回回补这种渐变节奏。比单独一个 is_holiday 布尔特征信息量大多了。
4.3 训练线性回归:预测值与特征的关系
拿到特征之后,训练线性回归模型并查看各特征系数,能让资金流变化的原因一目了然:
from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split feature_cols = ['dayofweek', 'day', 'is_month_start', 'is_month_end', 'lag_1', 'lag_7', 'ma_7', 'is_holiday', 'holiday_countdown'] X = df[feature_cols].fillna(0) y = df['total_purchase'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False) lr = LinearRegression() lr.fit(X_train, y_train) coef_df = pd.DataFrame({'feature': feature_cols, 'coef': lr.coef_}) print(coef_df.sort_values('coef', ascending=False))这里的 train_test_split 关闭 shuffle,目的是严格按时间顺序切分。系数输出里通常能看到 holiday_countdown 的负系数,也就是节前倒数天数越小、申购金额越大,同时 lag_7 的正系数说明周度惯性明显,这符合资金流场景的业务直觉。线性回归在这里承担的可解释性作用,比它本身的预测精度更重要,因为业务决策需要知道预测值为什么会涨。
4.4 中秋和国庆窗口的特殊处理
真正拉开预测差距的是特殊时间点的处理。国庆前后 7 天往往出现申购先涨、赎回后涨的镜像结构,因为用户在假期前买入货币基金赚假期收益,假期结束后立刻赎回。对这种模式,普通回归特征无法描述。
PRF6.py 里处理方式是构造交互特征:国庆前第 3 天开始,把距离国庆天数和是否工作日做交叉相乘,得到一个新的数值特征。这样可以捕捉到节前最后一个工作日申购暴增这种规律。模型训练时再把中秋、国庆等不同节日分开建特征,避免不同节日的效应互相稀释。
这里有一个容易犯的错误:直接把节假日当成连续的时间段处理。实际业务里中秋只影响 3 天,国庆影响 7 天,两者的资金转移幅度完全不同。所以特征构造时,我一般会为每个节日单独生成一套布尔标志和倒数天数,而不是共用一套 is_holiday。
5. 避坑指南:资金流时间序列预测里最容易踩的四个坑
5.1 节假日效应被差分天然抹平
现象:一阶差分之后,中秋节前一天的申购峰值变成相邻两天的差值,数值反而比平时还小,模型根本学不到节日记忆,预测误差在节日前后不减反增。
原因:ARIMA 的差分操作天然把原始值变成了增量序列,恒定水平的节日脉冲经过差分后就消失了,只剩一个毛刺。这段时间序列模型对“突然上升”的处理能力是结构性短板,不是单纯调参能解决的。
解决:不要在差分后的序列上单独做节假日特征,要把节假日特征放到线性回归这一侧,让回归模型基于原始值学习脉冲。或者像 PRF6.py 那样,在差分前单独构造节日日期的哑变量,确保信息不被差分吞掉。自查方式是画差分前后的时序图,看节日当天的形态是否还在。
5.2 滚动窗口统计混入未来信息
现象:用全样本计算均值和标准差做标准化后训练模型,回测集上的 MAE 指标虚低,但提交后分数立刻崩盘,差距可以达到两倍以上。
原因:标准化参数是全样本计算的,等于模型在训练阶段偷看了测试集的均值方差。这种未来信息泄露在时间序列里比常规分类里更隐蔽,因为不会报错,模型表面看起来也正常。
解决:标准化、缺失值填充这类统计计算全部放到训练窗口内完成,滚动预测时每个窗口重新计算标准化参数。代码层面就是自定义一个标准化器,在滚动循环内部重新训练,不要在整个数据集上提前 fit。自查方式是检查标准化器的均值和方差是否只来自训练窗口。
5.3 只对申购建模,赎回端完全复制申购的参数
现象:把申购的 ARIMA 参数直接套在赎回数据上,国庆后赎回预测整体偏低,误差甚至比随机数还大,而且回测图上能看到明显的系统性偏移。
原因:申购和赎回的行为节奏完全相反。申购在节前集中,赎回在节后爆发,两者虽然相关,但不等于同分布序列。资金在不同时间段里的主导因素不同,强行共用参数会把赎回的节后脉冲误当成申购的延续。
解决:为申购和赎回各跑一遍定阶流程。PRF2.py 和 PRF7.py 的脚本注释里其实已经提示这点,申购赎回两端应该独立建模再融合,直接复制参数是典型的偷懒错误。自查方式是分别看两个序列的 ACF 图,滞后结构明显不同,就不能共用参数。
5.4 分布式数仓聚合时出现浮点精度抖动
现象:SQL 聚合结果里突然出现 1e-9 级别的抖动,模型训练时 AIC 四处乱跳,无法稳定定阶,甚至同一个脚本跑两次结果不一样。
原因:分布式数仓里 string 字段与 double 字段拼接或者比较时,隐式转换可能丢失精度。长时间没有刷新分区时,混合类型的脏数据会导致 SUM 结果异常。
解决:建表时金额字段统一用 DOUBLE,日期字段统一用 STRING 且格式固定为 yyyy-MM-dd。聚合 SQL 末尾用 CAST 把结果显式转成 DOUBLE,杜绝隐式转换。自查方式是抽查几个大额日期,把 SQL 结果和原始流水的手算值做比对。
6. 验证与微调:残差白噪声检验、节假日权重微调与提交前检查
建好 ARIMA 加线性回归的融合模型之后,真正的工程时间应该花在验证和微调上。我一般先做残差诊断:把训练集拟合值和实际值的残差拿出来,画 ACF 图并跑 Ljung-Box 检验。如果残差在各阶滞后上的自相关不显著,说明模型已经把规律榨干,剩下的就是噪声;如果残差在滞后 7 阶上还有明显自相关,说明周级别的规律没学进去,需要回到特征工程补齐周期项。
from statsmodels.stats.diagnostic import acorr_ljungbox resid = fit.resid lb = acorr_ljungbox(resid, lags=[7, 14], return_df=True) print(lb)输出里 p 值大于 0.05 才能认为残差近似白噪声。如果 p 值小于 0.05,优先检查滞后特征是不是漏了周频。
节假日权重微调是我在整个项目里最有心得的地方。把国庆前 7 天单独拿出来,看线性回归的真实预测误差,如果系统性偏低,就给国庆窗口的 holiday_countdown 特征乘以 1.2 的权重,重新训练后误差会明显收窄。这种调整每轮只动一个节日窗口,避免不同节日之间互相干扰。中秋窗口通常只有 3 天,样本量小,权重调整要克制,我一般控制在 0.9 到 1.1 之间,幅度再大就过拟合了。
提交前还要检查三件事:预测日期范围是否还包含节假日,时间序列的最后一个训练日是否和测试起点连续,模型输出的申购赎回金额是否为正。曾经有次我提交后才发现某一天的预测值是负数,源头是线性回归对异常日期的外推,最后在输出层加了截断就解决了。
从那以后,我每次做资金流预测项目都会强制走一遍同样的流程:先查节假日列表,再确认标准化参数没有跨窗口,最后跑一遍残差白噪声检验。三步全过才敢出预测结果。希望帮到你。
本文还有配套的精品资源,点击获取