一、题目
蚂蚁金服拥有上亿会员并且业务场景中每天都涉及大量的资金流入和流出,面对如此庞大的用户群,资金管理压力会非常大。在既保证资金流动性风险最小,又满足日常业务运转的情况下,精准地预测资金的流入流出情况变得尤为重要。通过对例如余额宝用户的申购赎回数据的把握,精准预测未来每日的资金流入流出情况。对货币基金而言,资金流入意味着申购行为,资金流出为赎回行为 。预测未来 30 天内每一天申购和赎回的总量数据
二、数据
使用的数据主要包含四个部分,分别为用户基本信息数据、用户申购赎回数据、收益率表和银行间拆借利率表。下面分别介绍四组数据。
1.用户信息表
用户信息表: user_profile_table 。 我们总共随机抽取了约 3 万用户,其中部分用户在 2014 年 9 月份第一次出现,这部分用户只在测试数据中 。因此用户信息表是约 2.8 万 个用户的基本数据,在原始数据的基础上处理后,主要包含了用户的性别、城市和星座。具体的字段如下表 1 :
表1用户信息表
| 列名 | 类型 | 含义 | 示例 |
|---|---|---|---|
| user_id | bigint | 用户 ID | 1234 |
| Sex | bigint | 用户性别( 1 :男, 0 :女 ) | 0 |
| City | bigint | 所在城市 | 6081949 |
| constellation | string | 星座 | 射手座 |
2. 用户申购赎回数据表
用户申购赎回数据表: user_balance_table 。里面有 20130701 至 20140831 申购和赎回信息、以及所有的子类目信息, 数据经过脱敏处理。脱敏之后的数据,基本保持了原数据趋势。数据主要包括用户操作时间和操作记录,其中操作记录包括申购和赎回两个部分。金额的单位是分,即 0.01 元人民币。 如果用户今日消费总量为0,即consume_amt=0,则四个字类目为空。
表格 2 :用户申购赎回数据
| 列名 | 类型 | 含义 | 示例 |
|---|---|---|---|
| user_id | bigint | 用户 id | 1234 |
| report_date | string | 日期 | 20140407 |
| tBalance | bigint | 今日余额 | 109004 |
| yBalance | bigint | 昨日余额 | 97389 |
| total_purchase_amt | bigint | 今日总购买量 = 直接购买 + 收益 | 21876 |
| direct_purchase_amt | bigint | 今日直接购买量 | 21863 |
| purchase_bal_amt | bigint | 今日支付宝余额购买量 | 0 |
| purchase_bank_amt | bigint | 今日银行卡购买量 | 21863 |
| total_redeem_amt | bigint | 今日总赎回量 = 消费 + 转出 | 10261 |
| consume_amt | bigint | 今日消费总量 | 0 |
| transfer_amt | bigint | 今日转出总量 | 10261 |
| tftobal_amt | bigint | 今日转出到支付宝余额总量 | 0 |
| tftocard_amt | bigint | 今日转出到银行卡总量 | 10261 |
| share_amt | bigint | 今日收益 | 13 |
| category1 | bigint | 今日类目 1 消费总额 | 0 |
| category2 | bigint | 今日类目 2 消费总额 | 0 |
| category3 | bigint | 今日类目 3 消费总额 | 0 |
| category4 | bigint | 今日类目 4 消费总额 | 0 |
注 1 :上述的数据都是经过脱敏处理的,收益为重新计算得到的,计算方法按照简化后的计算方式处理,具体计算方式在下节余额宝收益计算方式中描述。
注 2 :脱敏后的数据保证了今日余额 = 昨日余额 + 今日申购 - 今日赎回,不会出现负值。
3.收益率表
收益表为余额宝在 14 个月内的收益率表: mfd_day_share_interest 。具体字段如表格 3 中所示
表格 3 收益率表
| 列名 | 类型 | 含义 | 示例 |
|---|---|---|---|
| mfd_date | string | 日期 | 20140102 |
| mfd_daily_yield | double | 万份收益,即 1 万块钱的收益。 | 1.5787 |
| mfd_7daily_yield | double | 七日年化收益率( % ) | 6.307 |
4.上海银行间同业拆放利率(Shibor)表
银行间拆借利率表是 14 个月期间银行之间的拆借利率(皆为年化利率): mfd_bank_shibor 。具体字段如下表格 4 所示:
表格 4 银行间拆借利率表
| 列名 | 类型 | 含义 | 示例 |
|---|---|---|---|
| mfd_date | String | 日期 | 20140102 |
| Interest_O_N | Double | 隔夜利率(%) | 2.8 |
| Interest_1_W | Double | 1周利率(%) | 4.25 |
| Interest_2_W | Double | 2周利率(%) | 4.9 |
| Interest_1_M | Double | 1个月利率(%) | 5.04 |
| Interest_3_M | Double | 3个月利率(%) | 4.91 |
| Interest_6_M | Double | 6个月利率(%) | 4.79 |
| Interest_9_M | Double | 9个月利率(%) | 4.76 |
| Interest_1_Y | Double | 1年利率(%) | 4.78 |
5.收益计算方式
本赛题的余额宝收益方式,主要基于实际余额宝收益计算方法,但是进行了一定的简化,此处计算简化的地方如下:
首先,收益计算的时间不再是会计日,而是自然日,以 0 点为分隔,如果是 0 点之前转入或者转出的金额算作昨天的,如果是 0 点以后转入或者转出的金额则算作今天的。
然后,收益的显示时间,即实际将第一份收益打入用户账户的时间为如下表格,以周一转入周三显示为例,如果用户在周一存入 10000 元,即 1000000 分,那么这笔金额是周一确认,周二是开始产生收益,用户的余额还是 10000 元,在周三将周二产生的收益打入到用户的账户中,此时用户的账户中显示的是 10001.1 元,即 1000110 分。其他时间的计算按照表格中的时间来计算得到。
表格 5 : 简化后余额宝收益计算表
| 转入时间 | 首次显示收益时间 |
|---|---|
| 周一 | 周三 |
| 周二 | 周四 |
| 周三 | 周五 |
| 周四 | 周六 |
| 周五 | 下周二 |
| 周六 | 下周三 |
| 周天 | 下周三 |
6.预测结果表:
表 格 6 预测结果表: tc_comp_predict_table
| 字段 | 类型 | 含义 | 示例 |
|---|---|---|---|
| report_date | bigint | 日期 | 20140901 |
| purchase | bigint | 申购总额 | 40000000 |
| redeem | bigint | 赎回总额 | 30000000 |
每一行数据是一天对申购、赎回总额的预测值, 2014 年 9 月每天一行数据,共 30 行数据。 Purchase 和 redeem 都是金额数据,精确到分,而不是精确到元。
三、 评估指标
评估指标的设计主要期望选手对未来 30 天内每一天申购和赎回的总量数据预测的越准越好,同时考虑到可能存在的多种情况。选用积分式的计算方法:每天的误差选用相对误差来计算,然后根据用户预测申购和赎回的相对误差,通过得分函数映射得到一个每天预测结果的得分,将 30 天内的得分汇总,然后结合实际业务的倾向,对申购赎回总量预测的得分情况进行加权求和,得到最终评分。具体的操作如下:
1) 计算所有用户在测试集上每天的申购及赎回总额与实际情况总额的误差。
2) 申购预测得分与 Purchasei 相关,赎回预测得分与 Redeemi 相关 , 误差与得分之间的计算公式不公布,但保证该计算公式为单调递减的,即误差越小,得分越高,误差与大,得分越低。当第 i 天的申购误差 Purchasei =0 ,这一天的得分为 10 分;当 Purchasei > 0.3 ,其得分为 0 。
3) 最后公布总积分 = 申购预测得分 *45%+ 赎回预测得分 *55% 。
四、代码
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import warnings
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
warnings.filterwarnings('ignore')
# 特征工程
def feature_engineering():
print("开始特征工程...")
# 读取数据
user_balance = pd.read_csv('Purchase_Redemption_Data/user_balance_table.csv')
shibor = pd.read_csv('Purchase_Redemption_Data/mfd_bank_shibor.csv')
interest = pd.read_csv('Purchase_Redemption_Data/mfd_day_share_interest.csv')
# 转换日期格式
user_balance['report_date'] = pd.to_datetime(user_balance['report_date'], format='%Y%m%d')
shibor['mfd_date'] = pd.to_datetime(shibor['mfd_date'], format='%Y%m%d')
interest['mfd_date'] = pd.to_datetime(interest['mfd_date'], format='%Y%m%d')
# 按日期聚合申购和赎回总量
daily_data = user_balance.groupby('report_date').agg({
'total_purchase_amt': 'sum',
'total_redeem_amt': 'sum'
}).reset_index()
# 合并利率和收益数据
daily_data = daily_data.merge(shibor[['mfd_date', 'Interest_1_M', 'Interest_3_M']],
left_on='report_date', right_on='mfd_date', how='left')
daily_data = daily_data.merge(interest[['mfd_date', 'mfd_7daily_yield']],
left_on='report_date', right_on='mfd_date', how='left')
daily_data.drop(['mfd_date_x', 'mfd_date_y'], axis=1, inplace=True)
# 创建时间特征
daily_data['day_of_week'] = daily_data['report_date'].dt.dayofweek
daily_data['day_of_month'] = daily_data['report_date'].dt.day
daily_data['month'] = daily_data['report_date'].dt.month
daily_data['is_month_start'] = (daily_data['report_date'].dt.is_month_start).astype(int)
daily_data['is_month_end'] = (daily_data['report_date'].dt.is_month_end).astype(int)
daily_data['is_quarter_start'] = (daily_data['report_date'].dt.is_quarter_start).astype(int)
daily_data['is_quarter_end'] = (daily_data['report_date'].dt.is_quarter_end).astype(int)
# 创建滞后特征
for i in [1, 2, 3, 5, 7, 14, 21]:
daily_data[f'purchase_lag_{i}'] = daily_data['total_purchase_amt'].shift(i)
daily_data[f'redeem_lag_{i}'] = daily_data['total_redeem_amt'].shift(i)
# 创建移动平均特征
for i in [3, 5, 7, 14, 21, 30]:
daily_data[f'purchase_ma_{i}'] = daily_data['total_purchase_amt'].rolling(window=i).mean()
daily_data[f'redeem_ma_{i}'] = daily_data['total_redeem_amt'].rolling(window=i).mean()
# 创建指数加权移动平均特征
for i in [5, 7, 14]:
daily_data[f'purchase_ewm_{i}'] = daily_data['total_purchase_amt'].ewm(span=i).mean()
daily_data[f'redeem_ewm_{i}'] = daily_data['total_redeem_amt'].ewm(span=i).mean()
# 创建差分特征
daily_data['purchase_diff_1'] = daily_data['total_purchase_amt'].diff(1)
daily_data['redeem_diff_1'] = daily_data['total_redeem_amt'].diff(1)
daily_data['purchase_diff_7'] = daily_data['total_purchase_amt'].diff(7)
daily_data['redeem_diff_7'] = daily_data['total_redeem_amt'].diff(7)
# 创建变化率特征
for i in [1, 3, 7, 14]:
daily_data[f'purchase_change_{i}'] = daily_data['total_purchase_amt'].pct_change(i)
daily_data[f'redeem_change_{i}'] = daily_data['total_redeem_amt'].pct_change(i)
# 创建周期性特征(正弦和余弦变换)
daily_data['day_of_year'] = daily_data['report_date'].dt.dayofyear
daily_data['sin_day_of_year'] = np.sin(2 * np.pi * daily_data['day_of_year'] / 365)
daily_data['cos_day_of_year'] = np.cos(2 * np.pi * daily_data['day_of_year'] / 365)
return daily_data
# 模型定义
def train_models(data):
print("开始模型训练和调参...")
# 准备特征和目标变量
feature_cols = [col for col in data.columns if col not in ['report_date', 'total_purchase_amt', 'total_redeem_amt']]
# 去除包含NaN的行
train_data = data.dropna()
X_purchase = train_data[feature_cols]
y_purchase = train_data['total_purchase_amt']
X_redeem = train_data[feature_cols]
y_redeem = train_data['total_redeem_amt']
# 分割训练集和验证集
X_train_p, X_val_p, y_train_p, y_val_p = train_test_split(X_purchase, y_purchase, test_size=0.2, random_state=42)
X_train_r, X_val_r, y_train_r, y_val_r = train_test_split(X_redeem, y_redeem, test_size=0.2, random_state=42)
# 初始化模型 - 调整参数以提高性能
models = {
'rf_purchase': RandomForestRegressor(n_estimators=200, max_depth=10, random_state=42, n_jobs=-1),
'gb_purchase': GradientBoostingRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42),
'lr_purchase': LinearRegression(),
'rf_redeem': RandomForestRegressor(n_estimators=200, max_depth=10, random_state=42, n_jobs=-1),
'gb_redeem': GradientBoostingRegressor(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42),
'lr_redeem': LinearRegression()
}
# 训练模型
for name, model in models.items():
if 'purchase' in name:
model.fit(X_train_p, y_train_p)
else:
model.fit(X_train_r, y_train_r)
print(f"{name} 训练完成")
# 验证模型并计算权重
scores = {}
weights = {}
for name, model in models.items():
if 'purchase' in name:
pred = model.predict(X_val_p)
mse = mean_squared_error(y_val_p, pred)
else:
pred = model.predict(X_val_r)
mse = mean_squared_error(y_val_r, pred)
scores[name] = mse
# 根据验证集表现计算权重(MSE越小权重越大)
weights[name] = 1 / (mse + 1e-8) # 添加小值避免除零
print(f"{name} MSE: {mse}")
return models, feature_cols, weights
# 模型融合
def ensemble_predict(models, X, weights=None):
predictions = []
model_names = []
for name, model in models.items():
pred = model.predict(X)
predictions.append(pred)
model_names.append(name)
# 如果没有指定权重,则基于模型表现计算权重
if weights is None:
weights = [1/len(predictions)] * len(predictions)
else:
# 提取对应模型的权重
weights = [weights[name] for name in model_names]
# 归一化权重
weight_sum = sum(weights)
weights = [w/weight_sum for w in weights]
# 加权平均
final_pred = np.average(predictions, axis=0, weights=weights)
return final_pred
# 预测未来30天
def predict_future(data, models, feature_cols):
print("开始预测未来30天...")
# 获取最后一天的日期
last_date = data['report_date'].max()
# 构建未来30天的日期
future_dates = [(last_date + timedelta(days=i)).strftime('%Y%m%d') for i in range(1, 31)]
# 复制最后几行数据作为预测基础
future_data = data.tail(60).copy() # 取最近60天数据用于构造特征
purchase_preds = []
redeem_preds = []
# 逐日预测
for i in range(30):
# 获取当前用于预测的特征
current_features = future_data[feature_cols].tail(1)
# 处理NaN值 - 用均值填充或者前向填充
if current_features.isnull().any().any():
# 使用前向填充,如果仍然有NaN则用0填充
current_features = current_features.fillna(method='ffill').fillna(method='bfill').fillna(0)
# 获取申购和赎回预测模型
purchase_models = {k: v for k, v in models.items() if 'purchase' in k}
redeem_models = {k: v for k, v in models.items() if 'redeem' in k}
# 预测
purchase_pred = ensemble_predict(purchase_models, current_features)[0]
redeem_pred = ensemble_predict(redeem_models, current_features)[0]
purchase_preds.append(int(purchase_pred))
redeem_preds.append(int(redeem_pred))
# 构造新行添加到future_data中,用于下一次预测
new_row = future_data.tail(1).copy()
new_row['report_date'] = new_row['report_date'] + timedelta(days=1)
new_row['total_purchase_amt'] = purchase_pred
new_row['total_redeem_amt'] = redeem_pred
# 更新滞后特征
for j in [1, 2, 3, 7, 14, 21]:
new_row[f'purchase_lag_{j}'] = future_data['total_purchase_amt'].iloc[-j] if len(future_data) >= j else purchase_pred
new_row[f'redeem_lag_{j}'] = future_data['total_redeem_amt'].iloc[-j] if len(future_data) >= j else redeem_pred
# 更新移动平均特征(简化处理)
for j in [7, 14, 30]:
if len(future_data) >= j:
new_row[f'purchase_ma_{j}'] = future_data['total_purchase_amt'].tail(j).mean()
new_row[f'redeem_ma_{j}'] = future_data['total_redeem_amt'].tail(j).mean()
else:
new_row[f'purchase_ma_{j}'] = purchase_pred
new_row[f'redeem_ma_{j}'] = redeem_pred
# 更新差分特征(简化处理)
new_row['purchase_diff_1'] = purchase_pred - future_data['total_purchase_amt'].iloc[-1]
new_row['redeem_diff_1'] = redeem_pred - future_data['total_redeem_amt'].iloc[-1]
if len(future_data) >= 7:
new_row['purchase_diff_7'] = purchase_pred - future_data['total_purchase_amt'].iloc[-7]
new_row['redeem_diff_7'] = redeem_pred - future_data['total_redeem_amt'].iloc[-7]
else:
new_row['purchase_diff_7'] = new_row['purchase_diff_1']
new_row['redeem_diff_7'] = new_row['redeem_diff_1']
# 更新时间特征
new_row['day_of_week'] = new_row['report_date'].dt.dayofweek.iloc[0]
new_row['day_of_month'] = new_row['report_date'].dt.day.iloc[0]
new_row['month'] = new_row['report_date'].dt.month.iloc[0]
future_data = pd.concat([future_data, new_row], ignore_index=True)
return future_dates, purchase_preds, redeem_preds
# 生成提交文件
def generate_submission(dates, purchase_preds, redeem_preds):
print("生成提交文件...")
submission = pd.DataFrame({
'date': dates,
'total_purchase_amt': purchase_preds,
'total_redeem_amt': redeem_preds
})
submission.to_csv('comp_predict_table.csv', index=False, header=False)
print("预测结果已保存到 comp_predict_table.csv")
# 主函数
def main():
# 特征工程
data = feature_engineering()
# 模型训练和调参
models, feature_cols = train_models(data)
# 预测未来30天
dates, purchase_preds, redeem_preds = predict_future(data, models, feature_cols)
# 生成提交文件
generate_submission(dates, purchase_preds, redeem_preds)
# 显示预测结果
result = pd.DataFrame({
'date': dates,
'total_purchase_amt': purchase_preds,
'total_redeem_amt': redeem_preds
})
print("\n未来30天预测结果预览:")
print(result.head(10))
if __name__ == "__main__":
main()
代码分析
1. 数据准备与特征工程
1.1 数据加载
-
读取三个数据源:
- 用户余额数据(
user_balance_table.csv) - 银行间同业拆放利率数据(
mfd_bank_shibor.csv) - 每日收益数据(
mfd_day_share_interest.csv)
- 用户余额数据(
1.2 数据预处理
- 日期格式转换:将字符串格式的日期转换为datetime对象
- 数据聚合:按日期汇总申购和赎回总量
1.3 特征工程亮点
-
时间特征:
- 提取星期、月份、季度等信息
- 标记月初/月末、季初/季末等特殊时点
-
滞后特征:
- 创建1/2/3/5/7/14/21天的滞后特征
-
移动平均特征:
- 计算3/5/7/14/21/30天的简单移动平均
- 计算5/7/14天的指数加权移动平均
-
差分特征:
- 1天和7天的差分值
-
变化率特征:
- 1/3/7/14天的变化率
-
周期性特征:
- 对一年中的天数进行正弦/余弦变换
-
外部特征:
- 合并了银行间利率和基金收益数据
2. 模型训练与验证
2.1 数据准备
- 分离特征和目标变量(申购和赎回金额)
- 处理缺失值(直接删除包含NaN的行)
- 划分训练集和验证集(80/20比例)
2.2 模型选择
实现了三种回归模型:
-
随机森林回归(RandomForestRegressor)
- 设置200棵树,最大深度10
- 使用并行计算(n_jobs=-1)
-
梯度提升回归(GradientBoostingRegressor)
- 设置200棵树,最大深度6
- 学习率0.1
-
线性回归(LinearRegression)
- 作为基准模型
2.3 模型评估
- 使用均方误差(MSE)作为评估指标
- 根据验证集表现计算模型权重(MSE越小权重越大)
3. 模型融合
3.1 加权融合策略
- 根据各模型在验证集上的MSE计算权重
- 权重归一化处理
- 对申购和赎回预测分别使用不同的模型组合
3.2 预测流程
- 使用集成模型进行预测
- 对申购和赎回分别预测
4. 未来30天预测
4.1 预测方法
- 采用滚动预测方式,逐日预测
- 每次预测后更新特征用于下一次预测
4.2 特征更新策略
- 滞后特征:用实际值(历史)或预测值(未来)填充
- 移动平均特征:基于历史数据或预测值重新计算
- 差分特征:基于最新数据计算
- 时间特征:根据预测日期更新