Press "Enter" to skip to content

数据驱动的调度

使用监督学习预测芝加哥汽车碰撞的服务呼叫

Photo by Sawyer Bengtson on Unsplash

介绍

在当今快节奏的世界中,数据驱动的调度响应系统决策的需求变得越来越重要。调度员在接听电话时会进行一种分类,根据严重程度、时间敏感性和其他因素对案件进行优先排序。通过利用监督学习模型的强大功能,有可能优化这个过程,以更准确地预测案件的严重程度,同时结合人工调度员的评估。

在本文中,我将介绍我开发的一种解决方案,以改进对芝加哥车辆碰撞中的伤亡和/或严重车辆损坏的预测。考虑到事故发生地点、道路条件、限速和发生时间等因素,来回答一个简单的是或否的问题:这次车祸是否需要救护车或拖车?

Photo by Chris Dickens on Unsplash

简而言之,这个机器学习工具的主要目标是基于其他已知因素对最有可能需要呼叫(医疗、拖车或两者)的碰撞进行分类。通过利用这个工具,响应者将能够根据各种条件(如天气和时间)有效地分配他们的资源到城市的不同部分。

为了使这样的工具准确有效,需要大量的数据来源来从历史数据中进行预测 – 幸运的是,芝加哥市已经有了这样的资源(芝加哥数据门户),所以这些数据将被用作测试案例。

实施这些预测模型肯定会提高处理城市街道上的碰撞事件的准备和响应时间效率。通过获取碰撞数据中的潜在模式和趋势的洞察,我们可以努力营造更安全的道路环境,并优化紧急服务。

在下面的部分中,我将详细介绍数据清理、模型构建、微调和评估,然后分析模型的结果并得出结论。项目的GitHub文件夹链接,其中包括一个Jupyter Notebook和一个更全面的项目报告,可以在此处找到。

数据收集和准备

初始设置

我在下面列出了项目中使用的基本数据分析库;项目中使用了诸如pandas和numpy的标准库,以及matplotlib的pyplot和seaborn进行可视化。此外,我还使用了missingno库来识别数据中的缺失值 – 我发现这个库对于可视化数据集中的缺失数据非常有用,并且我推荐在涉及数据框的任何数据科学项目中使用它:

#通用数据分析导入 os
import pandas as pd
from datetime import date
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import missingno as msno

从机器学习模块SciKit learn(sklearn)中导入的函数用于构建机器学习引擎。这些函数如下所示 – 我将在后面的分类模型部分描述每个函数的目的:

#预处理from sklearn.preprocessing import LabelEncoderfrom sklearn.preprocessing import StandardScaler#模型from sklearn.neighbors import KNeighborsClassifier#报告from sklearn.model_selection import train_test_splitfrom sklearn.model_selection import RandomizedSearchCV#度量from sklearn.metrics import accuracy_scorefrom sklearn.metrics import f1_scorefrom sklearn.metrics import precision_scorefrom sklearn.metrics import recall_score

这个项目的数据都来自芝加哥数据门户,有两个来源:

  1. 交通事故:芝加哥地区的车辆碰撞的实时数据集。此数据集的特征是记录在碰撞发生时的条件,例如天气条件、道路走向、纬度和经度等详细信息。
  2. 警察分区边界:静态数据集,指示芝加哥警察分区的边界;此数据集用于补充交通事故数据集的区域信息。可以将此数据集与原始数据集连接以对最频繁发生碰撞的区域进行分析。

数据清洗

导入两个数据集后,它们现在可以合并以将区域数据添加到最终分析中。这是使用pandas中的.merge()函数完成的——我在两个数据帧上使用内连接来捕获两者中的所有信息,使用两者中的碰撞数据作为连接键(在交通事故数据集中列为beat_of_occurrence,在警察区数据集中列为BEAT_NUM):

#将碰撞数据与区域数据合并 - 内联合并collisions = collision_raw.merge(beat_data, how='inner',                                 left_on='beat_of_occurrence',                                 right_on='BEAT_NUM'                                 )

通过使用.info()函数提供的信息快速查看,可以发现许多列中有稀疏数据。可以使用missingno矩阵函数可视化这些数据:

#可视化缺失数据#按报告接收日期排序collisions = collisions.sort_values(by='crash_date', ascending=True)#绘制缺失数据矩阵msno.matrix(collisions)plt.show()#排序后的数据信息print(collisions.info())

这将显示所有列中缺失数据的矩阵,如下所示:

未经处理的数据集,多个列包含大量空值

通过删除稀疏数据的列,可以提取出一个更干净的数据集;需要删除的列在一个列表中定义,然后使用.drop()函数从数据集中删除:

#定义不必要的列drop_cols = ['location', 'crash_date_est_i','report_type', 'intersection_related_i',       'hit_and_run_i', 'photos_taken_i', 'crash_date_est_i', 'injuries_unknown',       'private_property_i', 'statements_taken_i', 'dooring_i', 'work_zone_i',       'work_zone_type', 'workers_present_i','lane_cnt','the_geom','rd_no',            'SECTOR','BEAT','BEAT_NUM']#删除列collisions=collisions.drop(columns=drop_cols)#绘制缺失数据矩阵msno.matrix(collisions)plt.show()#排序后的数据信息print(collisions.info())

这将得到一个更干净的msno矩阵:

精简数据集的msno矩阵

查看纬度和经度数据时,有一小部分行的值为空值,而其他行的值错误地为零(很可能是报告错误):

纬度和经度列均包含零值(请参见每个列的最小值和最大值)

这些将导致训练模型时出现错误,因此我将其删除:

#一些不正确的经纬度数据 - 需要删除这些行collisions = collisions[collisions['longitude']<-80]collisions = collisions[collisions['latitude']>40]

通过充分清洗数据,我能够继续开发分类模型。

分类模型

探索性数据分析

在进行机器学习模型之前,需要进行一些探索性数据分析(EDA)——将数据框的每个列绘制在直方图上,使用50个箱子显示数据的分布。直方图在EDA步骤中非常有用,原因有很多,主要包括它们可以给出数据分布的概览,帮助识别异常值,并最终在特征工程上辅助做出决策:

#绘制数值值的直方图collisions.hist(bins=50,figsize=(16,12))plt.show()
最终数据集中各列的直方图

对列直方图的粗略观察表明,纬度数据是双峰的,而经度数据是右偏的。这需要进行标准化,以便更好地应用于机器学习目的。

未进行缩放的纬度经度数据

此外,事故发生小时列呈周期性,可以使用三角函数(例如正弦函数)进行转换。

未缩放的事故发生小时数据

缩放和转换

缩放是数据预处理中的一种技术,用于使特征具有相似的量级。这对于机器学习模型特别重要,因为模型通常对输入特征的尺度敏感。我定义了StandardScaler()函数作为该模型中的缩放器,该缩放函数将数据转换为均值为0,标准差为1。

对于具有偏态或双峰分布的数据,可以使用对数函数进行缩放。对数函数使偏态数据更对称,减小了数据的尾部,这在处理异常值时非常有用。我以这种方式缩放了纬度和经度数据;由于经度数据都是负数,因此计算了负对数,然后进行了缩放。

#对纬度和经度数据进行缩放
scaler = StandardScaler()
# 对经度进行对数转换
collisions_ml['neg_log_longitude'] = scaler.fit_transform(np.log1p(-collisions_ml['longitude']).values.reshape(-1,1))
# 对纬度进行归一化
collisions_ml['norm_latitude'] = scaler.fit_transform(np.log1p(collisions['latitude']).values.reshape(-1,1))

这样可以得到所需的效果,如下所示:

已缩放的纬度经度数据

相比之下,周期性数据通常使用三角函数(例如正弦和余弦)进行缩放。基于先前的观察,事故发生小时数据看起来大致呈周期性,因此我将正弦函数应用于数据,如下所示。由于numpy的sin()函数是以弧度为单位的,所以在计算输入的正弦之前,我首先将输入转换为弧度:

#转换事故发生小时数据,数据是周期性的,可以使用三角函数进行编码
collisions_ml['sin_hr'] = np.sin(2*np.pi*collisions_ml['crash_hour']/24)

下面是转换后数据的直方图:

已缩放的事故发生小时数据

最后,我从模型中删除了未缩放的数据,以避免对模型预测产生干扰:

#删除之前的纬度/经度列
lat_long_drop_cols = ['longitude','latitude']
collisions_ml.drop(lat_long_drop_cols,axis=1,inplace=True)
#删除事故发生小时列
collisions_ml.drop('crash_hour',axis=1,inplace=True)

数据编码

数据预处理中的另一项重要步骤是数据编码,即将非数字数据(例如类别)表示为数字格式,以使其与机器学习算法兼容。对于该模型中的分类数据,我使用了一种称为标签编码的方法,即在输入模型之前,为列中的每个类别赋予一个数字值。以下是该过程的示意图:

标签编码示例(由Zach M提供的来源)

我对数据集中的列进行了编码,首先从原始数据集中分离出我想要保留的列,并复制了数据框(collisions_ml)。然后,我将分类列定义为一个列表,并使用sklearn中的LabelEncoder()函数来拟合和转换分类列:

#将列分割为列表ml_cols = ['posted_speed_limit','traffic_control_device', 'device_condition', 'weather_condition',          'lighting_condition', 'first_crash_type', 'trafficway_type','alignment',            'roadway_surface_cond', 'road_defect', 'crash_type','damage','prim_contributory_cause',          'sec_contributory_cause','street_direction','num_units', 'DISTRICT',          'crash_hour','crash_day_of_week','latitude', 'longitude']cat_cols = ['traffic_control_device', 'device_condition', 'weather_condition', 'DISTRICT',           'lighting_condition', 'first_crash_type', 'trafficway_type','alignment',           'roadway_surface_cond', 'road_defect', 'crash_type','damage','prim_contributory_cause',           'sec_contributory_cause','street_direction','num_units']#复制数据集collisions_ml = collisions[ml_cols].copy()#编码分类值label_encoder = LabelEncoder()for col in collisions_ml[cat_cols].columns:    collisions_ml[col] = label_encoder.fit_transform(collisions_ml[col])

现在数据已经经过充分的预处理,可以将数据拆分为训练数据和测试数据,并对数据进行分类模型的拟合。

拆分训练和测试数据

在构建机器学习模型时,将数据分为训练集和测试集非常重要;训练集是初始数据的一部分,用于在正确的响应上训练模型,而测试集用于评估模型的性能。保持这些数据的分离是必要的,以减少过拟合和模型偏差的风险。

我使用drop()函数将crash_type列分离出来(剩余的特征将用作预测crash_type的变量),并将crash_type定义为要使用模型预测的y结果。使用sklearn中的train_test_split函数将初始数据集的20%作为训练数据,其余部分用于模型测试。

#创建测试集#设置X和y值X = collisions_ml.drop('crash_type', axis=1)y = collisions_ml['crash_type']X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

K最近邻分类

对于这个项目,我使用K最近邻(KNN)分类模型来预测特征的结果。KNN模型通过检查未知数据点周围的K个最近已知值的值,然后根据这些“邻居”点的值对数据点进行分类。它是一种非参数分类器,意味着它不对底层数据分布做任何假设;但它在计算上是昂贵的,并且对数据中的异常值敏感。

我使用初始的n_neighbors等于3和欧氏度量来实例化KNN分类器,然后将模型拟合到训练数据:

#分类器 - K最近邻#实例化KNN分类器KNNClassifier = KNeighborsClassifier(n_neighbors=3, metric = 'euclidean')KNNClassifier.fit(X_train,y_train)

模型拟合到训练数据后,我对测试数据进行了以下预测:

#预测#在训练集上进行预测y_train_pred = KNNClassifier.predict(X_train)#在测试集上进行预测y_test_pred = KNNClassifier.predict(X_test)

评估

机器学习模型的评估通常使用四个指标:准确率、精确率、召回率和F1分数。这些指标之间的差异非常微小,但可以用以下方式定义:

  1. 准确率:模型预测中真正预测结果的百分比。通常应该测量训练数据和测试数据的准确率以评估模型的拟合度。
  2. 精确率:模型预测中真正预测结果中的百分比。
  3. 召回率:模型预测中真正预测结果在数据集中所有正例的百分比。
  4. F1分数:模型在数据中识别正例的能力的综合指标,结合了精确率和召回率。

我使用下面的代码片段计算了KNN模型的度量指标 – 我还计算了模型在训练集和测试集上的准确率差异,以评估模型的拟合程度:

#评估模型#计算模型的准确率#计算训练数据上的准确率train_accuracy = accuracy_score(y_train, y_train_pred)#计算测试数据上的准确率test_accuracy = accuracy_score(y_test, y_test_pred)#计算f1得分、精确度、召回率f1 = f1_score(y_test, y_test_pred)precision = precision_score(y_test,y_test_pred)recall = recall_score(y_test,y_test_pred)#比较性能print("训练准确率:", train_accuracy)print("测试准确率:", test_accuracy)print("训练-测试准确率差异:", train_accuracy-test_accuracy)#打印精确度分数print("精确度分数:", precision)#打印召回率print("召回率:", recall)#打印f1得分print("f1得分:", f1)

KNN模型的初始度量指标如下:

第一次迭代中的KNN模型度量指标

该模型在测试准确率(79.6%)、精确度(82.1%)、召回率(91.1%)和f1得分(86.3%)方面表现良好,但测试准确率(93.1%)远高于训练准确率,相差13.5%。这表明模型对数据过拟合,意味着它在未知数据上准确预测的能力较差。因此,需要调整模型以获得更好的拟合效果 – 这可以通过一种称为超参数调整的过程来完成。

超参数调整

超参数调整是选择机器学习模型的最佳超参数集的过程。我使用k折交叉验证对模型进行了微调 – 这是一种重新采样技术,其中数据被分成k个子集(或折叠),然后每个折叠依次被用作验证集,而剩余的数据被用作训练集。这种方法有效地降低了由特定的训练/测试集选择引入模型偏差的风险。

KNN模型的超参数包括邻居数量(n_neighbors)和距离度量。在KNN分类器中,有多种不同的距离度量方法,但我主要关注两种选择:

  1. 欧式距离:可以理解为两点之间的直线距离 – 这是最常用的距离度量。
  2. 曼哈顿距离:也称为“城市街区”距离,这是两点坐标之间绝对差异的总和。如果你站在一座城市建筑的街角,试图到达对面的街角 – 你不会穿过建筑物去到达另一边,而是沿着一个街区上升,然后横穿一个街区。

请注意,我还可以微调权重参数(确定是否所有邻居投票权重相等,还是更近的邻居被赋予更高的重要性),但我决定保持投票权重均匀。

我定义了一个参数网格,其中n_neighbors为3、7和10,度量标准为欧式距离或曼哈顿距离。然后,我实例化了一个RandomizedSearchCV算法,将KNN分类器作为估计器,并传入参数网格。我通过将cv参数设置为5来将数据分成5个折叠,然后将其拟合到训练集。以下是此过程的代码片段:

#微调(RandomisedSearchCV)#定义参数网格param_grid = {    'n_neighbors': [3, 7, 10],    'metric': ['euclidean','manhattan']}#实例化RandomizedSearchCVrandom_search = RandomizedSearchCV(estimator=KNeighborsClassifier(), param_distributions=param_grid, cv=5)#拟合到训练数据random_search.fit(X_train, y_train)#检索最佳模型和性能best_classifier = random_search.best_estimator_best_accuracy = random_search.best_score_print("最佳准确率:", best_accuracy)print("最佳模型:", best_classifier)

从算法中检索到了最佳准确率和分类器,表明当n_neighbors设置为10,使用曼哈顿距离度量时,分类器的性能最佳,这将导致准确度得分为74.0%:

交叉验证结果-随机搜索分类器建议使用曼哈顿距离度量,n_neighbors=10

因此,这些参数被输入到分类器中,并且模型被重新训练:

#分类器-K最近邻#实例化K最近邻分类器KNNClassifier = KNeighborsClassifier(n_neighbors=10, metric = 'manhattan')KNNClassifier.fit(X_train,y_train)

性能指标再次从分类器中提取出来,以与之前的方式相同-可以在下面看到此迭代的指标截图:

调整后的KNN模型的指标

交叉验证导致所有指标的结果稍微较差;测试准确率下降了2.6%,精确率下降了1.5%,召回率下降了0.5%,F1得分下降了1%。然而,训练-测试准确率差异降低到了3.8%,最初为13.5%。这表明模型不再过拟合数据,因此更适合预测未知数据。

结论

总而言之,K最近邻分类器在预测碰撞是否需要拖车或救护车方面表现良好。模型的第一个迭代的初始指标令人印象深刻,但测试准确率和训练准确率之间的差异表明存在过拟合现象。超参数调优使得模型得到优化,显著减小了两个数据集之间的准确率差距。虽然在此过程中性能指标有所下降,但更好的拟合度带来的好处超过了这些问题。

参考文献

  1. Levy, J. (无日期)。交通事故-碰撞[数据集]。从芝加哥数据门户检索。网址:https://data.cityofchicago.org/Transportation/Traffic-Crashes-Crashes/85ca-t3if(访问日期:2023年5月14日)。
  2. 芝加哥警察局(无日期)。边界-警察巡逻区(当前)[数据集]。从芝加哥数据门户检索。网址:https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Beats-current-/aerh-rz74(访问日期:2023年5月14日)。
  3. Zach M. (2022). “如何在Python中执行标签编码(附例子)。”[在线]。网址:https://www.statology.org/label-encoding-in-python/(访问日期:2023年7月19日)。
Leave a Reply

Your email address will not be published. Required fields are marked *