AI大赛-资金流入流出-周期因子128分
当当当!128分!我用的纯周期因子来预测的,没想到效果这么好!

# 核心思路:化繁为简,抓住“规律”
在资金流入流出预测中,余额宝的数据有非常明显的“人类行为规律”:
- 周末效应:大家周末喜欢消费、转出,工作日喜欢转入。
- 发薪日效应:每月的月初(特别是1号到10号),大家发工资了,转入量会暴增。
纯周期因子的核心思想就是:历史会重演。 我们不需要去拟合复杂的数学曲线,只需要把历史上“周一”或者“每月5号”的平均水平算出来,作为未来“周一”或“5号”的预测值。
# 完整代码
"""
蚂蚁金服资金预测 - 纯周期因子方案
核心思路:将历史数据按“星期”和“月中第几天”分组求均值,直接作为未来预测
"""
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
# ========== 1. 加载并聚合数据 ==========
print("Step1: 加载数据...")
data = pd.read_csv('./data/user_balance_table.csv')
data['report_date'] = pd.to_datetime(data['report_date'], format='%Y%m%d')
# 按日聚合
daily = data.groupby('report_date').agg({
'total_purchase_amt': 'sum',
'total_redeem_amt': 'sum'
}).reset_index()
daily = daily.sort_values('report_date').reset_index(drop=True)
daily = daily.set_index('report_date')
# 补全日历(防止中间有缺失日期)
daily = daily.asfreq('D')
# 使用均值填充缺失值,避免 ffill 造成的趋势突变
daily = daily.fillna(daily.mean())
print(f"数据范围: {daily.index[0]} ~ {daily.index[-1]}")
# ========== 2. 选稳定期数据 ==========
print("\nStep2: 选稳定期数据 (2014-03-01 ~ 2014-08-31)...")
train_start = '2014-03-01'
train_end = '2014-08-31'
daily_stable = daily.loc[train_start:train_end]
print(f"训练天数: {len(daily_stable)}")
# ========== 3. 纯周期因子预测函数 ==========
def pure_periodic_predict(history_df, target_col, future_dates):
"""
基于星期因子 + 月中日期因子的纯周期预测
"""
df = history_df.copy()
df = df.reset_index()
# 提取周期特征
df['dayofweek'] = df['report_date'].dt.dayofweek # 0=周一, 6=周日
df['dayofmonth'] = df['report_date'].dt.day # 1-31
# 1. 计算星期因子(该星期几的平均值 / 全局平均值)
global_mean = df[target_col].mean()
weekday_means = df.groupby('dayofweek')[target_col].mean()
weekday_factors = weekday_means / global_mean
# 2. 计算月中日期因子(该天是几号的平均值 / 全局平均值)
# 余额宝通常在月初(1-10号)有发薪日效应,月中日期因子非常关键
dayofmonth_means = df.groupby('dayofmonth')[target_col].mean()
dayofmonth_factors = dayofmonth_means / global_mean
# 3. 生成未来预测
preds = []
for date in future_dates:
dow = date.dayofweek
dom = date.day
# 获取对应的因子,如果找不到(比如2月30号)则默认为1
w_factor = weekday_factors.get(dow, 1.0)
d_factor = dayofmonth_factors.get(dom, 1.0)
# 核心公式:预测值 = 全局均值 * 星期因子 * 月中日期因子
# 双因子叠加能更精确地捕捉“月初的周一”或“月末的周日”等复合效应
pred_val = global_mean * w_factor * d_factor
preds.append(max(0, pred_val)) # 负值截断
return np.array(preds)
# ========== 4. 执行预测 ==========
print("\nStep3: 纯周期因子预测...")
future_dates = pd.date_range('2014-09-01', periods=30)
# 预测申购
pred_purchase = pure_periodic_predict(daily_stable, 'total_purchase_amt', future_dates)
# 预测赎回
pred_redeem = pure_periodic_predict(daily_stable, 'total_redeem_amt', future_dates)
# ========== 5. 预测结果预览 ==========
print("\nStep4: 预测结果预览(前5天)...")
for i in range(5):
date_str = future_dates[i].strftime('%m-%d (%a)')
print(f" {date_str}: 申购={pred_purchase[i] / 1e8:.4f}亿, 赎回={pred_redeem[i] / 1e8:.4f}亿")
# ========== 6. 生成提交文件 ==========
print("\nStep5: 生成提交文件...")
submission = pd.DataFrame({
'report_date': future_dates.strftime('%Y%m%d'),
'total_purchase_amt': pred_purchase.astype(int),
'total_redeem_amt': pred_redeem.astype(int)
})
# 注意:天池提交格式通常要求没有表头,且用逗号分隔
submission.to_csv('submission_pure_factor.csv', sep=',', header=False, index=False)
print("完成!文件已保存为 submission_pure_factor.csv")
print("\n前5行预览:")
print(submission.head())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# 代码逐行拆解教学
# 第一部分:数据清洗与准备
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore') # 忽略掉运行过程中的一些无关紧要的警告
# 1. 加载数据
data = pd.read_csv('./data/user_balance_table.csv')
# 把 '20140101' 这种字符串格式转换成 Python 能识别的 datetime 时间对象
data['report_date'] = pd.to_datetime(data['report_date'], format='%Y%m%d')
# 2. 按日聚合
# 原始数据可能一天有多条记录,我们需要把它们按天加起来
daily = data.groupby('report_date').agg({
'total_purchase_amt': 'sum', # 申购总金额求和
'total_redeem_amt': 'sum' # 赎回总金额求和
}).reset_index()
# 3. 排序并设置索引
daily = daily.sort_values('report_date').reset_index(drop=True)
daily = daily.set_index('report_date') # 把日期设为索引,方便后面按时间切片
# 4. 补全日历(非常关键的一步!)
# 如果原始数据中间缺了几天(比如系统故障没记录),asfreq('D') 会把这些空缺补出来,变成 NaN
daily = daily.asfreq('D')
# 用整个数据集的平均值去填充这些空缺。为什么不用 ffill(用前一天的值填充)?
# 因为如果前一天是发薪日(金额巨大),用 ffill 会把巨大金额复制到空缺日,产生严重的噪声。用均值填充更平滑、更安全。
daily = daily.fillna(daily.mean())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 第二部分:核心预测函数(纯周期因子)
这是整个代码的灵魂,我们把它封装成了一个函数:
def pure_periodic_predict(history_df, target_col, future_dates):
"""
history_df: 历史训练数据 (2014-03-01 ~ 2014-08-31)
target_col: 要预测的列名 ('total_purchase_amt' 或 'total_redeem_amt')
future_dates: 未来要预测的30天日期列表
"""
df = history_df.copy()
df = df.reset_index() # 把日期从索引变回普通列,方便提取特征
# 【特征工程】提取两个最核心的周期特征
# dt.dayofweek: 0代表周一,6代表周日
df['dayofweek'] = df['report_date'].dt.dayofweek
# dt.day: 代表这是当月的第几天 (1 ~ 31)
df['dayofmonth'] = df['report_date'].dt.day
# 1. 计算全局平均值 (Global Mean)
# 这是我们的“基准线”,比如历史上平均每天转入 5亿
global_mean = df[target_col].mean()
# 2. 计算“星期因子”
# 先算出历史上每个星期几(周一到周日)的平均值
weekday_means = df.groupby('dayofweek')[target_col].mean()
# 计算因子:比如周一的均值是 6亿,全局均值是 5亿,那周一的因子就是 1.2
weekday_factors = weekday_means / global_mean
# 3. 计算“月中日期因子”
# 算出历史上每个月1号、2号...31号的平均值
dayofmonth_means = df.groupby('dayofmonth')[target_col].mean()
# 同样除以全局均值,得到因子。比如每月5号的因子可能是 1.5 (发薪日效应)
dayofmonth_factors = dayofmonth_means / global_mean
# 4. 生成未来30天的预测值
preds = []
for date in future_dates:
dow = date.dayofweek # 比如 2014-09-01 是周一,dow=0
dom = date.day # 比如 2014-09-01 是1号,dom=1
# 获取对应的因子。如果历史上没有这一天(比如2月30号),就默认因子为1.0
w_factor = weekday_factors.get(dow, 1.0)
d_factor = dayofmonth_factors.get(dom, 1.0)
# 【核心公式】预测值 = 基准线 * 星期因子 * 月中日期因子
# 比如:5亿 * 1.2(周一效应) * 1.5(月初效应) = 9亿
pred_val = global_mean * w_factor * d_factor
preds.append(max(0, pred_val)) # 资金不能为负数,如果算出负数就强制变成0
return np.array(preds)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 第三部分:执行预测与生成提交文件
# 生成未来30天的日期列表 (2014-09-01 到 2014-09-30)
future_dates = pd.date_range('2014-09-01', periods=30)
# 分别对“申购”和“赎回”调用我们刚才写的函数
pred_purchase = pure_periodic_predict(daily_stable, 'total_purchase_amt', future_dates)
pred_redeem = pure_periodic_predict(daily_stable, 'total_redeem_amt', future_dates)
# 把预测结果打包成 DataFrame
submission = pd.DataFrame({
'report_date': future_dates.strftime('%Y%m%d'), # 把日期变回 '20140901' 字符串格式
'total_purchase_amt': pred_purchase.astype(int), # 金额取整数
'total_redeem_amt': pred_redeem.astype(int)
})
# 保存为 CSV 文件。header=False 表示不保存列名,index=False 不保存行索引,sep=',' 用逗号分隔
submission.to_csv('submission_pure_factor.csv', sep=',', header=False, index=False)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我有个问题,乘了星期因子,又乘日期因子,不会让本来大的数更大,本来小的更小吗,两个这样乘不会重复吗
如果两个因子是“独立”的,直接相乘确实会导致极端值被过度放大。 但在我们的代码里,这种相乘不仅不会重复,反而是捕捉复杂规律的“杀手锏”。
为什么直接相乘不会“重复”?(因为它们是正交的)
在我们的模型里:
星期因子:反映的是“周维度的规律”(比如周一大家都转入,周日大家都转出)。
月中日期因子:反映的是“月维度的规律”(比如每月5号发工资,转入暴增)。
这两个规律在数学上被称为“正交”(互相独立)。也就是说,“今天是星期几”和“今天是几号”之间没有必然的因果关系。它们捕捉的是不同维度的人类行为。直接相乘到底在做什么?(捕捉“复合效应”)
我们来看一个具体的例子,假设全局平均每天的转入金额是 1个亿:
场景A:一个普通的周二
周二因子 = 1.0(平平无奇)
15号因子 = 1.0(月中,平平无奇)
预测值 = 1亿 * 1.0 * 1.0 = 1亿(很合理)
场景B:发薪日的周一(比如每月1号刚好是周一)
周一因子 = 1.2(大家刚过完周末,周一转入意愿强)
1号因子 = 1.5(发薪日,转入暴增)
预测值 = 1亿 * 1.2 * 1.5 = 1.8亿
关键点来了:
如果你只用周一因子,你只能预测到 1.2亿;如果你只用1号因子,你只能预测到 1.5亿。但实际上,当“发薪日”和“周一”撞在一起时,资金流入的爆发力是叠加的!直接相乘,正是为了精准捕捉这种“1+1 > 2”的复合效应。那什么时候会“过度放大”?(你的担忧是有道理的)
如果数据里有一个“超级极端值”(比如某个月1号刚好是周一,而且那天还发了双倍的年终奖,导致转入量是平时的3倍),那么:
周一因子会被拉高。
1号因子也会被拉高。
两者相乘,预测值就会变得极其夸张。
- 怎么解决?(进阶技巧) 在工业界或者更高级的比赛中,为了解决你说的“过度放大”问题,我们通常不会直接用“均值”算因子,而是会用“中位数”或者“去极值后的均值”来计算因子。这样可以剔除掉那些极端的异常值,让因子更稳健。
总结一下 你担心的“重复放大”在两个因子高度相关时才会发生(比如你同时用了“周一因子”和“工作日因子”,这两个就重复了)。但在我们的代码里,星期和日期是两个完全独立的维度,直接相乘是为了捕捉它们的联合爆发力。
# 总结与进阶
你看,这套代码其实没有用到任何复杂的机器学习模型,全靠对业务场景的理解(发薪日、周末效应)和统计学方法(求均值、算因子)。
你现在已经掌握了“单模型周期因子”的精髓。如果未来还想继续提分,可以在这个基础上做两个小升级:
- 节假日特殊处理:遇到国庆、春节,大家的操作会非常反常,可以单独给节假日算一个因子。
- 多模型融合:用这个周期因子算出一个结果,再用简单的线性回归算出一个结果,最后两个结果取个加权平均,通常能再榨出几分。