看了唐国梁老师视频讲的《机器学习实战 纽约出租车车费预测》受益匪浅,这一案例相对简单,适合初学者研究学习。
纽约出租车车费预测的背景是基于过去7年出租车行驶和收费的记录,预测未来某个时间点打车的车费,包括train.csv(训练集)、test.csv(测试集)、sample_submission.csv。
train.csv共1048576条数据,有key、fare_amount、pickup_datetime、pickup_longitude、pickup_latitude、dropoff_longitude、dropoff_latitude、passenger_count八个字段。
test.csv共9914条数据,有key、pickup_datetime、pickup_longitude、pickup_latitude、dropoff_longitude、dropoff_latitude、passenger_count七个字段。test.csv与train.csv相比少了fare_amount字段,该字段正是本次文章的目标-预测测试集数据中的车费。
sample_submission.csv只有key、fare_amount两个字段,共9914条数据。sample_submission.csv的意义是将测试集中预测出的车费赋给fare_amount展示。
背景和文档交代清楚了,接下来要考虑的是如何进行代码的编写了。代码编写逻辑如下:加载数据集,数据分析、清洗,模型训练和数据预测。其中数据分析、清洗是要结合业务进行深入的分析、筛选,也是本篇文章内容讲解的重点。
现在正式进行代码的编写,首先导入本次代码逻辑实现需要用到的库,如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import sklearn
第一步:加载数据集
因为train.csv有1048576条数据,所以用nrow获取1000000条数据。
train = pd.read_csv("train.csv", nrows=1000000) # 加载训练集
test = pd.read_csv("test.csv") # 加载测试集
第二步:数据分析、清洗
train.shape # 训练集的形状
执行上面的代码会获得1000000条数据,8个字段的数据统计
train.shape运行结果示意图
test.shape # 测试集的形状
执行上面的代码会获得9914条数据,7个字段的数据统计
test.shape运行结果示意图
train.head() # 默认显示前5行训练集数据
运行train.head()会获得前5条数据,分析这五条数据初步可知:1、key和pickup_datetme前半部分的数据相同,表示的上车时间;2、上车经度和下车经度在73~74之间,上车纬度和下车纬度集中在40之间;3、正常情况下乘客总数≥1;4、票价在4~17元之间
train.head()运行结果示意图
test.head() # 默认显示前5行测试集数据
运行test.head()得到test.csv的结果与train.csv的结果一致,唯一的区别就是没有fare_amount字段。
test.head() 运行结果示意图
train.describe() # 训练集描述
看train.descibe()的运行结果,发现了几处异常:1、车费的最小值是-44.9,顾客坐了出租车还要倒贴给顾客钱,不合情理;2、经度范围:-180~180,维度范围:-90~90。看上车经纬度和下车经纬度的最大值和最小值都超出了范围;3、乘客总数的最大值竟然是208。这几个异常会通过接下来的逻辑处理清洗掉。
train.describe()运行结果示意图
test.describe() # 测试集描述
看test.csv的数据稍微好些,经纬度在正常范围内,不过乘客总数的最大值是6是有问题的。
test.describe()运行结果示意图
以上是对数据的查看、分析,现在开始对数据进行清洗
1 检查数据中是否有空值
train.isnull().sum().sort_values(ascending=True) # 统计空值的数量,根据数量大小排序
训练集的下车经纬度有10条空值
训练集空值数量统计示意图
test.isnull().sum().sort_values(ascending=True) # 统计空值的数量,根据数量大小排序
测试集的数据正常,没有空值
测试集空值数量统计示意图
# 删除掉train中为空的数据(10行)
train.drop(train[train.isnull().any(axis=1)].index, axis=0, inplace=True)
train.isnull()指的是查出空值数据,any(axis=1)指的是将查出空值数据的 这一列,以行为基线从左到右删除这一行,以上的理解是查阅了相关资料后得出,到现在还是有一些疑惑,欢迎大佬指正。train.drop(train[train.isnull().any(axis=1)].index就是根据索引获取该条数据并删掉。
axis=0表示一行;inplace=True执行后会在代码中生效,后续得到的数据都会是删除空数据后的数据。
train.shape # 比原始数据少了10行
运行train.shape,训练集中已经将含有下车经纬度为空的10条数据删掉
train.shape运行示意图
2 检查车费fare_amount这一列
#查看车费这列的数据(车费不可能为负数)
train['fare_amount'].describe()
前面分析过,车费最小值是负数,这次针对车费单独描述,运行结果表明还是车费最小值有问题
train['fare_amount'].describe()运行结果示意图
# 统计train中车费小于0的数据有多少
#引入collections
from collections import Counter
Counter(train['fare_amount']<0)
# 有38行数据,车费小于0
#运行结果:车费大于0的是False,车费小于0的是True
Counter(train['fare_amount']<0)运行结果示意图
# 删除掉车费小于0的数据
train.drop(train[train['fare_amount']<0].index, axis=0, inplace=True)
train['fare_amount'].describe() # 查看车费数据
结果展示正常
train['fare_amount'].describe() 运行结果示意图
# 可视化(直方图):0 < 票价 < 100
train[train.fare_amount<100].fare_amount.hist(bins=100, figsize=(14,3))
plt.xlabel('fare $USD')
plt.title("Histogram")
看结果,0~20元的车费最多
0~100元票价运行结果示意图
3 检查乘客passenger_count这一列
# 检查乘客列的数据
train['passenger_count'].describe()
从获得的结果可知,乘客总数最大值208人有问题,包括司机,出租车最多能坐5人
train['passenger_count'].describe()代码运行示意图
个人理解乘客总数最多是4,代码是判断乘客人数是否大于6,大于6就是异常数据,这里按照老师讲的视频来吧
# 查看乘客人数大于6的数据
train[train['passenger_count']>6]
运行代码发现了一条大于6的异常数据
train[train['passenger_count']>6]运行结果示意图
# 删除这个离异值
train.drop(train[train['passenger_count']>6].index, axis=0, inplace=True)
4 检查上车点的经度和维度
1.经度范围:-180~180
2.维度范围:-90~90
train['pickup_latitude'].describe() #查看上车点维度数据(min和max的值异常)
上车维度数据描述
# 维度小于-90的数据(有3行)
train[train['pickup_latitude']<-90]
rain[train['pickup_latitude']<-90]运行结果示意图
# 经度大于90的数据(有9行)
train[train['pickup_latitude']>90]
train[train['pickup_latitude']>90]运行结果示意图
# 删除这些离异值数据
train.drop(train[(train['pickup_latitude']<-90) | (train['pickup_latitude']>90)].index, axis=0, inplace=True)
train.shape #查看训练集数据
train.shape代码运行结果示意图
# 查看上车点经度数据(min值异常)
train['pickup_longitude'].describe()
看运行结果,上车经度最小值是有问题的
train['pickup_longitude'].describe()代码运行结果示意图
# 查看经度小于-180的数据
train[train['pickup_longitude']<-180]
train[train['pickup_longitude']<-180]代码运行结果示意图
# 删除这些离异值
train.drop(train[train['pickup_longitude']<-180].index, axis=0, inplace=True)
train.shape
train.shape代码运行结果示意图
5 检查下车点的经度和维度
# 删除那些维度小于-90,大于90的数据
train.drop(train[(train['dropoff_latitude']<-90) | (train['dropoff_latitude']>90)].index, axis=0, inplace=True)
# 删除掉那些经度小于-180,大于180的数据
train.drop(train[(train['dropoff_longitude']<-180)|(train['dropoff_longitude']>180)].index, axis=0,inplace=True)
train.shape
train.shape代码运行结果示意图
6 可视化地图,清理一些离异值
这块的代码逻辑感觉很重要,做出来的图也很炫
# 1 在test数据集上确定一个区域框,删除掉train数据集中不在区域框内的奇异点
# (1) 维度最小值,维度最大值
min(test.pickup_latitude.min(), test.dropoff_latitude.min()), \
max(test.pickup_latitude.max(), test.dropoff_latitude.max())
确定上车、下车维度范围
# (2) 经度最小值,经度最大值
min(test.pickup_longitude.min(), test.dropoff_longitude.min()), \
max(test.pickup_longitude.max(), test.dropoff_longitude.max())
确定上车、下车经度范围
# (3) 根据指定的区域框,除掉那些奇异点
#该方法通过确定的经纬度范围,获取经纬度范围内的数据,不符合的数据筛选出去
def select_within_boundingbox(df, BB):
return (df.pickup_longitude >= BB[0]) & (df.pickup_longitude <= BB[1]) & \
(df.pickup_latitude >= BB[2]) & (df.pickup_latitude <= BB[3]) & \
(df.dropoff_longitude >= BB[0]) & (df.dropoff_longitude <= BB[1]) & \
(df.dropoff_latitude >= BB[2]) & (df.dropoff_latitude <= BB[3])
# 将经纬度的最大值、最小值赋予变量BB
BB = (-74.5, -72.8, 40.5, 41.8)
#截图
#视频中访问的是一个网址,网址加载图片很慢,直接访问会报错。这里直接加载下载好的图片
nyc_map = plt.imread('map_normal.jpg')
BB_zoom = (-74.3, -73.7, 40.5, 40.9) # 放大后的地图,将最小值变小,最大值变大
# 截图(放大)
nyc_map_zoom = plt.imread('map_enlarge.jpg')
train = train[select_within_boundingbox(train, BB)] # 删除区域之外的点
train.shape
train.shape代码运行结果示意图
# (4) 在地图显示这些点
def plot_on_map(df, BB, nyc_map, s=10, alpha=0.2):
fig,axs = plt.subplots(1, 2, figsize=(16, 10))
# 第一个子图
axs[0].scatter(df.pickup_longitude, df.pickup_latitude, alpha=alpha, c='r', s=s)
axs[0].set_xlim(BB[0], BB[1])
axs[0].set_ylim(BB[2], BB[3])
axs[0].set_title('PickUp Locations')
axs[0].imshow(nyc_map, extent=BB)
# 第二个子图
axs[1].scatter(df.dropoff_longitude, df.dropoff_latitude, alpha=alpha, c='r', s=s)
axs[1].set_xlim((BB[0], BB[1]))
axs[1].set_ylim((BB[2], BB[3]))
axs[1].set_title('Dropoff locations')
axs[1].imshow(nyc_map, extent=BB)
# 将训练集数据、经纬度范围传入plot_on_map方法中运行
plot_on_map(train, BB, nyc_map, s=1, alpha=0.3)
上车、下车经纬度范围示意图
#将训练集数据、放大后的经纬度范围传入plot_on_map方法中运行
plot_on_map(train, BB_zoom,nyc_map_zoom, s=1, alpha=0.3)
放大后的上车、下车经纬度范围示意图
7 检查数据类型
train.dtypes #查看训练集的数据类型
运行结果显示key、pickup_datetime是字符串类型
训练集数据类型示意图
# 日期类型转换:key, pickup_datetime
for dataset in [train, test]:
dataset['key'] = pd.to_datetime(dataset['key'])
dataset['pickup_datetime'] = pd.to_datetime(dataset['pickup_datetime'])
8 日期数据进行分析
1.year
2.month
3.day
4.hour
5.day of week
# 增加5列,分别是:year, month, day, hour, day of week
for dataset in [train, test]:
dataset['year'] = dataset['pickup_datetime'].dt.year
dataset['month'] = dataset['pickup_datetime'].dt.month
dataset['day'] = dataset['pickup_datetime'].dt.day
dataset['hour'] = dataset['pickup_datetime'].dt.hour
dataset['day of week'] = dataset['pickup_datetime'].dt.dayofweek
train.head()
看运行结果,每行多了五列数据,分别是year、month、day、hour、day of week
train.head()代码运行结果示意图
test.head()
test.head()代码运行结果示意图
9 根据经纬度计算距离
# 计算公式
def distance(lat1, long1, lat2, long2):
data = [train, test]
for i in data:
R = 6371 # 地球半径(单位:千米)
phi1 = np.radians(i[lat1])
phi2 = np.radians(i[lat2])
delta_phi = np.radians(i[lat2]-i[lat1])
delta_lambda = np.radians(i[long2]-i[long1])
#a = sin²((φB - φA)/2) + cos φA . cos φB . sin²((λB - λA)/2)
a = np.sin(delta_phi / 2.0) ** 2 + np.cos(phi1) * np.cos(phi2) * np.sin(delta_lambda / 2.0) ** 2
# c = 2 * atan2( √a, √(1-a))
c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a))
# d = R*c
d = (R * c) # 单位:千米
i['H_Distance'] = d
return d
说实话,distance()方法用到的具体数学原理我也没看懂,抽空需要好好学习了解下。distance()方法经过一系列逻辑计算后,将上车下车的距离赋值给i['H_Distance'],因为是for循环,i代表训练集和测试集,所以训练集、测试集都多了一个字段H_Distance。
此外,该方法将上下车距离返回。
distance('pickup_latitude','pickup_longitude', 'dropoff_latitude', 'dropoff_longitude')
调用此方法,返回了第二次遍历的测试集的每条出租车行驶的距离。
测试集每条出租车行驶距离数据
train.head() #多了一个字段H_Distance
训练集每条出租车行驶距离数据
test.head() #多了一个字段H_Distance
测试集每条出租车行驶距离数据
# 统计距离为0, 票价为0的数据
train[(train['H_Distance']==0) & (train['fare_amount']==0)]
距离为0,票价为0的数据
# 删除
train.drop(train[(train['H_Distance']==0) & (train['fare_amount']==0)].index, axis=0, inplace=True)
# 统计距离为0,票价不为0的数据
# 原因1:司机等待乘客很长时间,乘客最终取消了订单,乘客依旧支付了等待的费用;
# 原因2:车辆的经纬度没有被准确录入或缺失;
len(train[(train['H_Distance']==0) & (train['fare_amount']!=0)]) #运行代码得到的结果是10478
# 删除
train.drop(train[(train['H_Distance']==0) & (train['fare_amount']!=0)].index, axis=0, inplace=True)
10 新的字段:每公里车费:根据距离、车费,计算每公里的车费
train['fare_per_mile'] = train.fare_amount / train.H_Distance
train.fare_per_mile.describe()
fare_per_mile数据描述示意图
train.head() #代码运行后,多了fare_per_mile列
train_head()代码运行结果示意图
# 统计每一年的不同时间段的每小时车费
train.pivot_table('fare_per_mile', index='hour', columns='year').plot(figsize=(14, 6))
plt.ylabel('Fare $USD/mile')
每一年的不同时间段的每小时车费统计结果示意图
第3步:模型训练和数据预测
train.columns #查看训练集的每个字段
训练集包含的字段
X_train = train.iloc[:,[3,4,5,6,7,8,9,10,11,12,13]] #获取训练集中3~13字段中的所有数据
y_train = train.iloc[:, [1]] # 获取fare_amount(车费)这一列的所有数据
y_train
训练集中每一列车费示意图
X_train.shape #代码运行后,得到X_train的模型(968537, 11),即有968537条数据,每条数据有11个字段
y_train.shape #代码运行后,得到y_train的模型(968537,1),即有968537条数据,每条数据有1个字段
注意:从下面的代码运行开始,涉及到代码的训练、预测。因为数据量比较大,因此得到结果的时间会较长,需耐心等待
# 随机森林实现
from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor()
rf.fit(X_train, y_train)
上面的代码运行完后会返回RandomForestRegressor(),这个方法的原理还不是很明白,后续也是要花时间学习其原理的。
test.columns #查看测试集包含的每个字段
测试集包含的字段示意图
rf_predict = rf.predict(test.iloc[:, [2,3,4,5,6,7,8,9,10,11,12]])
#1.test.iloc[:, [2,3,4,5,6,7,8,9,10,11,12]]获取测试集2~12列所有行的数据
#2.rf为通过随机森林算法将训练集训练好的模型,rf.predict()方法直接调用test.iloc进行预测
submission = pd.read_csv("sample_submission.csv") #读取sample_submission.csv
submission.head()
submission结果显示:fare_amount那一列为11.35.下面的代码就是将随机森林对测试集预测的票价赋值给submission中的fare_amount。
sample_submission.csv前5条数据展示示意图
# 提交
submission = pd.read_csv("sample_submission.csv")
submission['fare_amount'] = rf_predict
submission.to_csv("submission_1.csv", index=False)
submission.head()
测试集票价的预测结果
至此,出租车票价预测讲完了。大家一起学习,欢迎各位大佬交流、指正,谢谢!
源码下载地址:
#头条创作挑战赛#