资讯详情

资讯详情

资金流入流出预测Task1实战:从数据理解到LightGBM基线搭建

1. Task1到底在解什么题先别急着写代码把业务目标掰开最近把学习赛的资金流入流出预测Task1从头到尾过了一遍感触挺深的。这类比赛看着像给了一个“预测未来资金流水”的任务实际上第一关往往不是建模而是先搞清楚题目要你预测的到底是什么评价指标怎么算数据里埋了哪些坑。很多人在Task1就翻车不是模型不行而是连题目都没读透。先说这个任务的本质。资金流入流出预测常见于金融机构、电商平台、支付系统里对账户资金变动的预估场景。放到学习赛里通常会给出一段历史流水数据包含用户标识、交易时间、交易方向流入或者流出、交易金额可能还有余额、渠道类型之类的辅助字段。Task1的目标一般不是让你直接上复杂模型而是让你基于历史数据完成一次“端到端”的流程数据读取、字段理解、分布分析、基线模型搭建。为什么Task1很重要因为后面的Task2、Task3都是在这个基础上做深化。如果Task1就把数据理解透了特征工程和模型调优就是水到渠成的事反过来如果Task1只停留在“跑通一个demo”的程度后面会发现每个环节都在补课。以我个人的经验看Task1投入产出比最高的动作不是把某个模型调到极致而是把数据里的“脾性”摸清楚。再说适合谁。这类Task1最合适两类人一类是刚入门数据竞赛、想完整走一遍流程的初学者另一类是已经有一定基础、但想快速熟悉某类业务数据结构的从业者。前者可以通过Task1建立“拿到数据后第一步做什么”的直觉后者则可以通过Task1快速判断这个赛题的难点到底在特征、在时序、还是在评价指标。在正式开始之前你先做一件事把题目给的评价指标看清楚。资金流入流出预测类赛题常见的评价指标包括均方误差MSE、均方根误差RMSE、平均绝对误差MAE也有用自定义分值函数的。不同的指标决定了你优化的方向。比如MAE对离群点不敏感如果你预测的是账户余额个别大额交易导致的极端值不会过度惩罚你而RMSE则会被极端值严重拉低一旦出现异常尾部分布你就必须考虑是否要对目标值做截断或者变换。2. 数据解剖Task1的第一步是摸字段而不是跑模型2.1 拿到数据之后先做的四件事很多人在Task1的通病是读入数据之后马上groupby、马上画图、马上开始造特征结果中途发现某个字段理解错了回头重做一遍。我的习惯是拿到数据之后按下面的顺序过一遍最多花半天时间后面能少走很多弯路。首先要确认的是数据集的“物理结构”。用pandas读入之后先看shape看每一列的类型看是否有缺失值。这步看起来基础但有一个很容易忽略的细节时间列的类型。很多比赛给的时间是字符串格式比如“2023-01-01 00:00:00”或者时间戳数值如果不统一转成datetime类型后面所有时间相关的切片和聚合都会报错或者结果异常。其次是看“粒度”。这一步尤其关键。资金流水数据的粒度一般是“每个用户每笔交易”但到了预测任务里题目往往要求你预测某个时间窗内的汇总值比如未来一周每天的净流入。从流水粒度到预测粒度的转换是整个特征工程的基石你需要明确原始数据一行代表什么预测目标一行代表什么。如果这个关系没理顺后面做的所有特征都可能错位。第三件事是看“方向”。流入流出预测里资金的流入和流出在数据里通常用一个字段区分比如type1表示流入type0表示流出或者用正负值表示。我建议先把两种方向的金额分开统计看总金额、笔数、均值再合起来看净额。因为流入和流出的行为模式差异很大比如流出往往有更强的周期性工资日、还款日流入则更容易受外部转账影响。第四件事就是看“时间范围”。数据覆盖了多久训练集和测试集的时间切点在哪里。时序类赛题最怕的就是训练集和测试集时间重叠或者测试集的时间点其实在训练集之前。这不是夸张我真见过某比赛的测试集时间范围有一部分落在训练集中间导致很多人的“预测结果”其实是用未来数据“回测”出来的分数虚高得离谱。2.2 实操用pandas快速做数据概览下面是我在Task1里用的第一段代码基本是固定套路你可以直接拿去改字段名用。import pandas as pd import numpy as np train pd.read_csv(train.csv, parse_dates[time]) test pd.read_csv(test.csv, parse_dates[time]) print(train.shape, test.shape) print(train.dtypes) print(train.head()) # 快速检查缺失值 print(train.isnull().sum()) print(test.isnull().sum()) # 时间范围 print(训练集时间范围, train[time].min(), -, train[time].max()) print(测试集时间范围, test[time].min(), -, test[time].max())运行完这段你要能从输出里读出几层信息一是数据量级训练集是几万行还是几百万行这决定了你要不要考虑用分布式工具或者采样策略二是时间跨度如果训练集只有三个月而预测目标是一个月那特征窗口就不能拉太长三是交易密度用groupby统计一下平均每天多少笔流水。# 检查每个用户有多少条记录 user_cnt train.groupby(user_id).size() print(user_cnt.describe()) # 检查每天的交易笔数分布 daily_cnt train.groupby(train[time].dt.date).size() print(daily_cnt.head())这里有一个很关键的业务判断每个用户的记录数分布是否均匀。如果大部分用户只有几条记录少数用户有上千条那么后面做用户维度特征的时候要特别小心稀疏问题可能需要做全局统计量填充而不是每个用户单独算。2.3 字段理解的常见误区资金流入流出预测的数据字段看着简单但有几个地方容易踩坑。第一个误区是把“余额”字段当成简单的数值。有些流水表里的balance字段是交易后的账户余额但需要注意余额可能被重置过、可能有负值也可能因为数据脱敏而失真。如果你直接用balance做差分来反推交易金额可能会得到和amount字段不一致的结果这时候要去核对两个字段的匹配度而不是想当然。第二个误区是忽略“渠道”或“业务类型”这类分类字段。很多赛题为了增加难度会故意在流水数据里加入不同的业务类型比如转账、消费、退款、理财赎回等。这些类型之间的金额分布差异极大如果不单独分析直接混在一起建模模型会被少数高金额类型主导。第三个误区也是我觉得最重要的很多人没有区分“交易时间”和“入账时间”。在真实业务里用户发起交易的时间和资金实际到账的时间往往不同节假日尤其明显。如果数据里同时存在这两个时间字段务必检查二者的差值分布。如果差值小可以用交易时间代替入账时间如果差值大就需要自己做时间对齐否则构建的“日汇总”特征会和真实资金流动的日节奏错位。 提示Task1阶段不要急着删“看似无关”的字段。先把所有字段列一张清单标注类型、含义、缺失率、唯一值数量。后面特征工程做到一半再回来补字段成本要高得多。3. 画图看趋势EDA做得越细后面建模越顺3.1 先看整体时间序列再做聚合视图吃到Task1这类题我一贯的建议是“先画图再算数”。因为资金流水数据的时间模式是很直观的周内效应、月初月末效应、节假日效应这些光靠describe是看不出来的画出来一眼就明白。第一张图画全量资金净流入的时间序列。这里的净流入等于流入总金额减去流出总金额。把时间作为横轴净流入作为纵轴用折线图展示。你观察几个点是否存在明显的周期性是否有突变的尖峰尖峰出现的时间点是否对应月初、月末或者特定节假日第二张图把流入和流出分开画。原因很简单净流入的波动是由两条独立序列的差决定的如果只盯着净流入你会忽略流入流出的各自规律。比如某个时间点净流入为正值可能是因为流入增加了也可能是因为流出减少了这两种情况对应的业务含义完全不同后续特征构造的方向也应该不同。第三张图按“星期几”聚合看一周内每天的流入流出均值。这一步往往是发现周期性规律的最快手段。比如周一交易量特别大周末骤降这种规律对资金预测价值极高。import matplotlib.pyplot as plt train[date] train[time].dt.date train[weekday] train[time].dt.weekday # 净流入时间序列 net train.groupby(date).apply( lambda x: x.loc[x[type] in, amount].sum() - x.loc[x[type] out, amount].sum() ) net.plot(figsize(14, 5), titleNet Flow Trend) plt.show() # 按星期几聚合 weekday_in train.loc[train[type] in].groupby(weekday)[amount].agg([sum, mean]) weekday_out train.loc[train[type] out].groupby(weekday)[amount].agg([sum, mean]) print(weekday_in) print(weekday_out)3.2 分布形态金额域和用户域的交叉分析除了时间维度金额本身的分布也值得细看。资金流水数据几乎必然存在“长尾分布”的特征大量小额交易加上少量超大额交易。这种分布对建模有直接影响表现在两个方面一是均值会被极端值拉高二是模型在预测大额交易时误差天然更大。我建议画金额的直方图分别画流入和流出横轴用对数刻度。你会发现数据在对数尺度下往往呈现近似正态分布尤其是流出金额。这个发现直接决定了后面特征构造的策略很多情况下对金额做log1p变换比直接用原始金额建模效果更好。再看用户维度的分布。统计每个用户的总流入、总流出、交易笔数然后按这些统计量排序看头部用户占比。经验上资金流水数据往往有“二八效应”20%的用户贡献了80%的资金量。如果存在这种效应那么在模型里加入用户级别的统计特征会非常有用但也要注意全局统计特征用到测试集上时要注意数据的时间切分会不会造成统计量泄露。# 金额分布对数图 fig, axes plt.subplots(1, 2, figsize(12, 4)) for ax, direction in zip(axes, [in, out]): subset train.loc[train[type] direction, amount] ax.hist(np.log1p(subset), bins80) ax.set_title(f{direction} amount distribution (log)) plt.show()3.3 从EDA里提炼出三个“题目隐藏考点”做完上面的EDA你应该能回答三个问题这三个问题我认为是Task1的隐藏考点。第一个考点数据是否平稳所谓平稳指的是时间序列的均值和方差是否随时间变化保持稳定。如果净流入的时间序列有明显的上升趋势或者季节性波动模型里就需要时间趋势项或者周期性编码而不是把时间当作单纯的索引。第二个考点是否存在“星期效应”之外的“月度效应”我处理过的很多资金流水数据月初和月末的流入流出会显著高于月中。原因不难理解工资发放、信用卡还款、账单结算都集中在月初月末。如果EDA里发现了这个规律那么日期相关的特征里一定要有“月份中的第几天”和“是否临近月末”这类变量。第三个考点用户之间的异质性有多强如果你把每个用户的时间序列归一化之后叠在一起看会发现有的用户交易稀疏而金额波动大有的用户交易频繁而金额稳定。这决定了你到底该用“全局模型”还是“分层模型”。Task1阶段不用急着做分层建模但你要在EDA阶段心里有数后续的优化空间很可能藏在用户分群上。 注意画图不是为了“交作业”而是为了给自己构建对数据的直觉。看到异常点之后先别急着当噪声扔掉去查一下原始数据里对应的是哪几笔交易往往能发现业务规则的线索。4. 把EDA变成特征Task1的特征工程怎么开局4.1 时间切片特征日期维度的信息抽提EDA跑完之后就该开始造特征了。Task1阶段我建议只做两类特征一类是时间切片特征一类是历史统计特征。这两类特征对基线模型最有用而且不容易出数据泄露的问题。时间切片特征的核心思想是把日期作为主键然后把流水聚合到“天”这个粒度上。具体来说每一个日期d我们构造该日期对应的星期几、是否月初、是否月末、是否节假日、月份编号等特征。这类特征虽然简单但对资金预测非常有效因为它直接回应了EDA阶段发现的时间规律。train[date] train[time].dt.date train[month] train[time].dt.month train[day] train[time].dt.day train[weekday] train[time].dt.weekday train[is_month_start] train[day] 5 train[is_month_end] train[day] 25这里有一个细节值得强调判断“月初月末”不要只盯着一号和三十号因为现实中资金行为往往集中在月初的前几天和月末的后几天。我习惯把1日到5日标记为月初把25日到月末标记为月末这样特征对真实业务节奏的拟合更好。当然这个窗口的宽度你可以根据数据分布调整。4.2 历史统计特征滑动窗口怎么选历史统计特征的本质是回答一个问题如果今天是t日那么过去一段时间内的资金行为对明天的资金行为有哪些影响。最常用的是滑动均值、滑动总和、滑动标准差。窗口的选择是有讲究的。如果数据是日粒度常见的窗口有7天、14天、30天。选择窗口时至少要覆盖一个完整的业务周期。如果EDA发现数据有明显的周效应那么最短的窗口也应该是7天。如果月度效应明显建议至少有一个30天窗口。# 按日期计算净流入然后做滚动统计 daily_net train.groupby(date).apply( lambda x: x.loc[x[type] in, amount].sum() - x.loc[x[type] out, amount].sum() ).reset_index(namenet_amount) daily_net daily_net.sort_values(date) daily_net[net_ma7] daily_net[net_amount].rolling(7).mean() daily_net[net_ma30] daily_net[net_amount].rolling(30).mean() daily_net[net_std7] daily_net[net_amount].rolling(7).std()这里有个坑必须提醒pandas的rolling默认窗口包含当前值这在特征工程里会造成小的数据泄露。具体来说如果你用t日的特征去预测t1日的目标那么rolling窗口如果包含了t日当天的值模型在训练时看到的特征里就有一部分来自目标日期的数据了。虽然这个泄露在Task1里不致命但如果你想让模型更严谨可以用shift(1)把窗口整体后移一天让特征只包含历史信息。4.3 用户维度特征做还是不做资金流入流出预测的原始数据通常包含user_id这就引出一个问题要不要做用户维度的特征我的建议是Task1先做最基础的“用户活跃度”特征因为它的公式简单、语义明确、不容易出问题。比如每个用户近7天交易笔数、近30天交易总金额、历史平均单笔金额等。这些特征不需要太复杂复杂特征留到后面的Task再去迭代。核心难点在于把用户维度的统计特征对齐到每日的预测数据集上。具体操作是先按“用户日期”聚合数据然后在用户组内做滚动统计。# 设每日每用户聚合后的表为 user_daily user_daily[user_amount_mean_7] user_daily.groupby(user_id)[amount].transform( lambda x: x.rolling(7).mean().shift(1) )shift(1)非常重要。它保证了这个特征只用到了预测日之前的数据不会把当天信息带入特征。这一点在后面的模型评估中会体现得很明显如果忘了shift离线指标的提升可能只是数据泄露的假象一旦上测试集就崩。 实操心得Task1阶段不要追求特征数量追求特征“干净”。宁可只做10个特征也要保证每一个都不含未来信息。特征工程的后期清理成本远高于前期构建成本。5. 用LightGBM快速搭出基线从划分数据到提交结果5.1 为什么选LightGBM而不是深度学习Task1阶段的基线模型我几乎总是推荐LightGBM。原因有三条第一它对表格数据的效果稳定且训练速度快资金流水数据经过聚合之后通常是纯表格结构用深度学习反而容易过拟合第二LightGBM的特征重要性可以帮助你做特征筛选这对任务初期的探索非常有价值第三它对缺失值和异常值的鲁棒性较好在Task1这种数据质量参差不齐的场景下能减少很多预处理工作量。你可能会有顾虑资金流水是时间序列数据用树模型能学好时间依赖吗答案是可以的前提是你要把时间信息通过特征的方式显式表达出来。比如滞后特征、滑动统计特征、周期性编码这些都是在喂给树模型之前要准备好的。树模型本身不“理解”时间但只要特征里蕴含了时间规律它就能学得很好。5.2 数据划分时序切分禁止随机切分时序赛题做数据划分时最忌讳的就是随机切分。随机切分会让训练集里混入未来数据模型“偷看”到答案离线验证分数虚假膨胀。在Task1中我推荐的切分方法是按时间切分拿前70%到80%的时间段做训练拿后20%到30%做验证。# 假设 daily_net 按日期排序 train_size int(len(daily_net) * 0.8) tr daily_net.iloc[:train_size] va daily_net.iloc[train_size:]切分完之后检查一下训练集和验证集的时间跨度是否重叠。如果不重叠恭喜你这个验证方案基本可靠。这里再补充一个小技巧如果你数据量足够可以尝试“滚动验证”也就是多个时间切点反复划分训练验证集。虽然Task1阶段不需要做这么复杂但这个意识要有因为后面的Task2、Task3做模型调优时会频繁用到滚动验证。5.3 建模与调参先看学习曲线再看分数一个人任务建模阶段最不建议一上来就调参。先把一组默认参数跑一遍看训练集和验证集的指标差距。如果训练集表现很好而验证集差说明模型过拟合如果两边都差可能特征是问题而不是模型。这一步花不了多少时间但能帮你确定下一步的优化方向。我习惯用下面的模板跑基线import lightgbm as lgb params { objective: regression, metric: mae, learning_rate: 0.1, num_leaves: 31, min_data_in_leaf: 20, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, verbose: -1 } train_data lgb.Dataset(X_train, y_train) val_data lgb.Dataset(X_valid, y_valid, referencetrain_data) model lgb.train( params, train_data, num_boost_round200, valid_sets[val_data], callbacks[lgb.early_stopping(50)] )关于评估指标需要注意一个细节如果题目给的是RMSE而你在验证时用MAE那模型的选择可能并不可靠。务必使用和官方评测一致的指标做验证。我见过不少人拿MAE调模型调得飞起结果官方用RMSE一评排名完全不是那么回事。5.4 结果检查预测值分布有没有离谱模型跑完之后别急着提交先检查预测分布。一个是训练集和测试集的特征统计是否近似。如果测试集的特征分布和训练集差异很大说明数据可能在时间分布上存在漂移此时模型预测的结果不可信。另一个是预测值本身是否有异常比如预测出了负的净流入而业务上净流入有最小值限制那就需要做后处理。还有一点预测值的方差如果远小于真实值的方差说明模型有“回归到均值”的倾向这时可以考虑对极端值做额外修正。# 检查预测分布与真实分布 import matplotlib.pyplot as plt plt.hist(y_valid, bins50, alpha0.5, labeltrue) plt.hist(y_pred, bins50, alpha0.5, labelpred) plt.legend() plt.show()这一步的目的不是为了让分布完全重合而是为了发现系统性的偏差。如果预测值整体偏低很可能是因为模型在树分裂时对极端值不敏感如果预测值整体偏高可能是特征里缺少某些拉低预测的因素。这些都值得记录下来作为后续迭代的起点。 注意事项提交前最后检查一遍训练集和测试集的时间切点是否一致特征拼接是否正确有没有“复制粘贴”导致的列错位。这个问题听起来低级但每次比赛都能看到有人因为列对不齐而拿零分。6. 常见问题与避坑清单Task1阶段最容易翻车的地方6.1 时间字段格式混乱这个问题出现频率极高。有的数据里时间字段是字符串有的是时间戳有的甚至混着两种格式。如果不在最开始统一处理后面的所有时间相关操作都会出问题。我的建议是用pandas的to_datetime统一转换遇到无法解析的格式先打印异常值而不是直接drop掉。很多时候异常时间字符串里藏着一部分测试样本不能随便丢。# 统一时间格式 train[time] pd.to_datetime(train[time], errorscoerce) # 检查无法解析的行 bad_time train[train[time].isnull()] print(bad_time.head())如果时间格式是时间戳比如10位或者13位的数字需要转换成秒还是毫秒要确认清楚。13位一般是毫秒要除以1000转成秒再转datetime。用错了转换单位所有的时间特征都会偏移一天。6.2 数据泄露滚动特征和不小心把未来信息带进去了数据泄露在资金预测里太常见了。最常见的路径就是滚动窗口函数没有shift导致特征里包含了当天的目标值。这种泄露在训练集上表现得“效果超好”一旦做线上预测模型立刻失效。判断是否泄露有一个简单方法用训练好的模型看特征重要性。如果“前一天的目标值”之类的滞后特征重要性排名奇高而且验证集分数好得不可思议先别高兴很有可能你是把未来信息泄漏进去了。此时立刻检查所有滚动特征的shift操作。另一种泄露是“标准化泄露”你在全量数据上计算了均值标准差然后对训练集测试集一起做标准化。这在表格赛里经常被忽略却几乎是所有时序预测的大忌。正确做法是只用训练集的统计量来变换测试集。6.3 金额字段的符号统一问题流入流出预测的数据里金额字段往往有两种表达方式一种是用单独的方向字段搭配正数金额另一种是直接用正负号表达方向。如果混合了两种表达方式建模前的数据清洗就可能出问题。建议全部统一为“金额为正方向用0/1表示”的格式减少后续特征构造的认知负担。6.4 预测粒度混淆日粒度还是用户日粒度Task1里最容易让人头晕的就是粒度问题。如果题目要求预测“未来N天每天的总净流入”那建模粒度就是日如果题目要求预测“未来N天每个用户的净流入”那建模粒度就是用户日。这两种粒度的数据处理方式完全不同特征构造逻辑也不同。一旦搞错后面所有的工作都是白费的。我的习惯是在代码第一行注释里写明“当前数据的粒度是XXX”然后在每次聚合操作之后重新评估粒度有没有变。这样做看起来笨但对防止后期混乱特别有效。6.5 排错速查表整理一个我在Task1阶段常用的排错清单按优先级排列训练集和测试集的时间切点是否重叠打印两者的min和max时间肉眼核对不要假设题目一定给的是顺序数据。特征列的顺序是否一致合并训练集和测试集一起做特征工程最后再拆分可以避免两边特征不一致。rolling窗口是否用了shift逐列检查重点看滞后类的特征。流失值和缺失值填充是否只用了训练集统计量检查所有fillna的参数。金额方向是否统一画出流入流出的汇总对比数值量级应该大致合理。目标列是否被意外包含进特征查看特征列表把目标名从特征里剔除。这份清单适合每次训练模型之前快速过一遍。虽然看起来基础但我敢说多数人在Task1至少会踩中一两项。7. 最后再说两句Task1的真正意义不在于分数高低我自己的体会是Task1的分数说明不了太多问题真正的收获是你对这套数据建立起来的“感觉”。比如你会知道这个数据的流入流出比大概是多少你会知道节假日对资金流动的影响有多大你会知道哪些用户在贡献主要的资金量。这些感觉在后面的特征工程和模型调优里都会变成你的决策依据。再分享一个小经验在做Task1的时候我会专门维护一个笔记文件把每一个发现记录下来哪怕是一个很简单的规律比如“周三的流出金额普遍大于流入”。这些碎片化的观察在后面解释模型结果、选择特征时非常管用。竞赛做到后面很多时候拼的不是模型复杂度而是对数据的理解深度。另外一个建议是跑通基线之后给自己提三个“如果”。如果我把滑动窗口从7天改成14天会怎样如果不区分用户维度直接预测总量会怎样如果增加一个节假日特征会怎样每个问题都对应一次小实验记录每次实验的验证分数变化。这个过程会让你逐步形成对这个赛题的系统认知也让你在写最终方案总结报告时有料可写。Task1只是整个学习赛的第一步但它决定你后续的节奏。在预测类任务里数据理解永远比模型调参更值钱。把这一步走扎实了后面不管题目怎么加难度你都有底气接得住。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →