Общий обзор
В США личные автомобили остаются наиболее распространенным видом транспорта. Большинство из нас зависит от приложений маршрутизации и карт, чтобы добраться до новых мест или даже просто для быстрой проверки условий движения. Одной из самых удобных функций этих приложений является просмотр мест автомобильных аварий в режиме реального времени и их влияние на время в пути.
Почти каждый день перед уходом с работы я проверяю пробки, чтобы понять, по какой автостраде мне следует добраться до дома. Эта функция отлично подходит для немедленных проверок, но может ли она работать для поездок, запланированных на ближайшее время? Во время экспериментов с картами Google я не мог видеть данные об авариях при использовании функции «Выезд в» более чем за 2 часа до времени (правда, это был ограниченный эксперимент).
Если бы приложения для маршрутизации могли надежно предсказать, как долго авария повлияет на трафик, они, вероятно, могли бы учитывать аварии для поездок, запланированных в ближайшем будущем. Можем ли мы мы предсказать, как долго та или иная авария будет влиять на движение транспорта?
Описание входных данных
Мы будем использовать набор данных US Accidents 2016–2021 Dataset.
Этот набор данных содержит следующую информацию о более чем 2,8 миллиона дорожно-транспортных происшествий в США:
- Тяжесть аварии
- Краткое описание
- Подробные погодные условия (видимость, влажность, осадки, температура, охлаждение ветром, тип погоды)
- Ближайшие дорожные объекты (неровности, перекрестки, перекрестки, благоустройство, перекрестки и т. д.)
- Местоположение (широта/долгота, штат, округ, почтовый индекс)
- Время (часовой пояс, время начала аварии, время окончания аварии, дневное/ночное время)
Стратегия решения проблемы
Приведенный выше набор данных содержит информацию, которую можно узнать, как только будет обнаружено или сообщено о происшествии, — погодные условия, время, местоположение и особенности близлежащих дорог. Используя эти данные, мы можем обучить модель для прогнозирования продолжительности, в течение которой авария влияет на трафик.
Для этого нам нужна целевая переменная для прогнозирования! К счастью, в нашем наборе данных столбцы Start_Time и End_Time — это именно то, что нам нужно — значение End_Time описывается как «когда влияние аварии на транспортном потоке прекращено».
Шаги для решения этой проблемы:
- Читайте в наших данных
- Создайте нашу целевую переменную, продолжительность аварии, из значений времени начала и окончания.
- Очистите и обработайте данные по мере необходимости
- Выполните исследовательский анализ, чтобы узнать больше о наших данных и доступных функциях, и решите, какие функции следует сохранить.
- Обучите модель, используя наш набор данных, для прогнозирования продолжительности дорожно-транспортного происшествия, оценивая производительность и настраивая гиперпараметры и функции по мере необходимости.
- Создайте приложение для ручного запроса несчастных случаев и отображения прогнозов продолжительности
Причина того, что приложение использует ручные запросы, заключается в том, что у меня нет доступа к API для получения информации о ближайших дорогах. Теоретически эта модель должна быть в состоянии вписаться в любое существующее приложение, которое может получать такую информацию с учетом местоположения, и что-то вроде Aviation Weather API можно использовать для получения подробных погодных условий с учетом ближайшего аэропорта к месту аварии.
Обсуждение ожидаемого решения
Мы должны рассчитывать на модель, которая учитывает выбранные нами функции и предсказывает продолжительность аварии с разумной степенью точности.
Из-за характера этой проблемы мы можем ожидать высокую дисперсию. Ни одна из используемых функций не будет являться данными о фактическом происшествии, а скорее об окружающих его условиях. Это ограничение для прогнозирования во время аварии без ввода информации человеком. Погода, время, было ли это рядом с перекрестком или нет и т. д. говорят нам только об обстоятельствах. Эти функции могут косвенно информировать о вещах, которые могут повлиять на то, сколько времени потребуется, чтобы авария была устранена: время реагирования на чрезвычайные ситуации, большой поток автомобилей, доступность буксировки и т. д.
Имея в виду вышеизложенное, мы можем соответствующим образом установить наши ожидания и подумать о функциях в правильном кадрировании.
Метрики с обоснованием
Учитывая, что мы будем использовать регрессионную модель, использования оценки R2 модели в наборе данных для обучения и тестирования будет достаточно для определения производительности. Я не ожидаю очень высокого R2 в этих данных, учитывая возможность дисперсии, но, конечно, должна быть некоторая корреляция, чтобы наша модель была полезной.
Исследовательский анализ данных
Давайте сначала посмотрим на запись в нашем наборе данных:
Глядя на одну запись, можно легко получить приблизительное представление о том, какие данные представлены и в каком типе данных они хранятся. Например, мы можем видеть, что:
- Серьезность — это категориальное
- Записи времени являются строками и должны быть обработаны перед использованием.
- Описание не обязательно содержит дополнительную информацию.
- Число имеет нулевые значения
- Дорожные объекты сохраняются как True/False
Обработка нулей
Давайте посмотрим, сколько нулевых значений у нас есть во всех столбцах:
#check where cols are null df.isnull().sum().sort_values(ascending=False) Number 1743911 Precipitation(in) 549458 Wind_Chill(F) 469643 Wind_Speed(mph) 157944 Wind_Direction 73775 Humidity(%) 73092 Weather_Condition 70636 Visibility(mi) 70546 Temperature(F) 69274 Pressure(in) 59200 Weather_Timestamp 50736 Airport_Code 9549 Timezone 3659 Nautical_Twilight 2867 Civil_Twilight 2867 Sunrise_Sunset 2867 Astronomical_Twilight 2867 Zipcode 1319 City 137 Street 2
К счастью для нас, наибольшее нулевое значение — это Number, которое относится только к почтовому адресу — это не имеет значения для нашей модели. Однако в параметре Осадки также много пустых значений. Возможно, это нулевое значение там, где просто нет дождя/снега. Проверка значений столбца Weather_Condition может подтвердить это:
#check weather_condition values where precipitation is null df[df["Precipitation(in)"].isnull()]["Weather_Condition"].value_counts().sort_values(ascending=False).head(10) Clear 172786 Overcast 72357 Mostly Cloudy 72029 Partly Cloudy 51271 Fair 45103 Scattered Clouds 44052 Cloudy 8959 Haze 6675 Light Rain 3908 Fog 3230
Выглядит неплохо! Мы можем принять значение 0,0 для осадков, где оно равно нулю. Даже для погодных условий «Небольшой дождь» это может объяснить, где дождь прекратился, но на улице все еще влажно.
Перемещаясь по другим нулевым значениям, мы видим, что охлаждение ветром, скорость и направление имеют нули. Охлаждение ветром, скорее всего, не будет использоваться в нашей модели, поскольку эта функция не всегда присутствует в более теплых штатах/сезонах. Направление ветра также не будет использоваться, так как его важность зависит от направления аварии (и интуитивно должно иметь незначительное влияние). Проверка погодных условий с нулевой скоростью ветра, аналогичных приведенным выше, возвращает:
Clear 37935 Overcast 13077 Fair 10204 Mostly Cloudy 9383 Partly Cloudy 6630 Scattered Clouds 5615 Light Rain 3157 Cloudy 2288 Haze 1994 Light Snow 1514
Точно так же можно предположить, что нулевая скорость ветра - это спокойные условия или скорость ветра 0 миль в час.
Для нулевых значений Weather_Condition, к сожалению, нет умного способа вменить эти значения. Учитывая, что это небольшая выборка нашего набора данных (0,024), мы можем удалить эти записи. Проверяя наши нули после этого сброса и приведенных выше вменений, мы видим:
Number 1697490 Wind_Chill(F) 406544 Wind_Direction 17505 Humidity(%) 15183 Temperature(F) 11412 Visibility(mi) 7268 Pressure(in) 3594 Nautical_Twilight 2256 Civil_Twilight 2256 Sunrise_Sunset 2256 Astronomical_Twilight 2256 City 131 Street 1
Намного лучше! Мы можем отбросить значения параметров Temperature, Visibility и Sunrise_Sunset, равные нулю, учитывая, как мало записей содержат их. Остальные столбцы в нашей модели использоваться не будут.
Анализ и создание функций
Нам нужно создать нашу функцию продолжительности аварии с учетом значений Start_Time и End_Time, что мы делаем с помощью функции ниже:
def time_diff(row): ''' Inputs: row - dataframe row containing "End_Time" and "Start_Time" columns with strings containing datetime information Outputs: diff (float) - new value that is difference between start & end, expressed in minutes accounts for diff length/spots after decimal 2016-02-08 00:37:08 2021-11-11 19:36:30.000000000 2021-10-23 15:50:00.000000 ''' format_t = "%Y-%m-%d %H:%M:%S" if len(row["End_Time"]) == 19: diff = (datetime.strptime(row["End_Time"], format_t) - datetime.strptime(row["Start_Time"], format_t)) elif len(row["End_Time"]) == 26: diff = (datetime.strptime(row["End_Time"][:-7], format_t) - datetime.strptime(row["Start_Time"][:-7], format_t)) elif len(row["End_Time"]) == 29: diff = (datetime.strptime(row["End_Time"][:-10], format_t) - datetime.strptime(row["Start_Time"][:-10], format_t)) diff = diff.total_seconds() / 60 return diff
Набор данных содержал три разных формата времени, различающихся количеством цифр после запятой — отсюда и логика в функции. Мы можем создать новый столбец с помощью этой функции, например:
#create new column from above function df["duration(min)"] = df.apply(lambda row: time_diff(row), axis=1)
А теперь, исследуя продолжительность:
df["duration(min)"].describe() count 2.774706e+06 mean 3.543284e+02 std 9.216739e+03 min 2.000000e+00 25% 7.375000e+01 50% 1.200000e+02 75% 2.237000e+02 max 1.682579e+06 df["duration(min)"].value_counts().head(20) 360.000000 343307 240.000000 54464 15.000000 36366 30.000000 34581 60.000000 27902 75.000000 25449 105.000000 24223 45.000000 18323 120.000000 16202 20.000000 12749 34.966667 11118 75.016667 10727 59.000000 7617 49.966667 6059 20.500000 5667 165.000000 5525 78.016667 5039 21.000000 4728 29.500000 4239 64.966667 4206
Это выглядит немного прикольно! Наш максимум слишком высок, чтобы быть разумным, и существует ненормальное количество записей продолжительностью 360,0 минут. Во-первых, мы установим продолжительность отсечки, после чего мы удаляем записи. Есть 19291 запись, продолжительность которых превышает 1 полный день (1440 минут), поэтому мы их исключим. Теперь смотрим на дистрибутив:
Мы наблюдаем всплеск на 360-й минуте. Это усиливает теорию о том, что это значение по умолчанию. Эти записи нельзя использовать в нашей модели, поэтому мы их отбросим, что приведет к следующему распределению:
Теперь, когда у нас есть чистая зависимая переменная, мы можем сосредоточиться на наших функциях. Сохраняя значения Start_Time как дату и время в столбце Time_of_Incident , мы можем создавать функции Day и Hour:
#create column of day of week to get dummies for df["Day"] = df.apply(lambda row: row.Time_of_Incident.day_name(), axis=1) #same for hour of the day df["Hour"] = df.apply(lambda row: row.Time_of_Incident.hour, axis=1)
Глядя на столбец Weather_Condition, можно увидеть 117 уникальных значений! Когда мы в конце концов закодируем это, это будет означать 117 функций. Глядя на значения, мы видим, что есть некоторое совпадение:
#check the most common 25 weather vals df["Weather_Condition"].value_counts().sort_values(ascending=False).head(25) Fair 1095690 Cloudy 344926 Mostly Cloudy 310864 Partly Cloudy 213764 Light Rain 111852 Clear 46329 Fog 39659 Light Snow 37945 Haze 32617 Overcast 29347 Rain 27171 Fair / Windy 14928 Scattered Clouds 10620 Heavy Rain 10520 Thunder in the Vicinity 6901 Smoke 6711 Cloudy / Windy 6705 T-Storm 6512 Mostly Cloudy / Windy 6216 Thunder 5986 Light Drizzle 5969 Light Rain with Thunder 5247 Snow 4521 Partly Cloudy / Windy 3810 Wintry Mix 3794
Ясно, ясно, ясно / ветрено, все одинаковы, и аналогично со значениями облачности. Чтобы исправить это, я создал словарь с ключами в качестве общих слов и значениями в качестве новой сортировки погодных условий. Затем, используя функцию ниже, создал новый столбец только с 10 категориями:
#create new weather column that narrows possible values def narrow_weather(row): #define keywords to categorize by keywords = { "rain" : "rain", "storm" : "storm", "drizzle" : "rain", "snow" : "snow", "sleet" : "snow", "fair" : "fair", "clear" : "fair", "windy" : "windy", "fog" : "fog", "haze" : "fog", "hail" : "hail", "thunder" : "storm", "overcast" : "cloudy", "cloud" : "cloudy", "wintry" : "snow", "drizzle" : "rain", "mist" : "fog", "smoke" : "smoke", "shower" : "rain", "precipitation" : "rain", "dust" : "dust", "ice" : "hail", "sand" : "dust", "squall" : "storm", } entry = np.nan for k, v in keywords.items(): if k in row["Weather_Condition"].lower(): entry = v return entry df["weather"] = df.apply(lambda row: narrow_weather(row), axis=1)
Теперь исследуем значения скорости ветра:
df["Wind_Speed(mph)"].describe() count 2.353551e+06 mean 7.177552e+00 std 5.489298e+00 min 0.000000e+00 25% 3.000000e+00 50% 7.000000e+00 75% 1.000000e+01 max 1.087000e+03
У нас снова очень высокий максимум! Глядя на самые высокие значения:
df["Wind_Speed(mph)"].sort_values(ascending=False).head(10) 1486460 1087.0 2225067 984.0 710258 812.0 2311806 518.0 2311795 518.0 1414359 243.0 2104007 232.0 1955910 211.0 1417386 186.0 1224096 186.0 df[df["Wind_Speed(mph)"] > 75].shape[0] 61
Кажется, что у нас всего несколько выбросов, и всего 61 запись со скоростью выше 75 миль в час. Мы будем использовать это как точку отсечки, а затем посмотрим на распределение:
Аналогичным образом, если посмотреть на температуру, у нас есть максимальное значение 196 градусов по Фаренгейту (выше, чем самая высокая зарегистрированная температура в мире!) и 8 значений выше 120 градусов по Фаренгейту, поэтому мы будем обрабатывать их путем отбрасывания.
Теперь, когда у нас есть чистые функции, мы можем начать анализировать отношения между нашими функциями и продолжительностью аварии! Используя метод DataFrame corr, мы можем получить первый взгляд:
#return highest correlation features #abs used because we care about the importance, not the direction df.corr(numeric_only=True)["duration(min)"].abs().sort_values(ascending=False) duration(min) 1.000000 Hour 0.106259 Distance(mi) 0.079753 Severity 0.059988 End_Lat 0.051920 Start_Lat 0.051916 Junction 0.051709 Station 0.044890 Traffic_Signal 0.043059 Pressure(in) 0.034518 Temperature(F) 0.031535 Number 0.028719 Humidity(%) 0.027520 Wind_Chill(F) 0.026542 Crossing 0.017282 Precipitation(in) 0.011894 End_Lng 0.011672 Start_Lng 0.011655 Amenity 0.009659 Visibility(mi) 0.005732 Wind_Speed(mph) 0.003740 No_Exit 0.003130 Stop 0.002897 Give_Way 0.002889 Railway 0.002466 Bump 0.002106 Traffic_Calming 0.001607 Roundabout 0.000793 Turning_Loop NaN
К сожалению, некоторые из наших функций с высокой корреляцией — это вещи, которые мы не можем использовать в нашей модели:
- Расстояние (мили) — это мера длины дороги, на которой затруднено движение транспорта. Возможно, мы сможем узнать об этом через несколько минут после аварии, но не в момент ее обнаружения.
- Серьезность — это мера того, как долго трафик подвергается воздействию. Я удивлен, что это значение не выше! Но, естественно, его нельзя использовать.
Интересно, что наша широта имеет некоторую корреляцию с продолжительностью! Нанося широту/долготу на диаграмму рассеяния цветом, обозначающим продолжительность аварии, мы получаем:
Из этого мы можем понять, почему широта имеет значение — но, скорее всего, существует более сильная корреляция с состоянием, в котором происходит авария! Построив график средней продолжительности по состояниям, получим:
Это подтверждает, что штат действительно влияет на продолжительность аварии — в Западной Вирджинии и Орегоне продолжительность аварии гораздо выше, чем в большинстве других штатов. Мы можем упорядочить состояния по средней продолжительности в серии pandas, а затем создать новый столбец фрейма данных с индексом. Пересчет корреляции после должен показать нам влияние государства на продолжительность аварии:
#lets ordinally encode state column and re-run df.corr() to see how heavily state matters #compute mean mean_duration = df.groupby('State')["duration(min)"].mean() #order above by mean duration mean_duration = mean_duration.sort_values(ascending=True) states_ordered = mean_duration.index #new column in dataframe mapped from states_ordered df["state_ordered"] = df["State"].map(lambda x: states_ordered.get_loc(x)) #return highest correlation features df.corr(numeric_only=True)["duration(min)"].abs().sort_values(ascending=False) duration(min) 1.000000 state_ordered 0.173288 Hour 0.106259 Distance(mi) 0.079753 ...
И посмотри на это! У нас есть самая высокая корреляционная характеристика.
Теперь мы можем легко построить график продолжительности на основе объекта дороги, создав новый фрейм данных для хранения каждого объекта дороги, средней продолжительности и наличия объекта дороги в виде логического значения:
#plot the road feature categoricals all on one plot cat_df = pd.DataFrame(columns=["category","t_f","mean_duration"]) for val in all_categoricals: for bool_val in [True, False]: entry = [val, bool_val, df[df[val] == bool_val]["duration(min)"].mean()] cat_df.loc[len(cat_df)] = entry sns.barplot(data=cat_df, x="category", y="mean_duration", hue="t_f").set_title("Road Feature Presence vs. Mean Duration") plt.xticks(rotation=45) plt.savefig("static/road_features.png",bbox_inches="tight")
Из этого мы можем видеть, что большинство дорожных особенностей оказывают некоторое влияние. Turning_Loop содержит только ложные записи и может быть исключен из нашего набора данных.
Ниже приведены гистограммы и диаграммы рассеяния некоторых других функций по сравнению с продолжительностью:
Важно учитывать, что для наших вышеперечисленных функций измеряемая продолжительность — это продолжительность воздействия на трафик. В ситуациях, когда дорожное движение изначально очень слабое (3 часа ночи, сельская местность, экстремальная погода и т. д.), многие аварии могут иметь незначительное влияние на движение или вообще не влиять на него. Это не повредит нашей модели или цели, но об этом следует помнить при рационализации взаимосвязей в данных.
Предварительная обработка данных
К счастью, большая часть наших данных уже хранится в подходящем формате для использования в нашей модели. Единственное, что нам нужно сделать, это сразу закодировать наши категориальные переменные. Для такого столбца, как Junction, который хранится как True/False, мы просто преобразуем его в 0 для False и 1 для True. Для такого столбца, как погода, который имеет 10 возможных значений, мы разделяем его на столбец для каждого значения и заполняем 1 там, где значение присутствует, и 0 в других местах. Если бы мы не сузили категории погоды раньше, это привело бы к 117 функциям в нашей модели для просто погоды! Мы можем легко достичь вышеуказанных целей, используя pandas.get_dummies:
#columns we want to keep for our model, split by continuous and categorical con_features = ["Temperature(F)", "Visibility(mi)", "Wind_Speed(mph)", "Precipitation(in)"] cat_features = ["state_ordered","weather", "Junction", "Stop", "Traffic_Signal", "Sunrise_Sunset", "Day", "Hour", "Station", "Crossing"] val_to_predict = ["duration(min)"] #initialize X with continuous features, then merge with one hot encoded categoricals X = df[con_features] X = X.join(pd.get_dummies(df[cat_features])) #initialize Y with the column we are trying to predict Y = df[val_to_predict] #use numpy ravel to set y to correct dimensions Y = np.array(Y).ravel() #split into test and train sets x_train, x_test, y_train, y_test = train_test_split(X, Y, train_size=0.8)
Метод get_dummies, используемый для столбцов Hour и state_ordered, не должен ничего делать — это потому, что мы изначально создали эти столбцы как целочисленное представление. Для state_ordered это было сделано намеренно во время EDA по той же причине, что и для столбца погода. Мы можем использовать штат США в нашей модели, не добавляя таким образом около 50 функций!
Поскольку прогнозируемая переменная не должна зависеть от времени, случайное разбиение на обучающие и тестовые наборы допустимо.
Моделирование
Теперь, когда наши данные подготовлены, мы можем начать моделирование. Я начинаю с GradientBoostingRegressor от sklearn без каких-либо изменений параметров, чтобы получить базовую производительность:
#instantiate model reg = GradientBoostingRegressor(random_state=42) #fit to training data reg.fit(x_train, y_train) #predict on test data y_pred = reg.predict(x_test) #score on test data reg.score(x_test, y_test) 0.12270359181619717 #score on train data reg.score(x_train, y_train) 0.12274743774121522
Не выглядит слишком большим вне летучей мыши! По крайней мере, похоже, что мы не подгоняем наши тренировочные данные — показатель R2 по сравнению с тренировочным и тестовым набором почти идентичен, но слишком низок, чтобы можно было быть уверенным в нашей точности.
Хотя мы могли начать настройку гиперпараметров здесь, я думаю, что базовая производительность не является многообещающей, и было бы полезнее снова подумать о нашей проблеме. Предполагаемый вариант использования этой модели — будущая маршрутизация. Переменная, которую мы пытаемся предсказать, продолжительность аварии, непрерывна и, возможно, слишком детализирована. Вместо того, чтобы предсказывать точную продолжительность, мы могли бы попытаться предсказать диапазон и переключиться на проблему классификации!
Имея в виду вышеизложенное, мы можем создать новый столбец фрейма данных, который является порядковым категориальным значением на основе продолжительности аварии:
#create new ETA column based off of duration(min) val #15mins or less = 0 #15-30mins = 1 #30mins-1hr = 2 #1-3hr = 3 #3hr-6hr = 4 #rest of day = 5 df['ETA'] = 0 df.loc[df['duration(min)'] <= 15, 'ETA'] = 0 df.loc[(df['duration(min)'] <= 30) & (df["duration(min)"] > 15), 'ETA'] = 1 df.loc[(df['duration(min)'] <= 60) & (df["duration(min)"] > 30), 'ETA'] = 2 df.loc[(df['duration(min)'] <= 180) & (df["duration(min)"] > 60), 'ETA'] = 3 df.loc[(df['duration(min)'] <= 360) & (df["duration(min)"] > 180), 'ETA'] = 4 df.loc[df['duration(min)'] > 360, 'ETA'] = 5
и заново создайте наши обучающие/тестовые наборы (тот же код, что и выше, но в функции, которую нужно набирать меньше):
#functionize encoding def prep_data(df): con_vals = ["Temperature(F)", "Visibility(mi)", "Wind_Speed(mph)", "Precipitation(in)"] cat_vals = ["state_ordered","weather", "Junction", "Stop", "Traffic_Signal", "Sunrise_Sunset", "Day", "Hour", "Station", "Crossing"] x = df[con_vals] y = df['ETA'] x = x.join(pd.get_dummies(df[cat_vals])) y = np.array(y).ravel() x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8) return x_train, x_test, y_train, y_test x_train, x_test, y_train, y_test = prep_data(df)
Теперь мы можем создать экземпляр модели классификации — я решил использовать классификатор XGBoost с целью multi:softprob. Это потому, что я хотел быть уверенным в каждой классификации, а также в самой классификации:
#set XGBoost params, objects #xgboost DMatrix objects can be easily set with the split data train = xgb.DMatrix(data=x_train, label=y_train) test = xgb.DMatrix(data=x_test, label=y_test) params = { "eta" : 0.05, #learning rate param "objective" : "multi:softprob", #provides confidence of each category "num_class" : 6, #number of categories in predicted variable "max_depth" : 15, #depth of trees } epochs = 50 #train model model = xgb.train(params, train, epochs) #predict on test set y_pred = model.predict(test) #get accuracy score. np.argmax is used because #the output of predict contains array of confidence for each category accuracy_score(y_test, np.argmax(y_pred,axis=1)) 0.6103670199881391
Вышеупомянутое выполнение заняло около 8 минут, и мы получили оценку точности 0,61 на тестовом наборе! Важно отметить, что это показатель, отличный от показателя R2 в регрессионной модели. Это можно интерпретировать как 61% шанс правильно классифицировать аварию.
Настройка гиперпараметров
Теперь, когда у нас есть приличная базовая производительность, мы можем использовать GridSearchCV из научного набора для настройки гиперпараметров и повышения производительности:
estimator = xgb.XGBClassifier(objective="multi:softprob", seed=42) params = { "max_depth" : range(2, 20, 4), "n_estimators" : range(50, 250, 50), "learning_rate" : [0.2, 0.1, 0.05] } #create grid_search object with above parameters grid_search = GridSearchCV( estimator=estimator, param_grid=params, n_jobs = -1, #run on all cores cv = 2, #cross validation verbose=3 ) #fit to train data grid_search.fit(x_train, y_train)
Обычно я бы использовал более высокое значение перекрестной проверки. Однако этот набор данных большой и менее подвержен переоснащению обучающих данных, поэтому я могу обойтись меньшим значением и сэкономить массу времени вычислений, поскольку приведенное выше значение в 120 раз больше!
Я также перебираю значение max_depth — это изменяет глубину дерева. Чем выше глубина, тем сложнее становится наша модель и тем точнее она становится, но больше подвержена переоснащению. То же самое можно сказать и о n_estimators — это изменяет количество деревьев, а слишком большое их количество может привести к переоснащению.
Значение learning_rate влияет на то, насколько быстро модель обновляет веса в деревьях. Более низкое значение может привести к более точной модели, но увеличивает время вычислений и требует больше времени для сходимости, а также рискует получить переоснащение.
Полученные результаты
После запуска оптимальная модель имела точность 0,63 на тренировочном наборе и имела параметры:
- скорость обучения: 0,1
- макс_глубина: 18
- n_оценщиков: 200
Это небольшое увеличение производительности по сравнению с нашим базовым уровнем, но все же увеличение! Точность, полнота и f1-баллы для каждой категории в тестовом наборе данных можно увидеть ниже:
precision recall f1-score support 0 0.82 0.30 0.44 12082 1 0.91 0.53 0.67 61317 2 0.79 0.27 0.41 52695 3 0.68 0.97 0.80 264520 4 0.81 0.35 0.49 57547 5 0.83 0.56 0.67 30723 accuracy 0.72 478884 macro avg 0.81 0.50 0.58 478884 weighted avg 0.75 0.72 0.69 478884
Наши значения точности все приличные, за исключением категории 3. Мы видим, что отзыв здесь очень высок. Глядя на поддержку, мы видим, что наш набор данных несбалансирован — есть гораздо больше образцов, которые попадают в третью категорию по сравнению с остальными. Наша модель вознаграждается за возвращение категории 3 из-за вероятности того, что случайная точка данных находится в категории 3. Глядя на гистограмму распределения полных наборов данных при категоризации, мы видим:
Для сравнения с точностью нашего прогноза общее количество точек данных в нашем наборе данных составляет ~ 55%.
Важность функций показана ниже:
#get feature importances, store in pandas series pd.Series(data=grid_search.best_estimator_.feature_importances_, index=x_train.columns).sort_values(ascending=False) state_ordered 0.087935 Sunrise_Sunset_Day 0.082273 Traffic_Signal 0.048076 Wind_Speed(mph) 0.039504 weather_smoke 0.037424 Stop 0.037335 Junction 0.036533 Day_Sunday 0.036297 Day_Saturday 0.034697 Hour 0.033719 Station 0.033649 weather_windy 0.030267 weather_dust 0.030218 weather_storm 0.030016 weather_rain 0.029880 Temperature(F) 0.029564 weather_hail 0.029323 weather_fair 0.029111 Crossing 0.027843 weather_fog 0.026316 Day_Friday 0.026270 Visibility(mi) 0.026105 weather_snow 0.026076 Day_Monday 0.026025 Day_Tuesday 0.025664 Day_Wednesday 0.025463 weather_cloudy 0.025074 Day_Thursday 0.024987 Precipitation(in) 0.024355
Мы видим, что функции с наибольшим весом в 2 раза — это состояние и дневное/ночное время. Далее мы видим, что важны близлежащие светофоры, скорость ветра, задымленная погода (экстремальная погода?) и некоторые другие вещи.
Мы видим, что день события, выпадающий на выходные, важен. Это может означать, что мы можем еще больше уменьшить нашу размерность, изменив признак День на категориальный между Выходной/День недели.
Результаты каждой итерации поиска по сетке можно увидеть ниже:
Из этого мы видим, что наша лучшая производительность достигается при максимальных значениях, разрешенных для n_estimators и max_depth. Учитывая это, мы, вероятно, могли бы повысить производительность, повторно запустив gridsearch с более высокими значениями этих параметров.
Мы смогли добиться более желаемых результатов, используя классификационную модель. Это компромисс, но, учитывая характер проблемы — мы используем косвенные, косвенные данные, которые не являются информацией о самом происшествии — это расширяет цели и позволяет нашей модели быть более полезной, чем она была бы в противном случае.
Я создал приложение flask для запроса модели и оценки результатов с различными параметрами — скриншот показан ниже. Если вы хотите сделать то же самое, вы можете сделать это, загрузив этот репозиторий и следуя показанным инструкциям!
Заключение
Чтобы определить, как долго авария может повлиять на трафик, мы создали классификационную модель для оценки продолжительности затронутого трафика, используя только данные, доступные во время инцидента, которые можно интегрировать в существующие приложения для маршрутизации или планирования.
Благодаря этому процессу я получил представление о большем количестве соображений и новых методах. В обстоятельствах, когда высокая точность не достигается с помощью регрессии, переключение на классификацию числовых диапазонов может быть жизнеспособным обходным путем, в зависимости от степени детализации, необходимой для решения.
Я также узнал об порядковом кодировании категориальных переменных, особенно в отношении штатов США. Ранжируя состояния (категории) в соответствии со средней продолжительностью аварии (целевая переменная), я смог значительно сократить пространство функций, сохраняя при этом функцию.
Эти два урока я буду иметь в виду для будущих проектов, и мне было очень приятно увидеть работу!
Улучшение
Дальнейшее улучшение может быть сделано несколькими способами:
- Дальнейшее уменьшение размерности некоторых функций, таких как Hour или state_ordered, за счет встраивания.
- Экспериментирование с диапазонами, используемыми для наших категориальных значений продолжительности аварии
- Балансировка категориального распределения нашего набора данных перед обучением
- Приоритет точности перед отзывом в функции потерь
- Использование feature_selection sklearn для изучения дополнительных функций
- Через GridSearch проверено больше параметров (больше max_depth, выше n_estimators)
- Добавление дополнительных функций, которые могут быть доступны вскоре после сообщения об аварии (например, описание)
Что касается последнего пункта выше, мы можем взглянуть на столбец Описание нашего набора данных и выполнить небольшой анализ, чтобы увидеть, может ли это улучшить нашу модель. Создание нескольких столбцов для общих ключевых слов и построение графика средней продолжительности присутствия каждого из них возвращает график ниже:
Это показывает сильную разницу между средней продолжительностью аварии и наличием этих слов в описании! Однако я не знаю, насколько быстро можно получить эти описания. Просить пользователей приложения описать аварию небезопасно и, вероятно, приведет к большей дисперсии этих функций. Однако, если есть способ быстро получить эти данные, возможно, его стоит включить в эту модель!
Подтверждение
Набор данных о дорожно-транспортных происшествиях в США:
- Мусави, Собхан, Мохаммад Хоссейн Самаватян, Шринивасан Партасарати и Раджив Рамнатх. «Общероссийский массив данных о дорожно-транспортных происшествиях.», 2019.
- Мусави, Собхан, Мохаммад Хоссейн Самаватян, Шринивасан Партасарати, Раду Теодореску и Раджив Рамнатх. «Прогнозирование риска несчастных случаев на основе разнородных разреженных данных: новый набор данных и идеи.» Материалы 27-й Международной конференции ACM SIGSPATIAL по достижениям в области географических информационных систем, ACM, 2019.
Весь используемый код можно найти на GitHub, а также полный список используемых библиотек в readme.
Не стесняйтесь обращаться с вопросами, потенциальными улучшениями или сотрудничеством, и спасибо за чтение! :)