Авторы Ройи Рази, Ариана Гордон, Эяль Хашимшони и Эран Перельман

Дорожные столкновения в США - обычное явление. Набор данных с открытым исходным кодом с информацией о дорожных столкновениях, произошедших в штате Калифорния в период с января 2001 г. по середину октября 2020 г., доступен на сайте kaggle. Мы использовали этот набор данных для прогнозирования серьезности травм в результате дорожно-транспортного происшествия. Если мы сможем точно предсказать серьезность травмы, это позволит лицам, оказывающим первую помощь, лучше расставить приоритеты в срочности реагирования на дорожно-транспортные происшествия и может даже оказаться полезным для определения основных факторов риска, которые определяют серьезность травмы.

Изучение и предварительная обработка данных

Одна из основных проблем при работе с этим набором данных заключается в том, что он в высшей степени несбалансирован. Мы решили эту проблему, выбрав подмножество данных, по сути занижая выборку данных, чтобы гарантировать, что мы сможем построить наши модели на сбалансированном наборе данных. После загрузки этих файлов поднабора в фрейм данных наш фрейм данных содержал 543230 образцов и 116 функций. Как и в большинстве случаев исследования данных, одним из первых шагов, которые мы сделали, было изучение заголовка фрейма данных.

df.head()

Как мы можем видеть в заголовке фрейма данных, похоже, что наши данные содержат значительное количество нулевых значений. Модели не могут быть обучены на данных, содержащих нулевые значения, поэтому это аспект данных, требующий обширного дальнейшего изучения.

df.isna().sum()

При изучении словаря данных мы обнаружили, что некоторые функции содержат нулевые значения, в которых должен был быть записан отрицательный ответ или конкретная категория. Таким образом, мы потратили много времени на изучение каждой функции по отдельности, прежде чем решить, как обрабатывать нулевые значения для этой конкретной функции. Мы также решили удалить некоторые функции и строки из фрейма данных из-за большого количества нулевых значений, содержащихся в этих конкретных функциях или строках.

Фрейм данных также состоял из ряда категориальных функций, которые имели слишком много категорий для эффективного и действенного применения оперативного кодирования. С помощью этих функций мы выполнили сокращение категории, изучив каждое значение функции по отдельности и сгруппировав похожие значения в одну категорию значений.

После обобщения этих функций были нанесены на карту некоторые из наиболее интересных особенностей. Графики распределения этих функций показаны ниже.

# Candidates for plotting - features:
f, axes = plt.subplots(4, 2, figsize=(12,16))
sns.distplot( X_train_corr['party_age'], color='g', bins = 30, 
              ax=axes[0, 0]).set_title("party age - distplot");
sns.distplot( X_train_corr['victim_age'], color='g', bins = 30,  
              ax=axes[0, 1]).set_title("victim age - distplot");
sns.countplot(ax=axes[1, 0], x = y_train, data= X_train_corr, 
              palette="magma", order=list(range(8)))
              .set_title("victim_degree_of_injury");
sns.countplot(ax=axes[1, 1], x = X_train_corr["party_race"], 
              data= X_train_corr, palette="magma")
              .set_title("party_race");
sns.countplot(ax=axes[2, 0], y = X_train_corr["type_of_collision"], 
              data= X_train_corr, palette="magma")
              .set_title("Type of collision");
sns.countplot(ax=axes[2, 1], y =  
              X_train_corr["motor_vehicle_involved_with"], 
              data= X_train_corr, palette="magma")
              .set_title("Motor car involved with");
sns.kdeplot( df['collision_time'], color='r', ax=axes[3, 0], 
             shade = True).set_title("Collision time for all accidents");
sns.countplot(ax=axes[3, 1], x = X_train_corr["weather_1"], 
              data= X_train_corr, palette="magma")
              .set_title("weather at time of accident");
plt.yscale("log")
plt.tight_layout()
plt.show()

Что касается числовых характеристик, одной из наших основных задач было надлежащее управление выбросами. Анализируя особенности по отдельности, мы отметили те, которые были кандидатами на сокращение выбросов, и построили каждую в виде коробчатой ​​диаграммы для визуального представления выбросов.

# Outlier boxplots
potential_outlier_features = ['injured_victims', 'party_count', 'party_number', 'vehicle_year', 'party_age', 'victim_age']
f, axes = plt.subplots(3, 2, figsize=(10,5))
list(itertools.product(range(3), range(2)))
for loc, feat in zip(locs, potential_outlier_features):
    sns.boxplot(x=feat, data=X_train, ax = axes[loc], color = "dodgerblue")
axes[loc].set_xlabel(feat, fontsize = 16)
plt.tight_layout()

Мы удалили выбросы из этих функций на основе следующих пороговых значений:

# Remove outliers
X_train = X_train[X_train['injured_victims'] <= 40]
X_train = X_train[X_train['party_count'] <= 30]
X_train = X_train[X_train['party_number'] <= 30]
X_train = X_train[X_train['vehicle_year'] >= 1920]
X_train = X_train[X_train['party_age'] <= 100]
X_train = X_train[X_train['victim_age'] <= 100]

Мы также исследовали корреляцию признаков, используя корреляционную матрицу, но определили, что ни одна из оставшихся числовых характеристик не коррелировала до такой степени, которая могла бы способствовать удалению.

# Correlation plot:
f, axes = plt.subplots(figsize=(12,12))
corr = X_train.corr()
mask = np.tril(np.ones_like(corr, dtype=np.bool))
sns.heatmap(corr, annot=True, fmt=".2f", mask = mask, square = True,  
            cmap="bwr_r",vmin=-1, vmax=1, center = 0)
plt.title('Correlation between selected numerical features', size = 15);
plt.show()

После преобразования оставшихся категориальных функций с использованием фиктивного кодирования наш фрейм данных содержал 213 функций. Мы сочли разумным выполнить выбор характеристик по критерию хи-квадрат для категориальных характеристик, сохранив при этом 60 основных характеристик.

# Keep top k categorical features
k = 60
fs = SelectKBest(score_func=chi2, k=k)
fs.fit(train[cat_w_dummies], y_train)
train_fs = fs.transform(train[cat_w_dummies])
test_fs = fs.transform(test[cat_w_dummies])
# Plot features by importance
fig, ax = plt.subplots(figsize=(18,35))
sns.barplot(y=cat_w_dummies[:k],x=sorted(fs.scores_, reverse=True)[:k])
plt.title("Chi-Square Scores for the Different Features", fontsize=30)
plt.ylabel("Feature", fontsize=30)
plt.xlabel("Chi-Score", fontsize=30)
plt.xticks(fontsize=30)
plt.yticks(fontsize=45)
plt.show()

Все эти исследования и предварительная обработка данных привели нас к финальным обучающим и тестовым кадрам данных, содержащим 72 функции, к которым мы применили стандартное масштабирование.

Классификация цели

Изначально целевая функция состояла из 8 категорий. Поскольку некоторые из этих категорий были повторяющимися или неоднозначными, мы переопределили нашу цель, чтобы она содержала 4 категории. Эти категории следующие:

0 - No injury       (Originally category 0)
1 - Mild injury     (Originally categories 6 & 7)
2 - Severe injury   (Originally categories 2 & 5)
3 - Killed          (Originally category 1)

Создание модели

Мы построили несколько моделей для этого проекта, начиная с простой базовой модели и постепенно усложняя модель, стремясь найти модель с наилучшими характеристиками. Разработанные нами модели включали логистическую регрессию, случайный лес, SVM, k-ближайших соседей, CatBoost, XGBoost и нейронную сеть. Мы обсудим результаты трех из этих моделей.

Базовая модель: логистическая регрессия

Наша базовая модель была простой моделью логистической регрессии. В этой модели показатель f1 уменьшался по мере увеличения класса, что означает, что модель могла предсказать отсутствие травмы лучше, чем легкая травма, легкая травма лучше, чем тяжелая травма, и тяжелая травма лучше, чем несчастный случай.

log_model = LogisticRegression(solver = "lbfgs", max_iter = 2000)
log_model.fit(X_train_scaled, y_train)
y_pred_logr = log_model.predict(X_test_scaled)
f, axes = plt.subplots(figsize=(6,6))
plot_confusion_matrix(log_model, X_test_scaled, y_test,ax = axes, 
                      cmap = 'YlOrBr', values_format = '.0f');

print(classification_report(y_test, y_pred_logr))

Классификатор случайного леса

Мы также создали классификатор случайного леса и использовали рандомизированный поиск для получения лучших параметров модели. Классификатор случайных лесов в целом показал лучшие результаты, чем базовая модель, со средней точностью 0,76. Точность была относительно высокой для отсутствия травм, легких травм и несчастных случаев, но модель изо всех сил пыталась предсказать серьезную травму с точностью. Кроме того, процент отзыва по легким травмам и несчастным случаям был намного ниже, чем нам хотелось бы запустить в производство.

parameters = {'n_estimators': 100,
              'min_samples_split': 6,
              'min_samples_leaf': 5,
              'max_depth': 11}
clf_RF = RandomForestClassifier(**parameters) 
clf_RF.fit(X_train_scaled, y_train)
y_pred_RF = clf_RF.predict(X_test_scaled)

XGBoost

Модель XGBoost была нашим лучшим исполнителем. Эта модель получила средневзвешенный показатель точности 0,81, средневзвешенный показатель отзыва 0,79 и средний балл f1 0,78. Он показал хорошие результаты при прогнозировании отсутствия травм, легких травм и травм с высокой точностью, но не смог точно предсказать тяжелые травмы. Кроме того, показатели отзыва по отсутствию травм и легкой травме были очень высокими, но оценка по отзыву для раненых была довольно низкой.

dtrain = xgb.DMatrix(data=X_train_scaled, label=y_train)
dtest = xgb.DMatrix(data=X_test_scaled)
params = {
    'max_depth': 3,
    'objective': 'multi:softmax',  # error evaluation for 
                                   # multiclass training
    'num_class': 4}
bst = xgb.train(params, dtrain)
pred_xgb = bst.predict(dtest)

Интерпретация результатов

Как видно из приведенных ниже матриц неточностей, все модели, которые мы оценивали, не могли отличить тяжелую травму (категория 2) от несчастного случая (категория 3). Мы считаем эту ошибку приемлемой, потому что, если модель прогнозирует серьезную травму, сотрудники службы экстренного реагирования сделают этот вызов приоритетным. Если бы ошибка была обращена вспять, это было бы неприемлемо, поскольку это могло бы лишить заботы тех, кто в ней больше всего нуждается. Классифицируя пострадавшие как тяжелые травмы, мы можем увидеть меньше пострадавших, так как службы быстрого реагирования могут прибыть вовремя, чтобы стабилизировать состояние пострадавших и потенциально спасти их жизни. Изображение ниже ясно показывает, что XGBoost превзошел другие модели.

Сложность, с которой наши модели столкнулись при точном прогнозировании пострадавших с тяжелыми травмами, дополнительно проиллюстрирована в таблице ниже. Опять же, это не слишком беспокоит нас, потому что неточность связана с тем, что те, кто был внесен в список пострадавших, были классифицированы как серьезно раненые. Наихудший сценарий, который может произойти из-за этой неправильной классификации, - это то, что на место происшествия будет отправлена ​​скорая помощь, чтобы оказать помощь, которая больше не нужна.

XGBoost принял свои классификационные решения на основе следующих функций:

Как видите, количество раненых было основным показателем, использованным при определении результата классификации. Мы обсуждали удаление этой функции, потому что мы не хотели по существу сообщать модели ее цель, но решили включить эту функцию по двум причинам. Во-первых, хотя это и дает модели ключ к пониманию того, существуют ли травмы или нет, мы пытаемся предсказать серьезность травм, и он не дает никаких мер по этой шкале. Во-вторых, когда кто-то звонит в службу экстренной помощи после аварии, можно ожидать, что оператор службы экстренной помощи спросит звонящего, ранен ли кто-нибудь или выглядит раненым, и если да, то сколько. Поскольку это информация, которую можно собрать до прибытия на место происшествия первых респондентов, мы сочли необходимым и целесообразным включить ее в модель.

Хотя мы не можем утверждать, почему модель отдает приоритет функциям именно таким образом, у нас есть некоторые предположения по этому поводу. Например, год может быть фактором, потому что с течением времени в большинство используемых транспортных средств интегрируется больше функций безопасности. В качестве альтернативы, возможно, были введены новые законы и постановления, касающиеся, например, использования сотовых телефонов. Когда мы оцениваем функцию, описывающую катапультирование жертвы, мы можем ясно видеть, что гораздо больший процент людей, которые использовали ремни безопасности, не были травмированы, по сравнению с теми, кто не использовал ремень безопасности.

Кроме того, хотя сохраняется заблуждение о том, что мужчины водят лучше, чем женщины, данные показывают, что 76 процентов погибших составили мужчины.

Одно откровение, полученное при наблюдении за разницей между уровнем несчастных случаев в будние дни и в выходные, может намекать на причастность лишения сна и / или употребления алкоголя к смертельным столкновениям. Как видно на графике ниже, наблюдается значительный всплеск количества жертв по выходным, в ранние утренние часы, когда закрываются бары и клубы.

Также интересно отметить, что в феврале 2019 года в районе Лос-Анджелеса было гораздо меньше жертв, чем в любой другой месяц того же года.

Заключение

После выполнения полного исследовательского анализа данных, предварительной обработки и моделирования для изучения столкновений транспортных средств в штате Калифорния мы смогли найти много интересных дополнительных компонентов, как указано выше. Мы призываем тех, кому эта статья показалась интересной, провести собственные исследования и поделиться с нами своими выводами.

Полный код этого проекта можно найти на GitHub.