【竞赛系列】机器学习实操项目07——全球城市计算AI挑战赛(baseline、时间序列分析、地铁流量预测)
上一章:【竞赛系列】机器学习实操项目06——客户信用评估模型进阶流程(特征重要性分析与稳定性监控)
下一章:
机器学习核心知识点目录:机器学习核心知识点目录
机器学习实战项目目录:【从 0 到 1 落地】机器学习实操项目目录:覆盖入门到进阶,大学生就业 / 竞赛必备
文章目录
- 一、时间序列基础与数据探索
- 1.1 导入时间序列分析工具库
- 1.2 读取股票时间序列数据
- 1.3 可视化股票价格时间序列
- 1.4 读取手机游戏时间序列数据
- 1.5 可视化每小时广告观看量
- 1.6 可视化每日游戏货币支出
- 二、时间序列评价指标与模式分析
- 2.1 导入时间序列预测评价指标
- 2.2 时间序列季节性分解(游戏货币支出)
- 2.3 时间序列季节性分解(每小时广告观看量)
- 2.4 时间序列的常见特性
- 季节性
- 趋势性
- 周期性
- 随机性
- 三、全球城市计算AI挑战赛:地铁流量预测
- 3.1 导入挑战赛所需工具库
- 3.2 读取挑战赛数据
- 四、地铁流量数据EDA(探索性数据分析)
- 4.1 基础数据概览(数据结构与类型)
- 4.2 缺失值检查
- 4.3 时间特征工程(提取时间维度信息)
- 4.4 单个站点的日内流量趋势(0号站点1日进站量)
- 4.5 地铁线路连接关系分析(Metro_roadMap)
- 4.6 站点连接度分析(识别换乘站与终点站)
- 4.7 单站点多日流量趋势对比(0号站点)
- 4.8 同类站点流量趋势验证(1号站点)
- 4.9 高流量站点识别(误差敏感站点)
- 4.10 高流量站点趋势分析(15号站点)
- 4.11 同期流量对比(15号站点21日vs28日)
- 4.12 高流量站点趋势验证(9号站点)
- 五、规则建模(基于时间序列相似性的流量预测)
- 5.1 计算上周10分钟级均值(21-25日)
- 5.2 计算上周小时级均值(21-25日)
- 5.3 计算10分钟段占小时的比例
- 5.4 比例异常值压缩(凌晨时段)
- 5.5 计算28日小时级均值(基准日流量)
- 5.6 构建预测数据集(匹配测试集与特征)
- 5.7 计算29日预测值(比例映射+衰减调整)
- 六、总结与补充说明
- 6.1 规则建模核心逻辑回顾
- 6.2 模型优缺点与改进方向
- 6.3 扩展工具与技术选型建议
本项目围绕“时间序列分析”核心技术,先通过股票价格、游戏行为等数据拆解时间序列的趋势性、季节性特征,再聚焦“全球城市计算AI挑战赛”的地铁流量预测任务,完成从数据探索到规则建模的全流程落地。整体工作以“时间序列相似性”为核心逻辑:先通过EDA验证地铁流量的工作日稳定模式(如早晚高峰时段固定、同期数据分布相似),再基于此设计规则模型——以上周(21-25日)10分钟级流量的小时占比提取稳定分布规律,以预测日前一天(28日)的小时级流量为基准,结合春运返程的流量衰减系数(0.95),最终实现29日10分钟级进站/出站人数的精准预测,兼顾模型的可解释性与工程落地效率。
通过网盘分享的文件:天池地铁流量预测
链接: https://pan.baidu.com/s/1k-sI1sDGufBSLJveNTm6nA?pwd=pgkk 提取码: pgkk
一、时间序列基础与数据探索
1.1 导入时间序列分析工具库
该步骤加载数据处理、可视化及时间序列分析所需的基础库,同时通过warnings.filterwarnings('ignore')
屏蔽无关警告,确保代码运行输出简洁。
import warnings
warnings.filterwarnings('ignore') # 屏蔽运行过程中的警告信息,避免干扰输出import numpy as np # 用于数值计算(如数组操作、统计量计算)
import pandas as pd # 用于数据处理(如DataFrame构建、CSV读取)
import matplotlib.pyplot as plt # 用于数据可视化(如时间序列趋势图)
import seaborn as sns # 用于更美观的统计可视化# plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签(若需中文可视化可启用)
# plt.rcParams['axes.unicode_minus']=False #用来正常显示负号(若需中文可视化可启用)
# plt.rcParams['font.family'] = ['sans-serif'] #设置默认字体# from dateutil.relativedelta import relativedelta # 处理日期差(本案例暂未使用)
# from scipy.optimize import minimize # 函数最小化(本案例暂未使用)# import statsmodels.formula.api as smf # 统计学公式接口(本案例暂未使用)
# import statsmodels.tsa.api as smt # 时间序列分析专用接口(本案例暂未使用)
# import statsmodels.api as sm # statsmodels核心库(后续季节性分解将使用)
# import scipy.stats as scs # 科学计算统计工具(本案例暂未使用)from itertools import product # 生成笛卡尔积(本案例暂未使用)
from tqdm import tqdm_notebook # 显示循环进度条(本案例暂未使用)%matplotlib inline # 设置Jupyter Notebook中图像内嵌显示
1.2 读取股票时间序列数据
该步骤读取谷歌(Google)和微软(Microsoft)2006-2018年的股票数据,数据以“日期(Date)”为索引,后续将用于展示时间序列的趋势特征。
# 读取谷歌股票数据(2006-2018年),指定Date列为索引并解析为日期格式
google = pd.read_csv('./google/GOOGL_2006-01-01_to_2018-01-01.csv', index_col='Date', parse_dates=['Date'])# 读取微软股票数据(2006-2018年),指定Date列为索引并解析为日期格式
microsoft = pd.read_csv('./google/MSFT_2006-01-01_to_2018-01-01.csv', index_col='Date', parse_dates=['Date'])
1.3 可视化股票价格时间序列
该步骤绘制谷歌和微软股票的“最高价(High)”时间序列图,直观对比两家公司股票价格的长期趋势,体现时间序列的“趋势性”特征。
# plt.figure(figsize=(20, 10)) # 若需调整图像大小可启用(宽20,高10)
# 绘制谷歌股票最高价时间序列
google.High.plot()
# 绘制微软股票最高价时间序列
microsoft.High.plot()
# 设置y轴标签(股票价格)
plt.ylabel('share price',fontsize=14)
# 设置x轴标签(日期)
plt.xlabel('date',fontsize=14)
# 添加图例,区分两条曲线
plt.legend(['Google','Microsoft'],fontsize=12)
# 保存图像为SVG格式(矢量图,放大不失真)
plt.savefig("9_1.svg", format="svg")
1.4 读取手机游戏时间序列数据
该步骤读取手机游戏的“每小时广告观看量(ads)”和“每天游戏内货币支出(currency)”数据,用于后续时间序列模式(趋势、季节性)分析。
# 读取每小时广告观看数据,指定Time列为索引并解析为日期时间格式
ads = pd.read_csv('./data/ads.csv', index_col=['Time'], parse_dates=['Time'])
# 读取每天游戏内货币支出数据,指定Time列为索引并解析为日期时间格式
currency = pd.read_csv('./data/currency.csv', index_col=['Time'], parse_dates=['Time'])
1.5 可视化每小时广告观看量
该步骤绘制“每小时广告观看量(Ads)”时间序列图,展示高频时间序列(小时级)的波动特征,为后续季节性分解提供原始数据。
# 设置图像大小(宽12,高6)
plt.figure(figsize=(12, 6))
# 绘制每小时广告观看量时间序列
plt.plot(ads.Ads)
# plt.title('每小时广告观看') # 若需添加标题可启用
# plt.grid(True) # 若需显示网格线可启用
# 设置y轴标签(流量,即广告观看次数)
plt.ylabel('traffic',fontsize=14)
# 设置x轴标签(日期时间)
plt.xlabel('date',fontsize=14)
# 保存图像为SVG格式
plt.savefig("9_2.svg", format="svg")
1.6 可视化每日游戏货币支出
该步骤绘制“每日游戏内货币支出(GEMS_GEMS_SPENT)”时间序列图,展示低频时间序列(日级)的趋势特征,为后续季节性分解提供对比数据。
# 设置图像大小(宽15,高7)
plt.figure(figsize=(15, 7))
# 绘制每日游戏内货币支出时间序列
plt.plot(currency.GEMS_GEMS_SPENT)
# 添加标题(每日的游戏花费)
plt.title('每天的游戏花费')
# 显示网格线,便于读取数据
plt.grid(True)
# 显示图像
plt.show()
二、时间序列评价指标与模式分析
2.1 导入时间序列预测评价指标
该步骤导入回归任务常用的评价指标(如R²、MAE、MSE),并自定义“平均绝对百分比误差(MAPE)”,用于后续时间序列预测模型的性能评估。
# 从sklearn.metrics导入回归任务评价指标
from sklearn.metrics import r2_score, median_absolute_error, mean_absolute_error
from sklearn.metrics import median_absolute_error, mean_squared_error, mean_squared_log_error# 自定义平均绝对百分比误差(Mean Absolute Percentage Error, MAPE)
# 公式:MAPE = (1/n) * Σ|(实际值 - 预测值)/实际值| * 100(百分比形式)
def mean_absolute_percentage_error(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100
2.2 时间序列季节性分解(游戏货币支出)
该步骤使用statsmodels
的seasonal_decompose
函数,将“每日游戏内货币支出”序列分解为原始数据(observed)、趋势(trend)、季节性(seasonal)、残差(residual) 四部分,量化分析时间序列的核心模式。
import statsmodels.api as sm # 导入statsmodels核心库(用于季节性分解)# 对每日游戏内货币支出序列进行季节性分解
# model='additive'表示使用加法模型(适用于季节性波动幅度稳定的序列)
res = sm.tsa.seasonal_decompose(currency.GEMS_GEMS_SPENT)
# 绘制分解结果(包含4个子图:原始数据、趋势、季节性、残差)
res.plot()
# 显示图像
plt.show()
- 分解结果说明:
- observed:原始时间序列数据
- trend:序列的长期趋势(去除季节性和随机波动后的整体变化方向)
- seasonal:序列的季节性波动(固定周期重复的模式)
- residual:残差(去除趋势和季节性后的随机噪声)
2.3 时间序列季节性分解(每小时广告观看量)
该步骤对“每小时广告观看量”序列进行季节性分解,对比日级序列与小时级序列的分解差异,突出高频序列的季节性特征(如日内周期)。
# 对每小时广告观看量序列进行季节性分解(加法模型)
res = sm.tsa.seasonal_decompose(ads.Ads)
# 绘制分解结果
res.plot()
# 显示图像
plt.show()
2.4 时间序列的常见特性
季节性
以固定时间段重复的模式
例如,一个网站在周末可能会收到更多的访问; 这会产生季节性(周期)为7天的数据;
也可能是某个电商平台的每天销售数据,那么这样数据的季节性(周期)是 365.25天。
趋势性
指标的基本趋势
一个日益流行的网站应该显示出一个普遍的趋势;某个电商品类也有有自己的趋势。
周期性
当时间序列数据存在不固定频率的上升和下降时,表示该序列有 周期性 。这些波动经常由经济活动引起,并且与“商业周期”有关。周期波动通常至少持续两年。
季节性和周期性
这两个概念是完全不同的。当数据的波动是无规律时,表示序列存在周期性;如果波动的频率不变并且与固定长度的时间段有关,表示序列存在季节性。一般而言,周期的长度较长,并且周期的波动幅度也更大。
随机性
也称为“噪音”,“不规则”或“余数(remainder)”,这是季节和趋势序列删除后原始时间序列的残差(residuals);比如一场演唱会或者马拉松对地铁流量的影响,这是很难取预测的。
三、全球城市计算AI挑战赛:地铁流量预测
3.1 导入挑战赛所需工具库
该步骤加载地铁流量预测任务所需的库,包括数据处理(numpy/pandas)、文本处理(re/gensim)、特征工程(TF-IDF/SVD)、模型训练(XGBoost/LightGBM)及交叉验证工具,为后续数据处理和建模做准备。
## 数据工具包
import numpy as np
np.random.seed(42) # 设置numpy随机种子,确保结果可复现
import pandas as pd
from tqdm import tqdm # 显示循环进度条(后续数据处理将使用)## 字符串处理工具包(本案例暂未用于文本,可备用)
import string
import re # 正则表达式
import gensim # 文本语义建模(本案例暂未使用)
from collections import Counter # 计数工具(本案例暂未使用)
import pickle # 数据序列化存储(本案例暂未使用)
from nltk.corpus import stopwords # 停用词库(本案例暂未使用)# 特征工程工具:TF-IDF文本特征(本案例暂未使用)、SVD降维、标准化
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import TruncatedSVD
from sklearn.preprocessing import StandardScaler
# 模型评估与验证工具:训练集划分、AUC指标、K折交叉验证
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import KFoldimport warnings
warnings.filterwarnings('ignore') # 屏蔽警告# 梯度提升树模型:XGBoost和LightGBM(本案例规则建模暂未使用,可备用)
import xgboost as xgb
import lightgbm as lgb
from functools import partial # 函数偏应用(本案例暂未使用)# 系统操作工具:文件路径、内存回收、稀疏矩阵合并、时间处理
import os
import gc # 垃圾回收(释放内存)
from scipy.sparse import vstack # 垂直堆叠稀疏矩阵(本案例暂未使用)
import time
import datetime # 日期时间处理(后续特征工程将使用)import joblib # 模型保存与加载(本案例暂未使用)# 多进程处理工具(本案例暂未使用)
import multiprocessing as mp
import seaborn as sns # 统计可视化
%matplotlib inline # 图像内嵌显示
3.2 读取挑战赛数据
该步骤读取地铁流量训练数据(含进站/出站人数)和测试提交模板,其中训练数据包含“站点ID、时间段、进站人数、出站人数”,测试模板用于后续填充预测结果。
# 定义数据路径(根据实际文件位置调整)
path ='./input/' # 读取训练数据(df_data.csv):包含地铁各站点的10分钟级进站/出站人数
train = pd.read_csv(path + 'df_data.csv') # 读取测试提交模板(testA_submit_2019-01-29.csv):需填充2019-01-29日的预测结果
test_A_submit = pd.read_csv(path + 'testA_submit_2019-01-29.csv')
四、地铁流量数据EDA(探索性数据分析)
4.1 基础数据概览(数据结构与类型)
该步骤通过head()
、shape
、info()
函数,查看训练数据的前5行、数据维度(行数×列数)及数据类型,快速了解数据基本特征。
# 查看训练数据的前5行(了解数据列名和格式)
train.head()
输出结果:
stationID | startTime | endTime | inNums | outNums | |
---|---|---|---|---|---|
0 | 0 | 2019-01-01 00:00:00 | 2019-01-01 00:10:00 | 0.0 | 0.0 |
1 | 0 | 2019-01-01 00:10:00 | 2019-01-01 00:20:00 | 0.0 | 0.0 |
2 | 0 | 2019-01-01 00:20:00 | 2019-01-01 00:30:00 | 0.0 | 0.0 |
3 | 0 | 2019-01-01 00:30:00 | 2019-01-01 00:40:00 | 0.0 | 0.0 |
4 | 0 | 2019-01-01 00:40:00 | 2019-01-01 00:50:00 | 0.0 | 0.0 |
# 查看训练数据的维度(行数:314928条记录,列数:5列特征)
train.shape
输出结果:
(314928, 5)
# 查看训练数据的列名、非空值数量及数据类型
# 关键信息:stationID(站点ID,int64)、startTime/endTime(时间段,object)、inNums/outNums(进出站人数,float64)
train.info()
输出结果:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 314928 entries, 0 to 314927
Data columns (total 5 columns):# Column Non-Null Count Dtype
--- ------ -------------- ----- 0 stationID 314928 non-null int64 1 startTime 314928 non-null object 2 endTime 314928 non-null object 3 inNums 314928 non-null float644 outNums 314928 non-null float64
dtypes: float64(2), int64(1), object(2)
memory usage: 12.0+ MB
4.2 缺失值检查
该步骤通过isnull().sum()
统计各列的缺失值数量,确认训练数据无缺失,无需进行缺失值填充。
# 统计每列的缺失值数量(结果均为0,说明无缺失值)
train.isnull().sum()
输出结果:
stationID 0
startTime 0
endTime 0
inNums 0
outNums 0
dtype: int64
4.3 时间特征工程(提取时间维度信息)
该步骤将startTime
(开始时间)解析为日期时间格式,提取“日(day)、小时(hours_in_day)、星期几(day_of_week)、日内10分钟段(ten_minutes_in_day)”等特征,为后续时间序列分析和规则建模提供时间维度支持。
# 将startTime列解析为日期时间格式(便于提取时间特征)
train['time'] = pd.to_datetime(train['startTime'])# 提取“日”(1-31,对应月份中的日期)
train['day'] = train['time'].dt.day # 提取“小时”(0-23,对应一天中的小时)
train['hours_in_day'] = train['time'].dt.hour # 提取“星期几”(0-6,0=星期一,6=星期日)
train['day_of_week'] = train['time'].dt.dayofweek # 提取“日内10分钟段”(0-143,一天24小时×6个10分钟段=144个段)
# 计算逻辑:小时×6 + 分钟//10(如00:00→0,00:10→1,...,23:50→143)
train['ten_minutes_in_day'] = train['hours_in_day'] * 6 + train['time'].dt.minute // 10 # 删除临时的time列(已提取所需时间特征,避免冗余)
del train['time']
4.4 单个站点的日内流量趋势(0号站点1日进站量)
该步骤绘制“0号站点1日(day=1)”的进站人数(inNums)时间序列图,直观展示单个站点在一天内的流量波动(如早晚高峰),为后续多站点趋势对比做铺垫。
# 筛选“0号站点(stationID==20)、1日(day==1)”的数据,按startTime排序
# 绘制进站人数(inNums)的时间序列图,展示日内流量变化
train[train.stationID==20][train.day==1].sort_values('startTime')['inNums'].plot()
4.5 地铁线路连接关系分析(Metro_roadMap)
该步骤读取地铁线路地图数据(Metro_roadMap.csv),该数据为邻接矩阵格式(行/列均为站点ID,值为1表示两站相邻,0表示不相邻),用于后续分析站点连接度(如换乘站、终点站识别)。
# 读取地铁线路地图数据(邻接矩阵格式)
Metro_roadMap = pd.read_csv(path + 'Metro_roadMap.csv')# 查看邻接矩阵的前5行(了解数据格式:行/列对应站点ID,值为0/1表示是否相邻)
Metro_roadMap.head()
输出结果:
Unnamed: 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ... | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 2 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 3 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
4 | 4 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
5 rows × 82 columns
4.6 站点连接度分析(识别换乘站与终点站)
该步骤通过计算邻接矩阵每行的和(即每个站点的相邻站点数量,称为“连接度”),排序后识别换乘站(连接度高)和终点站/始发站(连接度低,通常为1),为后续分析不同类型站点的流量特征提供依据。
# 定义站点ID列表(0-80,共81个站点)
lines = [str(i) for i in range(81)] # 计算每个站点的连接度(每行求和:1的数量即相邻站点数),按连接度升序排序,查看前20个站点
# 关键发现:0、66、67、33、34、27等站点连接度为1(终点站/始发站);10、5、51等站点连接度高(换乘站)
Metro_roadMap[lines].sum(axis=1).sort_values()[:20]
输出结果:
0 1
66 1
67 1
33 1
34 1
27 1
49 2
60 2
59 2
58 2
57 2
56 2
55 2
54 2
53 2
61 2
48 2
47 2
45 2
44 2
dtype: int64
4.7 单站点多日流量趋势对比(0号站点)
该步骤选取“0号站点”,绘制1月21-25日(工作日)和28日(下周一)的进站人数(inNums)时间序列,观察多日流量分布的相似性——发现工作日流量模式高度一致,28日与21日(上周一同期)分布相近,为后续“基于同期数据的规则建模”提供依据。
import matplotlib.pyplot as plt# 设置图像大小(宽8,高8)
plt.figure(figsize=[8,8])
# 设置x轴标签(时间)
plt.xlabel('Time')
# 设置y轴标签(进站人数)
plt.ylabel('Count')# 遍历目标日期(21-25日为上周工作日,28日为下周一)
for day in [21,22,23,24,25,28]:# 筛选“0号站点、目标日期”的数据tmp = train.loc[(train.day == day) & (train.stationID == 0)].copy()# 按站点ID和开始时间排序(确保时间序列顺序正确)tmp = tmp.sort_values(['stationID','startTime'])# 绘制该日期的进站人数时间序列plt.plot(tmp['startTime'],tmp['inNums'])
4.8 同类站点流量趋势验证(1号站点)
该步骤重复0号站点的分析逻辑,绘制1号站点21-25日、28日的进站人数趋势,验证“工作日流量模式一致、同期数据相似”的规律在其他站点同样成立,进一步支撑规则建模的合理性。
# 设置图像大小(宽8,高8)
plt.figure(figsize=[8,8])
# 设置x轴标签(时间)
plt.xlabel('Time')
# 设置y轴标签(进站人数)
plt.ylabel('Count')# 遍历目标日期
for day in [21,22,23,24,25,28]:# 筛选“1号站点、目标日期”的数据tmp = train.loc[(train.day == day) & (train.stationID == 1)].copy()# 按站点ID和开始时间排序tmp = tmp.sort_values(['stationID','startTime'])# 绘制进站人数时间序列plt.plot(tmp['startTime'],tmp['inNums'])
4.9 高流量站点识别(误差敏感站点)
该步骤统计21-25日、28日各站点的日均进站/出站总人数,按降序排序后识别高流量站点(如15号、9号、4号站点)。这类站点的流量预测误差对整体模型性能影响更大,需在后续建模中重点关注。
# 统计21-25日、28日各站点每日的进站总人数,按降序排序(查看前25条,识别高流量站点)
train.loc[train.day.isin([21,22,23,24,25,28])].groupby(['stationID','day'])['inNums'].sum().sort_values(ascending = False)[:25]
输出结果:
stationID day
15 25 95389.022 87758.024 87414.023 87108.021 87069.028 79595.0
9 24 55736.025 55585.023 54981.028 53287.022 52837.021 52206.0
4 25 38730.021 38088.024 37990.023 37795.022 37398.028 35810.0
7 25 34810.028 33993.023 33548.022 33509.021 33412.024 33195.0
10 25 32852.0
Name: inNums, dtype: float64
# 统计21-25日、28日各站点每日的出站总人数,按降序排序(补充高流量站点识别)
train.loc[train.day.isin([21,22,23,24,25,28])].groupby(['stationID','day'])['outNums'].sum().sort_values(ascending = False)[:25]
输出结果:
stationID day
15 28 107870.0
25 107672.0
23 97706.0
24 96199.0
22 96153.0
21 94349.0
9 25 56068.0
24 55576.0
23 55170.0
28 53516.0
7 25 53201.0
28 53168.0
9 22 52478.0
21 52342.0
7 24 49400.0
23 47418.0
22 44601.0
21 43428.0
4 21 40142.0
24 39737.0
25 39597.0
23 39348.0
22 39285.0
28 36844.0
10 25 31788.0
Name: outNums, dtype: float64
4.10 高流量站点趋势分析(15号站点)
该步骤以15号站点(进站/出站流量最高)为例,分别绘制21-25日、28日的进站/出站人数趋势,观察高流量站点的时间分布特征——发现28日峰值略低于21日(可能受春运返程影响),但整体模式一致,为后续规则建模的“流量衰减系数”提供依据。
# 设置图像大小(宽8,高8)
plt.figure(figsize=[8,8])
# 设置x轴标签(时间)
plt.xlabel('Time')
# 设置y轴标签(进站人数)
plt.ylabel('Count')# 遍历目标日期,绘制15号站点进站人数趋势
for day in [21,22,23,24,25,28]:tmp = train.loc[(train.day == day) & (train.stationID == 15)].copy()tmp = tmp.sort_values(['stationID','startTime'])plt.plot(tmp['startTime'],tmp['inNums'])
# 设置图像大小(宽8,高8)
plt.figure(figsize=[8,8])
# 设置x轴标签(时间)
plt.ylabel('Count')# 遍历目标日期,绘制15号站点出站人数趋势
for day in [21,22,23,24,25,28]:tmp = train.loc[(train.day == day) & (train.stationID == 15)].copy()tmp = tmp.sort_values(['stationID','startTime'])plt.plot(tmp['startTime'],tmp['outNums'])
4.11 同期流量对比(15号站点21日vs28日)
该步骤将15号站点21日(上周周一)和28日(本周周一)的进站/出站人数按“小时”聚合,绘制对比图,直观展示同期流量的差异——28日进站量整体低于21日,出站量部分时段高于21日,但时间分布模式一致,为规则建模的“小时级比例映射”提供支撑。
# 筛选15号站点21日和28日的数据
tmp = train.loc[(train.day.isin([21,28])) & (train.stationID == 15)]
# 按“小时(hours_in_day)”和“日期(day)”聚合,计算每小时进站人数均值
# 绘制对比图(o-表示带圆点的折线)
tmp.pivot_table(index='hours_in_day',columns='day',values='inNums').plot(style='o-')
# 筛选15号站点21日和28日的数据
tmp = train.loc[(train.day.isin([21,28])) & (train.stationID == 15)]
# 按“小时”和“日期”聚合,计算每小时出站人数均值
# 绘制对比图
tmp.pivot_table(index='hours_in_day',columns='day',values='outNums').plot(style='o-')
4.12 高流量站点趋势验证(9号站点)
该步骤重复15号站点的分析逻辑,绘制9号站点(第二高流量)的多日进站/出站趋势及21日vs28日小时级对比,进一步验证“同期流量模式一致、存在小幅衰减”的规律,确保规则建模适用于多个高流量站点。
# 设置图像大小(宽8,高8)
plt.figure(figsize=[8,8])
plt.xlabel('Time')
plt.ylabel('Count')# 绘制9号站点21-25日、28日进站人数趋势
for day in [21,22,23,24,25,28]:tmp = train.loc[(train.day == day) & (train.stationID == 9)].copy()tmp = tmp.sort_values(['stationID','startTime'])plt.plot(tmp['startTime'],tmp['inNums'])
# 设置图像大小(宽8,高8)
plt.figure(figsize=[8,8])
plt.xlabel('Time')
plt.ylabel('Count')# 绘制9号站点21-25日、28日出站人数趋势
for day in [21,22,23,24,25,28]:tmp = train.loc[(train.day == day) & (train.stationID == 9)].copy()tmp = tmp.sort_values(['stationID','startTime'])plt.plot(tmp['startTime'],tmp['outNums'])
# 筛选9号站点21日和28日的数据,按小时聚合进站人数,绘制对比图
tmp = train.loc[(train.day.isin([21,28])) & (train.stationID == 9)]
tmp.pivot_table(index='hours_in_day',columns='day',values='inNums').plot(style='o-')
# 筛选9号站点21日和28日的数据,按小时聚合出站人数,绘制对比图
tmp = train.loc[(train.day.isin([21,28])) & (train.stationID == 9)]
tmp.pivot_table(index='hours_in_day',columns='day',values='outNums').plot(style='o-')
五、规则建模(基于时间序列相似性的流量预测)
5.1 计算上周10分钟级均值(21-25日)
该步骤以21-25日(上周工作日)为基准,按“站点ID、小时、日内10分钟段”聚合,计算每个10分钟段的平均进站/出站人数(in_mean
/out_mean
),消除单日随机波动,提取稳定的10分钟级流量模式。
# 筛选21-25日(上周工作日)的数据
t21_25 = train[(train['day']>=21)&(train['day']<=25)]# 按“站点ID、小时(hours_in_day)、日内10分钟段(ten_minutes_in_day)”聚合,计算进站人数均值
t21_25_in = t21_25.groupby(['stationID', 'hours_in_day', 'ten_minutes_in_day'])['inNums'].mean().reset_index()
# 按相同维度聚合,计算出站人数均值
t21_25_out = t21_25.groupby(['stationID', 'hours_in_day', 'ten_minutes_in_day'])['outNums'].mean().reset_index()# 重命名列名(明确为“均值”)
t21_25_in.columns = ['stationID', 'hours_in_day', 'ten_minutes_in_day', 'in_mean']
t21_25_out.columns = ['stationID', 'hours_in_day', 'ten_minutes_in_day', 'out_mean']# 合并进站和出站均值数据(按站点、小时、10分钟段对齐)
t21_25_in_out = t21_25_in.merge(t21_25_out,on = ['stationID', 'hours_in_day', 'ten_minutes_in_day'],how = 'left')# 查看合并后的数据前5行
t21_25_in_out.head()
输出结果:
stationID | hours_in_day | ten_minutes_in_day | in_mean | out_mean | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0.0 | 1.8 |
1 | 0 | 0 | 1 | 0.0 | 0.8 |
2 | 0 | 0 | 2 | 0.0 | 0.0 |
3 | 0 | 0 | 3 | 0.4 | 0.0 |
4 | 0 | 0 | 4 | 0.2 | 0.2 |
5.2 计算上周小时级均值(21-25日)
该步骤按“站点ID、小时”聚合21-25日数据,计算每个小时的平均进站/出站人数(in_mean_hour
/out_mean_hour
),用于后续计算“10分钟段占小时的比例”(小时级流量更稳定,可降低随机波动影响)。
# 按“站点ID、小时”聚合,计算21-25日每小时进站人数均值
t21_25_in_hour = t21_25.groupby(['stationID', 'hours_in_day'])['inNums'].mean().reset_index()
# 按相同维度聚合,计算每小时出站人数均值
t21_25_out_hour = t21_25.groupby(['stationID', 'hours_in_day'])['outNums'].mean().reset_index()# 重命名列名(明确为“小时均值”)
t21_25_in_hour.columns = ['stationID', 'hours_in_day', 'in_mean_hour']
t21_25_out_hour.columns = ['stationID', 'hours_in_day', 'out_mean_hour']# 合并小时级进站和出站均值数据
t21_25_in_out_hour = t21_25_in_hour.merge(t21_25_out_hour,on = ['stationID', 'hours_in_day'],how = 'left')# 查看合并后的数据前5行
t21_25_in_out_hour.head()
输出结果:
stationID | hours_in_day | in_mean_hour | out_mean_hour | |
---|---|---|---|---|
0 | 0 | 0 | 0.100000 | 0.466667 |
1 | 0 | 1 | 0.000000 | 0.000000 |
2 | 0 | 2 | 0.000000 | 0.000000 |
3 | 0 | 3 | 0.000000 | 0.000000 |
4 | 0 | 4 | 0.033333 | 0.033333 |
5.3 计算10分钟段占小时的比例
该步骤将10分钟级均值(in_mean
/out_mean
)除以对应小时的均值(in_mean_hour
/out_mean_hour
),得到“10分钟段占小时的比例”(in_ratio
/out_ratio
)。该比例反映了小时内流量的时间分布模式,可用于后续将28日(本周一)的小时级流量映射到10分钟级。
# 合并10分钟级均值和小时级均值数据(对齐站点、小时维度)
t21_25_in_out = t21_25_in_out.merge(t21_25_in_out_hour,on = ['stationID', 'hours_in_day'],how = 'left')# 计算进站10分钟段占小时的比例(+0.001避免除零错误)
t21_25_in_out['in_ratio'] = t21_25_in_out['in_mean'] / (t21_25_in_out['in_mean_hour']+0.001)
# 计算出站10分钟段占小时的比例
t21_25_in_out['out_ratio'] = t21_25_in_out['out_mean'] / (t21_25_in_out['out_mean_hour']+0.001)# 提取核心列(站点ID、小时、10分钟段、进站比例、出站比例),用于后续建模
t21_25_in_out_ratio = t21_25_in_out[['stationID', 'hours_in_day', 'ten_minutes_in_day','in_ratio','out_ratio']]# 查看比例数据
t21_25_in_out_ratio
输出结果:
stationID | hours_in_day | ten_minutes_in_day | in_ratio | out_ratio | |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0.000000 | 3.848895 |
1 | 0 | 0 | 1 | 0.000000 | 1.710620 |
2 | 0 | 0 | 2 | 0.000000 | 0.000000 |
3 | 0 | 0 | 3 | 3.960396 | 0.000000 |
4 | 0 | 0 | 4 | 1.980198 | 0.427655 |
... | ... | ... | ... | ... | ... |
11659 | 80 | 23 | 139 | 1.405591 | 2.050373 |
11660 | 80 | 23 | 140 | 1.218179 | 1.215036 |
11661 | 80 | 23 | 141 | 0.281118 | 0.607518 |
11662 | 80 | 23 | 142 | 0.187412 | 0.151880 |
11663 | 80 | 23 | 143 | 0.468530 | 0.303759 |
11664 rows × 5 columns
5.4 比例异常值压缩(凌晨时段)
该步骤定义函数function
,对凌晨6点前(hours_in_day <=6
)比例大于2的异常值进行压缩(强制设为2)。原因:凌晨流量极小,小时级均值接近0,易导致比例异常放大,而凌晨流量对整体预测误差影响小,压缩后可避免预测值失真。
# 定义异常值压缩函数:凌晨6点前(hours_in_day <=6)比例>2时,强制设为2;其他情况保留原比例
def function(a, b):if a>=2 and b <=6: return 2else:return a# 对进站比例进行异常值压缩
t21_25_in_out_ratio['in_ratio'] = t21_25_in_out_ratio.apply(lambda x: function(x.in_ratio, x.hours_in_day), axis = 1)
# 对出站比例进行异常值压缩
t21_25_in_out_ratio['out_ratio'] = t21_25_in_out_ratio.apply(lambda x: function(x.out_ratio, x.hours_in_day), axis = 1)
5.5 计算28日小时级均值(基准日流量)
该步骤筛选28日(本周一,预测日29日的前一天)数据,按“站点ID、小时”聚合,计算每小时的平均进站/出站人数(in28_mean_hour
/out28_mean_hour
)。28日与29日时间相近(连续两天),小时级流量更具参考性,作为预测的基准流量。
# 筛选28日(本周一)的数据
t28 = train[(train['day']==28)]# 按“站点ID、小时”聚合,计算28日每小时进站人数均值
t28_in_hour = t28.groupby(['stationID', 'hours_in_day'])['inNums'].mean().reset_index()
# 按相同维度聚合,计算28日每小时出站人数均值
t28_out_hour = t28.groupby(['stationID', 'hours_in_day'])['outNums'].mean().reset_index()# 重命名列名(明确为“28日小时均值”)
t28_in_hour.columns = ['stationID', 'hours_in_day', 'in28_mean_hour']
t28_out_hour.columns = ['stationID', 'hours_in_day', 'out28_mean_hour']# 合并28日小时级进站和出站均值数据
t28_in_out_hour = t28_in_hour.merge(t28_out_hour,on = ['stationID', 'hours_in_day'],how = 'left')# 将28日小时级均值合并到28日原始数据中(对齐站点、小时维度)
t28 = t28.merge(t28_in_out_hour,on = ['stationID', 'hours_in_day'],how = 'left')# 提取核心列(站点ID、小时、10分钟段、当日10分钟级流量、小时级均值),用于后续匹配
t_28 = t28[['stationID', 'hours_in_day', 'ten_minutes_in_day','inNums','outNums','in28_mean_hour','out28_mean_hour']]
5.6 构建预测数据集(匹配测试集与特征)
该步骤读取测试提交模板(需预测29日数据),提取测试集中的时间特征(小时、10分钟段),并将其与28日小时级均值、上周10分钟段比例数据合并,为最终计算29日10分钟级流量搭建数据框架。
# 读取测试提交模板(2019-01-29日),删除模板中默认的inNums和outNums列(后续填充预测值)
sub29=pd.read_csv('./input/testA_submit_2019-01-29.csv')
del sub29['inNums']
del sub29['outNums']# 从测试集startTime提取“小时”特征(用于匹配28日小时级均值)
sub29['hours_in_day'] = pd.to_datetime(sub29['startTime'],format='%Y-%m-%d %H:%M:%S').dt.hour
# 从测试集startTime提取“日内10分钟段”特征(用于匹配上周10分钟段比例)
sub29['ten_minutes_in_day']= pd.to_datetime(sub29['startTime'],format='%Y-%m-%d %H:%M:%S').dt.minute // 10 # 分钟//10得到0-5的段编号# 第一步:将测试集与28日数据合并,获取28日小时级均值(in28_mean_hour/out28_mean_hour)
sub29=sub29.merge(t_28,on = ['stationID', 'hours_in_day', 'ten_minutes_in_day'],how = 'left')
# 第二步:将测试集与上周10分钟段比例合并,获取in_ratio/out_ratio
sub29=sub29.merge(t21_25_in_out_ratio,on = ['stationID', 'hours_in_day', 'ten_minutes_in_day'],how = 'left')# 填充合并过程中可能产生的缺失值(若有站点-时间组合无历史数据,默认设为0)
sub29=sub29.fillna(0)
5.7 计算29日预测值(比例映射+衰减调整)
该步骤基于“时间序列相似性”核心逻辑计算预测值:
29日10分钟级流量 = 上周10分钟段比例 × 28日小时级均值 × 0.95
其中×0.95是考虑春运返程背景下,29日(周二)流量可能比28日(周一)小幅衰减,通过经验系数调整预测合理性。
# 计算29日进站人数预测值:上周10分钟段进站比例 × 28日进站小时级均值 × 0.95(衰减系数)
sub29['in29'] = sub29['in_ratio'] * sub29['in28_mean_hour'] * 0.95
# 计算29日出站人数预测值:上周10分钟段出站比例 × 28日出站小时级均值 × 0.95
sub29['out29']= sub29['out_ratio'] * sub29['out28_mean_hour'] * 0.95# 提取测试提交所需列(stationID、startTime、endTime、预测的inNums/outNums)
submition=sub29[['stationID','startTime','endTime','in29','out29']]
# 重命名预测列,匹配提交模板要求(in29→inNums,out29→outNums)
submition=submition.rename(columns = {'in29':'inNums'})
submition=submition.rename(columns = {'out29':'outNums'})# (可选)保存预测结果为CSV文件,用于提交
# submition.to_csv('submission_20190129.csv', index=False)
六、总结与补充说明
6.1 规则建模核心逻辑回顾
本案例采用“基于时间序列相似性的规则建模”,核心假设与步骤如下:
- 相似性假设:工作日地铁流量具有稳定的时间分布模式(如早晚高峰时段固定),且相邻日期(如28日与29日)的小时级流量趋势相近。
- 数据平滑:用上周(21-25日)10分钟级均值消除单日随机波动,提取稳定的“小时内流量分布比例”。
- 基准映射:以28日(预测日前一天)小时级均值为基准,通过“比例映射”将小时级流量拆分为10分钟级流量。
- 场景调整:引入0.95衰减系数,适配春运返程期间流量逐步下降的实际场景,提升预测合理性。
6.2 模型优缺点与改进方向
维度 | 优点 | 缺点 | 改进方向 |
---|---|---|---|
稳定性 | 基于历史统计规律,避免过拟合 | 对突发情况(如极端天气、大型活动)不敏感 | 加入实时数据(如天气、事件日历)作为修正特征 |
可解释性 | 比例映射逻辑直观,结果可追溯 | 依赖经验系数(如0.95),主观性较强 | 用线性回归或时间序列模型(如ARIMA)优化衰减系数 |
效率 | 无需复杂训练,计算速度快 | 未利用站点拓扑关系(如换乘站联动效应) | 加入站点连接度、相邻站点流量等空间特征 |
泛化性 | 适用于流量模式稳定的场景(如工作日) | 节假日流量预测误差较大 | 按日期类型(工作日/周末/节假日)分别建模 |
6.3 扩展工具与技术选型建议
若需提升预测精度,可结合以下技术:
- 时间序列模型:使用ARIMA、SARIMA(处理季节性)、Prophet(Facebook开源,适配节假日)等模型,捕捉长期趋势与周期性。
- 机器学习模型:用XGBoost/LightGBM构建集成模型,输入特征包括:
- 时间特征:小时、星期几、是否节假日、是否月初/月末;
- 历史特征:前1天/前7天同期流量、滚动均值(如24小时滑动平均);
- 空间特征:站点连接度、相邻站点流量、站点所在区域(如商业区/住宅区)。
- 深度学习模型:用LSTM/Transformer处理时序数据,尤其适合长序列、多站点联动的复杂场景。
上一章:【竞赛系列】机器学习实操项目06——客户信用评估模型进阶流程(特征重要性分析与稳定性监控)
下一章:
机器学习核心知识点目录:机器学习核心知识点目录
机器学习实战项目目录:【从 0 到 1 落地】机器学习实操项目目录:覆盖入门到进阶,大学生就业 / 竞赛必备