├── betaencoder.py
├── README.md
├── other.py
└── main.py
/betaencoder.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # @Author: zhushuai
3 | # @Date: 2019-04-02 12:02:01
4 | # @Last Modified by: zhushuai
5 | # @Last Modified time: 2019-04-02 12:03:41
6 | # 定义目标编码函数
7 |
8 | class BetaEncoder(object):
9 |
10 | def __init__(self, group):
11 |
12 | self.group = group
13 | self.stats = None
14 |
15 | # get counts from df
16 | def fit(self, df, target_col):
17 | self.prior_mean = np.mean(df[target_col])
18 | stats = df[[target_col, self.group]].groupby(self.group)
19 | stats = stats.agg(['sum', 'count'])[target_col]
20 | stats.rename(columns={'sum': 'n', 'count': 'N'}, inplace=True)
21 | stats.reset_index(level=0, inplace=True)
22 | self.stats = stats
23 |
24 | # extract posterior statistics
25 | def transform(self, df, stat_type, N_min=1):
26 |
27 | df_stats = pd.merge(df[[self.group]], self.stats, how='left')
28 | n = df_stats['n'].copy()
29 | N = df_stats['N'].copy()
30 |
31 | # fill in missing
32 | nan_indexs = np.isnan(n)
33 | n[nan_indexs] = self.prior_mean
34 | N[nan_indexs] = 1.0
35 |
36 | # prior parameters
37 | N_prior = np.maximum(N_min - N, 0)
38 | alpha_prior = self.prior_mean * N_prior
39 | beta_prior = (1 - self.prior_mean) * N_prior
40 |
41 | # posterior parameters
42 | alpha = alpha_prior + n
43 | beta = beta_prior + N - n
44 |
45 | # calculate statistics
46 | if stat_type == 'mean':
47 | num = alpha
48 | dem = alpha + beta
49 |
50 | elif stat_type == 'mode':
51 | num = alpha - 1
52 | dem = alpha + beta - 2
53 |
54 | elif stat_type == 'median':
55 | num = alpha - 1 / 3
56 | dem = alpha + beta - 2 / 3
57 |
58 | elif stat_type == 'var':
59 | num = alpha * beta
60 | dem = (alpha + beta) ** 2 * (alpha + beta + 1)
61 |
62 | elif stat_type == 'skewness':
63 | num = 2 * (beta - alpha) * np.sqrt(alpha + beta + 1)
64 | dem = (alpha + beta + 2) * np.sqrt(alpha * beta)
65 |
66 | elif stat_type == 'kurtosis':
67 | num = 6 * (alpha - beta) ** 2 * (alpha + beta + 1) - alpha * beta * (alpha + beta + 2)
68 | dem = alpha * beta * (alpha + beta + 2) * (alpha + beta + 3)
69 |
70 | else:
71 | num = self.prior_mean
72 | dem = np.ones_like(N_prior)
73 |
74 | # replace missing
75 | value = num / dem
76 | value[np.isnan(value)] = np.nanmedian(value)
77 | return value
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
地铁乘客流量预测
2 |
3 |
4 | Table of Contents
5 | =================
6 |
7 | * [地铁乘客流量预测](#title)
8 | * [1. 赛题分析和前期思路](#1)
9 | * [1.1 数据清洗](#1.1)
10 | * [1.2 特征工程](#1.2)
11 | * [1.3 划分训练集和测试集](#1.3)
12 | * [1.4 搭建模型预测](#1.4)
13 | * [2. 其他的一些想法](#2)
14 | * [3. 总结与思考](#3)
15 |
16 |
17 | > **竞赛题目**
18 |
19 | 通过分析地铁站的历史刷卡数据,预测站点未来的客流量变化。开放了20190101至20190125共25天的刷卡记录,共涉及3条线路81个地铁站约7000万条数据作为训练数据`Metro_train.zip`,供选手搭建地铁站点乘客流量预测模型。同时大赛提供了路网地图,即各个地铁站之间的连接关系表,存储在文件`Metro_roadMap.csv`文件中。
20 |
21 | 测试阶段,提供某天所有线路所有站点的刷卡记录数据,预测未来一天00时至24时以10分钟为单位的各时段各站点的进站和出站人次。
22 |
23 | 测试集A集上,提供2019年1月28日的刷卡数据`testA_record_2019-01-28.csv`,选手需对2019年1月29日全天各地铁站以10分钟为单位的人流量进行预测。
24 |
25 | 评估指标采用**平均绝对误差`Mean Absolute Error, MAE`**,分别对入站人数和出站人数预测结果进行评估,然后在对两者取平均,得到最终评分。
26 |
27 | 关于数据的具体描述以及说明[详见天池官网]()。
28 |
29 |
30 |
31 | # 1. 赛题分析和前期思路
32 |
33 | 比赛提供了1号到25号共25天的刷卡记录数据,所以第一步就是对每一天的文件进行处理。原始数据集中包含了`time`, `lineID`, `stationID`, `deviceID`, `status`, `userID`, `payType`这几个列,根据题目要求要预测进站和出站的人流量,所以要先统计出每一天的进站和出站流量。
34 |
35 | ## 1.1 数据清洗
36 |
37 | > **提取基础信息**
38 |
39 | 首先对时间信息进行处理,提取出日、周、时、分、秒的信息,由于是按照10分钟为间隔统计,所以在提取分钟信息的时候只需要取整十。接着总计80个站点(除去缺失数据的54站),每个站点从0点到24点,以10分钟为一次单位,总计144段时间间隔。根据站点、日、时、分进行分组统计,得到每个时段的进站人数和出站人数。
40 |
41 | 经过第一轮处理之后,得到了每一天的文件包含的`columns`有:`stationID`, `weekday`, `is_holiday`, `day`, `hour`, `minute`, `time_cut`, `inNums`以及`outNums`这几列。
42 |
43 | 增加了一些和刷卡设备相关的特征,包括`nuni_deveiceID_of_stationID`, `nuni_deviceID_of_stationID_hour`, `nuni_deviceID_of_stationID_hour_minute`。
44 |
45 |
46 |
47 | ## 1.2 特征工程
48 |
49 | > **增加同一站点相邻时间段的进出站流量信息**
50 |
51 | 考虑到当前时刻的流量信息与前后时刻的流量信息存在一定的关系,所以将当前时间段的前两个时段以及后两个时段流量信息作为特征。
52 |
53 | 增加的特征包括`inNums_before1`, `inNums_before2`, `inNums_after1`, `inNums_after2`, `outNums_before1`, `outNums_before2`, `outNums_after1`, `outNums_after2`。
54 |
55 |
56 |
57 | > **增加乘车高峰时段相关的特征**
58 |
59 | 根据杭州地铁的运营时段信息,将高峰时段分为四类,0表示非运营时间段,1表示非高峰时间段,2表示高峰时间段,3表示特殊高峰时间段。由于周末和非周末的高峰时间存在一定的差异,所以需要分别计算。
60 |
61 | 增加了特征`peak_type`。
62 |
63 |
64 |
65 | > **增加同周次的进出站流量信息**
66 |
67 | 均值、最大值以及最小值在一定程度上反映了数据的分布信息,所以增加同周次进站流量和出站流量的均值、最大值以及最小值作为特征。
68 |
69 | 增加的特征包括`inNums_whm_max`, `inNums_whm_min`, `inNums_whm_mean`, `outNums_whm_max`, `outNums_whm_min`, `outNums_whm_mean`, `inNums_wh_max`, `inNums_wh_min`, `inNums_wh_mean`, `outNums_wh_mean`。
70 |
71 |
72 |
73 | > **增加线路信息**
74 |
75 | 根据某一天的刷卡记录表统计每条线路和站点的对应信息,并计算各条线路的站点数,用站点数量代表该站的线路信息。
76 |
77 | 增加特征`line`。
78 |
79 |
80 |
81 | > **增加站点的类型信息**
82 |
83 | 不同的站点属于不同的类型,比如起点站、终点站、换乘站、普通站等,而这些站点的类别信息可以通过邻站点的数量表示,所以根据路网图对邻站点的数量进行统计,表示各个站点的类别。
84 |
85 | 增加特征`station_type`。
86 |
87 |
88 |
89 | > **增加特殊站点的标记**
90 |
91 | 对站点流量的分析过程中,发现第15站的流量与其他站点存在明显的区别,全天都处于高峰状态,因此给15站添加特别的标记。
92 |
93 | 增加特征`is_special`。
94 |
95 |
96 |
97 | > **连接训练特征和目标值**
98 |
99 | 本次建模的思想使用前一天的流量特征和时间特征,以及预测当天的时间特征,来预测进站流量和出站流量。所以要对之前处理好的数据集进行拼接。
100 |
101 | 增加新的特征`yesterday_is_holiday`以及`today_is_holiday`,增加目标值列`inNums`和`outNums`。
102 |
103 |
104 |
105 | > **对时间间隔进行目标编码**
106 |
107 | 考虑时间间隔信息与进站、出站流量的相关性,对时间间隔信息针对`inNums`和`outNums`进行目标编码。(这一步并非必须的,一定程度上可能会导致过拟合,所以可以考虑加入和不加入的情况都测试一下)
108 |
109 | 目标编码后得到了`in_time_cut`和`out_time_cut`。
110 |
111 |
112 |
113 | ## 1.3 划分训练集和测试集
114 |
115 | 根据上面数据清洗以及特征工程得到的结果对数据集进行划分。
116 |
117 |
118 |
119 | ## 1.4 搭建模型预测
120 |
121 | 我们使用了LightGBM和CatBoost两个模型预测并取其均值,其实也可以尝试加入XGBoost,然后取3个模型的加权平均,但是我们当时训练时发现XGBoost得到的结果不是很好,所以直接丢掉了。其实,通过加权平均,给XGBoost的结果一个比较好的权重,也有可能会得到比较不错的结果。最后,**对模型结果平均**。
122 |
123 |
124 |
125 | # 2. 其他的一些想法
126 |
127 | (1) 由于官方给了路网图,所以我们尝试将路网图拼接在特征后面,表示各个站点之间的连接关系,但是这样反而降低了模型最终的性能。
128 |
129 |
130 |
131 | (2) 过程中我们一直想充分利用邻站点的信息,想在邻站点上提取尽可能多的特征,包括邻站点相同时刻以及相邻时刻的流量信息,但是这样都会降低模型的性能,也在这上面浪费了不少的时间。
132 |
133 |
134 |
135 | (3) 除了从以后的数据中提取特征之外,我们还对提出出来的特征做了一些特征工程,包括计算一些可能有一定关联的特征**计算加减以及比率信息**,但是这些工作都没有能够提升模型的性能。
136 |
137 |
138 |
139 | (4) 根据官方交流群的讨论,我们在A榜的时候尝试去掉周末的信息,只讲工作日的信息进行提取和拼接,这在一定程度上提升了模型的效果,后来我们在[鱼的代码]()基础上加入了我们之前找到的一些特征。之后,仔细推敲了一下鱼的代码,发现在进行特征`feature`和目标值`target`拼接的时候,和`deviceID`相关的特征使用的是预测当天的,这样一方面会导致leak,另一方面就是最后的测试集这些特征都是`nan`值,也就是说在最终的预测中没有起到作用。于是,我们改变了拼接的方法,再次加入了自己提取的特征,最终在A榜跑出的成绩是`12.99`。注意,**这里使用的数据并不是全部的数据,而只是使用了工作日的数据拼接**。
140 |
141 |
142 |
143 | 在B榜的时候,我们还是希望能够训练一个通用的模型,所以这次把所有的数据放在一起训练,并没有将周末的数据单独提取出来,但是在滑窗的时候使用了`13`和`20`两天的数据作为验证,最终跑出了`12.57`的成绩,说明这种想法是可行的。为了验证一下只提取周末信息的效果,我们也尝试把周末的信息单独提取出来,最后得分一直在`14`以上,可能也是因为我们提的特征不太适用于这种场景。
144 |
145 |
146 |
147 | 最终,我们使用了全部的数据通过Stacking的方法进行了训练,将三个梯度提升树模型进行了堆叠,最后得到的结果也是`14`多一点。
148 |
149 |
150 |
151 | # 3. 总结与思考
152 |
153 | (1) 首先是对数据的EDA做的不够,包括对各个站点的分析,各个时间段综合分析,对特征重要性的分析等等。主要还是因为经验不够,不知道该怎么做,甚至15站点的特殊性也是从交流群里得到的信息。另外,就是调参做的有问题,反而把模型的性能调低了,说明对参数的理解不够。
154 |
155 |
156 |
157 | (2) 第一次团队作战,不知道该怎么协作,分工不是很明确,所以效率不是很高,没能有机会尝试更多的模型。代码写的不规范,导致后面修改的时候浪费了比较多的时间,包括整理也花了不少的时间。
158 |
159 |
160 |
161 | (3) 知道的模型太少,只使用了梯度提升树模型,其实还有很多可能有效的模型可以尝试,包括图神经网络,时空模型以及LSTM,但是都因为不够熟悉而无从入手。
162 |
163 |
164 |
165 | (4) 总结:
166 |
167 | - 了解自己能做的事情,明确分工;
168 | - 做好EDA,做到对数据的充分理解;
169 | - 代码书写规范,每一个功能模块应该定义为一个函数;
170 | - 熟悉不同模型的功能以及试用场景。
171 |
172 |
--------------------------------------------------------------------------------
/other.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # @Author: zhushuai
3 | # @Date: 2019-04-02 13:08:20
4 | # @Last Modified by: zhushuai
5 | # @Last Modified time: 2019-04-02 13:11:13
6 |
7 | import xgboost as xgb
8 | from sklearn.metrics import mean_absolute_error
9 | from sklearn.base import BaseEstimator, TransformerMixin, RegressorMixin, clone
10 |
11 | # 定义XGB模型
12 |
13 | # 设置模型参数
14 | xgb_params = {
15 | 'booster': 'gbtree',
16 | 'objective': 'reg:linear',
17 |
18 | 'eval_metric': 'mae',
19 |
20 | 'learning_rate': 0.0894,
21 | 'max_depth': 9,
22 | 'max_leaves': 20,
23 |
24 | 'lambda': 2,
25 | 'alpha': 1,
26 | 'subsample': 0.8,
27 | 'colsample_bytree': 0.8,
28 | 'silent': 1,
29 |
30 | 'gpu_id': 0,
31 | 'tree_method': 'gpu_hist'
32 | }
33 |
34 | ## 预测进站流量
35 | in_xgb_pred = np.zeros(len(X_test))
36 | X_data = train_data[features].values
37 | y_data = train_data['inNums'].values
38 |
39 |
40 | for i, date in enumerate(slip):
41 | train = train_data[train_data.day=6 ).astype(int)
50 | train_df['hour'] = train_df['time'].dt.hour
51 | date = str(train_df.month.values[0])+'-'+str(train_df.day.values[0])
52 | if date in w2h or (date not in h2w and train_df.weekday.values[0] in [6,7]):
53 | train_df['is_holiday'] = 1
54 | else:
55 | train_df['is_holiday'] = 0
56 |
57 | train_final = train_df.groupby(['stationID', 'weekday', 'is_holiday', 'day', 'hour', 'minute']).status.agg(['count', 'sum']).reset_index()
58 | # 这一段参考yu的代码
59 | # 考虑和刷卡设备相关的特征
60 | # 每个站点的刷卡设备数量与该站点的人流量存在一定的关系
61 | # 每个时段的刷卡设备数与流量也存在一定的关系
62 | tmp = train_df.groupby(['stationID'])['deviceID'].nunique().reset_index(name='nuni_deviceID_of_stationID')
63 | train_final = train_final.merge(tmp, on=['stationID'], how='left')
64 | tmp = train_df.groupby(['stationID','hour'])['deviceID'].nunique().reset_index(name='nuni_deviceID_of_stationID_hour')
65 | train_final = train_final.merge(tmp, on=['stationID', 'hour'], how='left')
66 | tmp = train_df.groupby(['stationID','hour','minute'])['deviceID'].nunique().\
67 | reset_index(name='nuni_deviceID_of_stationID_hour_minute')
68 | train_final = train_final.merge(tmp, on=['stationID','hour','minute'], how='left')
69 |
70 | train_final['time_cut'] = train_final['hour'] * 6 + train_final['minute'] // 10
71 | train_final['inNums'] = train_final['sum']
72 | train_final['outNums'] = train_final['count'] - train_final['sum']
73 | del train_final['sum'], train_final['count']
74 |
75 | return train_final
76 |
77 | # 特征工程
78 |
79 | ## 增加同站点相邻时间段的信息
80 | def add_neighbor_time(df_):
81 | train_df = df_.copy()
82 | # 生成一个用于中间转换的DataFrame
83 | train_now = train_df[['stationID', 'day', 'time_cut', 'inNums', 'outNums']]
84 |
85 | train_df.rename(columns={'inNums': 'inNums_now', 'outNums': 'outNums_now'}, inplace=True)
86 |
87 | # 考虑前多少个时间段,默认考虑前两个时间段
88 | for i in range(2, 0, -1):
89 | train_before = train_now.copy()
90 | train_before['time_cut'] = train_before['time_cut'] + i
91 | train_df = train_df.merge(train_before, how='left', on = ["stationID","day", "time_cut"])
92 | train_df.rename(columns = {'inNums': f'inNums_before{i}', 'outNums': f'outNums_before{i}'}, inplace=True)
93 | train_df.fillna(0, inplace=True)
94 |
95 | # 考虑后多少个时间段,默认考虑前两个时间段
96 | for j in range(2, 0, -1):
97 | train_after = train_now.copy()
98 | train_after['time_cut'] = train_after['time_cut'] - j
99 | train_df = train_df.merge(train_after, how='left', on = ["stationID", "day", "time_cut"])
100 | train_df.rename(columns = {'inNums': f'inNums_after{j}', 'outNums': f'outNums_after{j}'}, inplace=True)
101 | train_df.fillna(0, inplace=True)
102 |
103 | return train_df
104 |
105 | ## 增加乘车高峰时段相关的特征
106 |
107 | def add_peak_type(df_):
108 |
109 | train_df = df_.copy()
110 |
111 | ### 7:00-9:00、17:00-19:00为高峰时段,7:30-8:30、17:30-18:30为特殊高峰
112 | w_time = ['6:00', '7:00', '7:30', '8:30', '9:00', '17:00', '17:30', '18:30', '19:00', '0:00']
113 |
114 | w_time_cut = [10*pd.to_datetime(ti).hour+pd.to_datetime(ti).minute//10 for ti in w_time]
115 |
116 | # 节假日的高峰时间段
117 | h_time = ['6:00','7:00', '9:00', '12:00', '15:00', '18:00', '20:00', '0:00']
118 |
119 | h_time_cut = [10*pd.to_datetime(ti).hour+pd.to_datetime(ti).minute//10 for ti in h_time]
120 |
121 | # 每个时间点对应的time_cut
122 | temp_array = {'temp': [i*10+j for i in range(24) for j in range(6)], 'time_cut': list(range(0, 144))}
123 |
124 | # 工作日
125 | ## 早高峰的开始和结束时间
126 | ### 运营开始时间
127 | w_start = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[0])]
128 |
129 | mp_start_1 = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[1])]
130 | mp_end_1 = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[2])]
131 |
132 | mp_start_2 = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[3])]
133 | mp_end_2 = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[4])]
134 |
135 | ## 早特殊高峰开始结束时间
136 | msp_start = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[2])]
137 | msp_end = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[3])]
138 |
139 | ## 晚高峰开始和结束时间
140 | ap_start_1 = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[5])]
141 | ap_end_1 = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[6])]
142 |
143 | ap_start_2 = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[7])]
144 | ap_end_2 = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[8])]
145 |
146 | ## 晚特殊高峰开始结束时间
147 | asp_start = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[6])]
148 | asp_end = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[7])]
149 |
150 | w_end = temp_array['time_cut'][temp_array['temp'].index(w_time_cut[9])]
151 |
152 | ## 工作日peak类型映射表
153 | ### 0表示停运期间,1表示运行开始到高峰前和高峰后到运营结束,2表示高峰时间,3表示特殊高峰时间
154 |
155 | w_peak = {0: list(range(w_end, w_start)),
156 | 2: list(range(mp_start_1, mp_end_1))
157 | + list(range(mp_start_2, mp_end_2))
158 | + list(range(ap_start_1, ap_end_1))
159 | + list(range(ap_start_2, ap_end_2)),
160 | 3: list(range(msp_start, msp_end))
161 | + list(range(asp_start, asp_end))}
162 |
163 | peak_workday = {}
164 |
165 | for key, value in w_peak.items():
166 | for t in value:
167 | peak_workday[t] = key
168 |
169 |
170 | # 节假日
171 |
172 | ## 早高峰的开始和结束时间
173 |
174 | h_start = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[0])]
175 | h_end = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[7])]
176 |
177 | h_mp_start_1 = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[1])]
178 | h_mp_end_1 = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[2])]
179 |
180 | h_mp_start_2 = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[3])]
181 | h_mp_end_2 = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[4])]
182 |
183 | ## 早特殊高峰开始结束时间
184 | h_msp_start = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[2])]
185 | h_msp_end = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[3])]
186 |
187 | ## 晚高峰开始和结束时间
188 | h_ap_start_1 = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[5])]
189 | h_ap_end_1 = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[6])]
190 |
191 | ## 晚特殊高峰开始结束时间
192 | h_asp_start = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[4])]
193 | h_asp_end = temp_array['time_cut'][temp_array['temp'].index(h_time_cut[5])]
194 |
195 | ## 节假日日peak类型映射表
196 |
197 | h_peak = {0: list(range(h_end, h_start)),
198 | 2: list(range(h_mp_start_1, h_mp_end_1))
199 | + list(range(h_mp_start_2, h_mp_end_2))
200 | + list(range(h_ap_start_1, h_ap_end_1)) ,
201 | 3: list(range(h_msp_start, h_msp_end))
202 | + list(range(h_asp_start, h_asp_end))}
203 |
204 | peak_holiday = {}
205 |
206 | for key, value in h_peak.items():
207 | for t in value:
208 | peak_holiday[t] = key
209 |
210 | # 转换成用于连接的DataFrame
211 | wo_peak = pd.DataFrame({'time_cut':list(peak_workday.keys()) , 'peak_type': list(peak_workday.values()) })
212 | wo_peak['is_holiday'] = 0
213 |
214 | ho_peak = pd.DataFrame({'time_cut': list(peak_holiday.keys()), 'peak_type': list(peak_holiday.values())})
215 | ho_peak['is_holiday'] = 1
216 |
217 | to_peak = pd.concat([wo_peak, ho_peak], axis=0, sort=False)
218 |
219 |
220 |
221 | train_df = train_df.merge(to_peak, on=['is_holiday', 'time_cut'], how='left')
222 | train_df['peak_type'].fillna(1, inplace=True)
223 |
224 | return train_df
225 |
226 | ## 增加同周次进出站流量的信息
227 | def add_week_flow(df_):
228 |
229 | train_df = df_.copy()
230 |
231 | tmp = train_df.groupby(['stationID','weekday','hour','minute'], as_index=False)['inNums_now'].agg({
232 | 'inNums_whm_max' : 'max',
233 | 'inNums_whm_min' : 'min',
234 | 'inNums_whm_mean' : 'mean'
235 | })
236 | train_df = train_df.merge(tmp, on=['stationID','weekday','hour','minute'], how='left')
237 |
238 | tmp = train_df.groupby(['stationID','weekday','hour','minute'], as_index=False)['outNums_now'].agg({
239 | 'outNums_whm_max' : 'max',
240 | 'outNums_whm_min' : 'min',
241 | 'outNums_whm_mean' : 'mean'
242 | })
243 | train_df = train_df.merge(tmp, on=['stationID','weekday','hour','minute'], how='left')
244 |
245 | tmp = train_df.groupby(['stationID','weekday','hour'], as_index=False)['inNums_now'].agg({
246 | 'inNums_wh_max' : 'max',
247 | 'inNums_wh_min' : 'min',
248 | 'inNums_wh_mean' : 'mean'
249 | })
250 | train_df = train_df.merge(tmp, on=['stationID','weekday','hour'], how='left')
251 |
252 | tmp = train_df.groupby(['stationID','weekday','hour'], as_index=False)['outNums_now'].agg({
253 | #'outNums_wh_max' : 'max',
254 | #'outNums_wh_min' : 'min',
255 | 'outNums_wh_mean' : 'mean'
256 | })
257 | train_df = train_df.merge(tmp, on=['stationID','weekday','hour'], how='left')
258 |
259 | return train_df
260 |
261 | ## 增加线路信息
262 | def add_line(df_):
263 | def station_line(record):
264 | station_line = record[['lineID', 'stationID']]
265 | station_line = station_line.drop_duplicates().reset_index(drop=True)
266 | station_line = station_line.sort_values(by='stationID').reset_index(drop=True)
267 | return station_line
268 |
269 | train_df = df_.copy()
270 |
271 | # 这里的file表示原始文件
272 | train_files = [f for f in sorted(os.listdir(train_path)) if f.endswith("csv")]
273 | record = pd.read_csv(os.path.join(train_path, train_files[0]))
274 | line = station_line(record)
275 | line_pad = pd.DataFrame(Counter(line['lineID']), index=line.lineID.unique())
276 | line_ID = (line_pad.T)['B'].reset_index().rename(columns={'index': 'lineID', 'B': 'line'})
277 | line = line.merge(line_ID, on='lineID', how='left')
278 | line.drop('lineID', axis=1, inplace=True)
279 | train_df = train_df.merge(line, on=['stationID'], how='left')
280 | return train_df
281 |
282 | ## 增加车站类型信息
283 | def add_station_type(df_):
284 |
285 | def get_map(roadmap):
286 | roadmap.rename(columns={"Unnamed: 0": 'stationID'}, inplace=True)
287 | tmp = roadmap.drop(['stationID'], axis=1)
288 | roadmap['station_type'] = tmp.sum(axis=1)
289 | return roadmap[['stationID', 'station_type']]
290 |
291 |
292 | roadmap = pd.read_csv(roadfile)
293 | map_pad = get_map(roadmap)
294 |
295 | train_df = df_.copy()
296 | train_df = train_df.merge(map_pad, on=['stationID'], how='left')
297 | return train_df
298 |
299 | ## 增加特征站点标记
300 |
301 | def add_special_station(df_):
302 | train_df = df_.copy()
303 |
304 | train_df['is_special'] = np.nan
305 | train_df.loc[train_df['stationID']==15, 'is_special'] = 1
306 | train_df['is_special'].fillna(0, inplace=True)
307 | return train_df
308 |
309 |
310 | ## 连接训练特征和目标值
311 | # 定义增加周末信息的函数
312 | def add_is_holiday(test):
313 | date = str(test.month.values[0])+'-'+str(test.day.values[0])
314 | if date in w2h or (date not in h2w and test.weekday.values[0] in [6,7]):
315 | test['is_holiday'] = 1
316 | else:
317 | test['is_holiday'] = 0
318 | return test
319 |
320 |
321 | # 获取最终的测试日期数据
322 | def read_test(test):
323 | test['weekday'] = pd.to_datetime(test['startTime']).dt.dayofweek + 1
324 | #test['weekend'] = (pd.to_datetime(test.startTime).dt.weekday >=5).astype(int)
325 | test['month'] = pd.to_datetime(test['startTime']).dt.month
326 | test['day'] = test['startTime'].apply(lambda x: int(x[8:10]))
327 | test['hour'] = test['startTime'].apply(lambda x: int(x[11:13]))
328 | test['minute'] = test['startTime'].apply(lambda x: int(x[14:15]+'0'))
329 | test = test.drop(['startTime', 'endTime'], axis=1)
330 |
331 | test = add_is_holiday(test)
332 | test['time_cut'] = test['hour'] * 6 + test['minute'] // 10
333 | test.drop(['inNums', 'outNums', 'month'], axis=1, inplace=True)
334 | return test
335 |
336 | # 用于对weekday的信息进行修正
337 | def fix_weekday(w):
338 | if w == 7:
339 | return 1
340 | else:
341 | return w+1
342 |
343 | # 合并得到训练集和测试集的数据
344 | def merge_train_test(df_):
345 | train_df = df_.copy()
346 |
347 | # 读取最后要提交的数据
348 | test = pd.read_csv(os.path.join(test_path, sub_file))
349 | test_df = read_test(test)
350 | all_data = pd.concat([train_df, test_df], axis=0, sort=False)
351 |
352 | # 将当天对应的节假日信息取出备用
353 | th = all_data[['day', 'is_holiday']].drop_duplicates().sort_values(by=['day']).reset_index(drop=True).rename(columns={'is_holiday': 'today_is_holiday'})
354 | # 提取用于合并为target的部分
355 | train_target = all_data[['stationID','day', 'hour','minute', 'time_cut', 'inNums_now', 'outNums_now']]
356 |
357 | train_target.rename(columns={'inNums_now': 'inNums', 'outNums_now': 'outNums'}, inplace=True)
358 | # 将所有数据的节假日信息名称改为前一天的节假日信息
359 | all_data.rename(columns={'is_holiday': 'yesterday_is_holiday'}, inplace=True)
360 | # 为了之后合并,将day的特征加1
361 | all_data['day'] += 1
362 | # 对周的信息进行修正
363 | all_data['weekday'] = all_data['weekday'].apply(fix_weekday)
364 |
365 | # 需要将训练集和测试集单独合并
366 |
367 | train_df = all_data[(all_data.day != 29) & (all_data.day != 26) & (all_data.day != 30)]
368 |
369 | test_df = all_data[all_data.day == 29]
370 |
371 | # 首先对生成训练集数据
372 | train_df = train_df.merge(train_target, on=['stationID', 'day', 'hour', 'minute', 'time_cut'], how='left')
373 | ## 对预测目标值的缺失值补0
374 | train_df['inNums'].fillna(0, inplace=True)
375 | train_df['outNums'].fillna(0, inplace=True)
376 |
377 | ## 补充当天是否周末的信息
378 | train_df = train_df.merge(th, on='day', how='left')
379 |
380 | # 生成测试集的数据
381 | test_target = train_target[train_target.day == 29]
382 | # 对测试值进行连接
383 | test_df = test_df.merge(test_target, on=['stationID', 'day', 'hour', 'minute', 'time_cut'], how='outer')
384 | test_df = test_df.sort_values(by=['stationID', 'hour', 'minute']).reset_index(drop=True)
385 | use_fe = ['stationID', 'weekday', 'yesterday_is_holiday', 'nuni_deviceID_of_stationID', 'line', 'station_type', 'is_special']
386 | test_merge = test_df[use_fe].drop_duplicates().dropna()
387 | test_df = test_df.drop(use_fe[1:], axis=1)
388 | test_df = test_df.merge(test_merge, on=['stationID'], how='left')
389 |
390 | test_df = test_df.merge(th, on='day', how='left')
391 | test_df.fillna(0, inplace=True)
392 | all_data = pd.concat([train_df, test_df], axis=0, sort=False)
393 | return all_data, train_df
394 |
395 | # 考虑进出站流量和时间段密切相关
396 | # 针对time_cut进行目标编码
397 | def target_encoding(all_data, train_df):
398 | # 设置参数
399 | N_min = 300
400 | fe = 'time_cut'
401 |
402 | # 针对进站流量的目标编码
403 | te_in = BetaEncoder(fe)
404 | te_in.fit(train_df, 'inNums')
405 | all_data['in_time_cut'] = te_in.transform(all_data, 'mean', N_min=N_min)
406 |
407 | # 针对出站流量的目标编码
408 | te_out = BetaEncoder(fe)
409 | te_out.fit(train_df, 'outNums')
410 | all_data['out_time_cut'] = te_out.transform(all_data, 'mean', N_min = N_min)
411 |
412 | return all_data
413 |
414 |
415 | def train(all_data):
416 | # 设置需要使用的特征
417 | features = [f for f in all_data.columns if f not in ['inNums', 'outNums', 'time_cut']]
418 |
419 | # 提取训练集和测试集
420 |
421 | # 所有数据
422 | # 由于1号是元旦节,情况相对特殊,所以去掉了该极端值
423 | # 也可以使用一下看看效果
424 | train_data = all_data[(all_data.day != 29) & (all_data.day != 2)]
425 |
426 | # 用于训练的数据
427 | test = all_data[all_data.day == 29]
428 | X_test = test[features].values
429 |
430 | # 设置滑动窗口
431 | # 这里A榜要预测的是29号的信息,所以设置同是周二的日期作为滑动窗口的末尾
432 | # 而B榜要预测的是27号的信息,所以设置周末的日期作为滑窗末尾,即13,20
433 | slip = [15, 22]
434 |
435 | n = len(slip)
436 |
437 |
438 | ## 搭建LGB模型
439 | # 设置模型参数
440 | lgb_params = {
441 | 'boosting_type': 'gbdt',
442 | 'objective': 'regression_l1',
443 | 'metric': 'mae',
444 | 'num_leaves': 63,
445 | 'learning_rate': 0.01,
446 | 'feature_fraction': 0.9,
447 | 'bagging_fraction': 0.9,
448 | 'bagging_seed':0,
449 | 'bagging_freq': 1,
450 | 'verbose': 1,
451 | 'reg_alpha':1,
452 | 'reg_lambda':2,
453 |
454 | # 设置GPU
455 | 'device' : 'gpu',
456 | 'gpu_platform_id':1,
457 | 'gpu_device_id':0
458 | }
459 |
460 | ## 预测进站流量
461 | in_lgb_pred = np.zeros(len(X_test))
462 | X_data = train_data[features].values
463 | y_data = train_data['inNums'].values
464 | for i, date in enumerate(slip):
465 | train = train_data[train_data.day