Проект: Оценка риска ДТП по выбранному маршруту движения¶
Описание проекта:
От каршеринговой компании поступил заказ: нужно создать систему, которая могла бы оценить риск ДТП по выбранному маршруту движения. Под риском понимается вероятность ДТП с любым повреждением транспортного средства. Как только водитель забронировал автомобиль, сел за руль и выбрал маршрут, система должна оценить уровень риска. Если уровень риска высок, водитель увидит предупреждение и рекомендации по маршруту.
Идея создания такой системы находится в стадии предварительного обсуждения и проработки. Чёткого алгоритма работы и подобных решений на рынке ещё не существует.
Цель проекта: понять, возможно ли предсказывать ДТП, опираясь на исторические данные одного из регионов.
ТЗ от заказчика:
Создать модель предсказания ДТП (целевое значение —
at_fault(виновник) в таблицеparties);- Для модели выбрать тип виновника — только машина (
car); - Выбрать случаи, когда ДТП привело к любым повреждениям транспортного средства, кроме типа
SCRATCH(царапина); - Для моделирования ограничиться данными за 2012 год — они самые свежие;
- Обязательное условие — учесть фактор возраста автомобиля.
- Для модели выбрать тип виновника — только машина (
На основе модели исследовать основные факторы ДТП и понять, помогут ли результаты моделирования и анализ важности факторов ответить на вопросы:
- Возможно ли создать адекватную системы оценки водительского риска при выдаче авто?
- Какие ещё факторы нужно учесть?
- Нужно ли оборудовать автомобиль какими-либо датчиками или камерой?
Заказчик предлагает поработать с базой данных по происшествиям и сформировать свои идеи создания такой системы.
Описание данных:
collisions— общая информация о ДТП. Имеет уникальныйcase_id. Эта таблица описывает общую информацию о ДТП. Например, где оно произошло и когда.parties— информация об участниках ДТП. Имеет неуникальныйcase_id, который сопоставляется с соответствующим ДТП в таблицеcollisions. Каждая строка здесь описывает одну из сторон, участвующих в ДТП. Если столкнулись две машины, в этой таблице должно быть две строки с совпадениемcase_id. Если нужен уникальный идентификатор, этоcase_idиparty_number.vehicles— информация о пострадавших машинах. Имеет неуникальныеcase_idи неуникальныеparty_number, которые сопоставляются с таблицейcollisionsи таблицейparties. Если нужен уникальный идентификатор, этоcase_idиparty_number.
Описание признаков:
- Таблица
collisions— общая информация о ДТП.
| Описание | Обозначение в таблице | Подробнее |
|---|---|---|
| Идентификационный номер в базе данных | CASE_ID | Уникальный номер для зарегистрированного происшествия в таблице происшествий. |
| Дата происшествия | COLLISION_DATE | Формат год/месяц/день |
| Время происшествия | COLLISION_TIME | Формат: 24-часовой |
| Является ли место происшествия перекрёстком |
INTERSECTION | Y — Intersection (перекрёсток) N — Not Intersection (не перекрёсток) - — Not stated (Не указано) |
| Погода | WEATHER_1 | A — Clear (Ясно) B — Cloudy (Облачно) C — Raining (Дождь) D — Snowing (Снегопад) E — Fog (Туман) F — Other (Другое) G — Wind (Ветер) - — Not Stated (Не указано) |
| Серьёзность происшествия | COLLISION_DAMAGE | 1 — FATAL ТС (Не подлежит восстановлению) 2 — SEVERE DAMAGE (Серьёзный ремонт, большая часть под замену/Серьёзное повреждение капитального строения) 3 — MIDDLE DAMAGE (Средний ремонт, машина в целом на ходу/Строение в целом устояло) 4 — SMALL DAMAGE (Отдельный элемент кузова под замену/покраску) 0 – SCRATCH (Царапина) |
| Основной фактор аварии | PRIMARY_COLLISION_FACTOR | A — Code Violation (Нарушение правил ПДД) B — Other Improper Driving (Другое неправильное вождение) C — Other Than Driver (Кроме водителя) D — Unknown (Неизвестно) E — Fell Asleep (Заснул) - — Not Stated (Не указано) |
| Состояние дороги | ROAD_SURFACE | A — Dry (Сухая) B — Wet (Мокрая) C — Snowy or Icy (Заснеженная или обледенелая) D — Slippery (Muddy, Oily, etc.) (Скользкая, грязная, маслянистая и т. д.) - — Not Stated (Не указано) |
| Освещение | LIGHTING | A — Daylight (Дневной свет) B — Dusk-Dawn (Сумерки-Рассвет) C — Dark-Street Lights (Темно-Уличные фонари) D — Dark-No Street Lights (Темно-Нет уличных фонарей) E — Dark-Street Lights Not Functioning (Темно-Уличные фонари не работают) - — Not Stated (Не указано) |
| Номер географических районов, где произошло ДТП | COUNTY_CITY_LOCATION | Число |
| Названия географических районов, где произошло ДТП | COUNTY_LOCATION | Список разных названий, категориальный тип данных |
| Направление движения | DIRECTION | N — North (Север) E — East (Восток) S — South (Юг) W — West (Запад) - or blank — Not State (Не указано) на перекрёстке |
| Расстояние от главной дороги (метры) | DISTANCE | Число |
| Тип дороги | LOCATION_TYPE | H — Highway (Шоссе) I — Intersection (Перекрёсток) R — Ramp (or Collector) (Рампа) - or blank — Not State Highway (Не указано) |
| Количество участников | PARTY_COUNT | Число |
| Категория нарушения | PCF_VIOLATION_CATEGORY | 01 — Driving or Bicycling Under the Influence of Alcohol or Drug (Вождение или езда на велосипеде в состоянии алкогольного или наркотического опьянения) 02 — Impeding Traffic (Препятствие движению транспорта) 03 — Unsafe Speed (Превышение скорости) 04 — Following Too Closely (Опасное сближение) 05 — Wrong Side of Road (Неправильная сторона дороги) 06 — Improper Passing (Неправильное движение) 07 — Unsafe Lane Change (Небезопасная смена полосы движения) 08 — Improper Turning (Неправильный поворот) 09 — Automobile Right of Way (Автомобильное право проезда) 10 — Pedestrian Right of Way (Пешеходное право проезда) 11 — Pedestrian Violation (Нарушение пешеходами) 12 — Traffic Signals and Signs (Дорожные сигналы и знаки) 13 — Hazardous Parking (Неправильная парковка) 14 — Lights (Освещение) 15 — Brakes (Тормоза) 16 — Other Equipment (Другое оборудование) 17 — Other Hazardous Violation (Другие нарушения) 18 — Other Than Driver (or Pedestrian) (Кроме водителя или пешехода) 19 — Speeding (Скорость) 20 — Pedestrian dui (Нарушение пешехода) 21 — Unsafe Starting or Backing (Опасный старт) 22 — Other Improper Driving (Другое неправильное вождение) 23 — Pedestrian or “Other” Under the Influence of Alcohol or Drug (Пешеход или «Другой» в состоянии алкогольного или наркотического опьянения) 24 — Fell Asleep (Заснул) 00 — Unknown (Неизвестно) - — Not Stated (Не указано) |
| Тип аварии | TYPE_OF_COLLISION | A — Head-On (Лоб в лоб) B — Sideswipe (Сторона) C — Rear End (Столкновение задней частью) D — Broadside (Боковой удар) E — Hit Object (Удар объекта) F — Overturned (Опрокинутый) G — Vehicle (транспортное средство/ Пешеход) H — Other (Другое) - — Not Stated (Не указано) |
| Дополнительные участники ДТП | MOTOR_VEHICLE_INVOLVED_WITH | Other motor vehicle (Другой автомобиль) Fixed object (Неподвижный объект) Parked motor vehicle (Припаркованный автомобиль) Pedestrian (Пешеход) Bicycle (Велосипедист) Non-collision (Не столкновение) Other object (Другой объект) Motor vehicle on other roadway (Автомобиль на другой проезжей) Animal (Животное) Train (Поезд) |
| Дорожное состояние | ROAD_CONDITION_1 | A — Holes, Deep Ruts (Ямы, глубокая колея) B — Loose Material on Roadway (Сыпучий материал на проезжей части) C — Obstruction on Roadway (Препятствие на проезжей части) D — Construction or Repair Zone (Зона строительства или ремонта) E — Reduced Roadway Width (Уменьшенная ширина проезжей части) F — Flooded (Затоплено) G — Other (Другое) H — No Unusual Condition (Нет ничего необычного) - — Not Stated (Не указано) |
- Таблица
parties— информация об участниках ДТП.
| Описание | Обозначение в таблице | Подробнее |
|---|---|---|
| Идентификационный номер в базе данных | CASE_ID | Уникальный номер для зарегистрированного происшествия в таблице происшествий |
| Номер участника происшествия | PARTY_NUMBER | От 1 до N — по числу участников происшествия |
| Тип участника происшествия | PARTY_TYPE | 1 — Car (Авто) 2 — Road bumper (Отбойник) 3 — Building (Строения) 4 — Road signs (Дорожные знаки) 5 — Other (Другое) 6 — Operator (Оператор) - — Not Stated (Не указано) |
| Виновность участника | AT_FAULT | 0/1 |
| Сумма страховки (тыс. $) | INSURANCE_PREMIUM | Число |
| Состояние участника: физическое или с учётом принятых лекарств | PARTY_DRUG_PHYSICAL | E — Under Drug Influence (Под воздействием лекарств) F — Impairment — Physical (Ухудшение состояния) G — Impairment Unknown (Не известно) H — Not Applicable (Не оценивался) I — Sleepy/Fatigued (Сонный/Усталый) - — Not Stated (Не указано) |
| Трезвость участника | PARTY_SOBRIETY | A — Had Not Been Drinking (Не пил) B — Had Been Drinking, Under Influence (Был пьян, под влиянием) C — Had Been Drinking, Not Under Influence (Был пьян, не под влиянием) D — Had Been Drinking, Impairment Unknown (Был пьян, ухудшение неизвестно) G — Impairment Unknown (Неизвестно ухудшение) H — Not Applicable (Не оценивался) - — Not Stated (Не указано) |
| Наличие телефона в автомобиле (возможности разговаривать по громкой связи) | CELLPHONE_IN_USE | 0/1 |
- Таблица
vehicles— информация о пострадавших машинах.
| Описание | Обозначение в таблице | Подробнее |
|---|---|---|
| Индекс текущей таблицы | ID | Номер в таблице |
| Идентификационный номер в базе данных | CASE_ID | Уникальный номер для зарегистрированного происшествия в таблице происшествий |
| Номер участника происшествия | PARTY_NUMBER | От 1 до N — по числу участников происшествия |
| Тип кузова | VEHICLE_TYPE | MINIVAN COUPE SEDAN HATCHBACK OTHER |
| Тип КПП | VEHICLE_TRANSMISSION | auto (Автоматическая) manual (Ручная) - — Not Stated (Не указано) |
| Возраст автомобиля (в годах) | VEHICLE_AGE | Число |
Ход исследования:
Подготовка данных: загрузка и изучение общей информации из представленных датасетов.Статистический анализ факторов ДТП: рассматриваются вопросы, которые помогут лучше понимать данные и их взаимосвязи.Предобработка данных: выгрузка датасета из базы данных, необходимого для обучения модели и дальнейшая его предобработка — заполнение пропусков, обработка явных и неявных дубликатов, корректировка типов данных.Исследовательский анализ данных: изучение признаков, их распределение, поиск выбросов/аномалий в данных.Корреляционный анализ: изучение взимосвязей между входными признаками и целевыми, а также и между ними.Обучение моделей и выбор лучшей: подготовка данных для обучения моделей с помощью построенных пайплайнов, использованиеBayesSearchCVдля поиска лучших гиперпараметров для моделей, сравнение лучших метрик моделей на кросс-валидации и выбор лучшей, лучей моделью делаем прогноз на тестовых данных и проводит анализ результатов.Анализ важности факторов ДТП с помощью SHAP: анализ степени важности признаков их влияния на принятие решений моделью с помощью методаSHAP, и проводим дополнительное исследование одного из признаков.Общий вывод: резюмирование полученных результатов, формулировка ключевых выводов и рекомендаций.
Подготовка рабочей среды и вспомогательные функции¶
Импорт библиотек и базовые настройки блокнота¶
# Стандартные библиотеки
import math
import random
import warnings
# Сторонние библиотеки
from catboost import CatBoostClassifier
from lightgbm import LGBMClassifier
from IPython.display import display
import matplotlib.pyplot as plt
import missingno as msno
import numpy as np
import pandas as pd
import phik
import scipy.stats as stats
from scipy.stats import anderson
import seaborn as sns
import shap
from sklearn.compose import ColumnTransformer
from sklearn.dummy import DummyClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_selection import mutual_info_classif
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (classification_report,
confusion_matrix,
roc_auc_score)
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import (OneHotEncoder,
StandardScaler,
TargetEncoder,
MinMaxScaler,
FunctionTransformer)
from skopt import BayesSearchCV
from skopt.space import Real, Integer, Categorical
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sqlalchemy import create_engine
from sqlalchemy.pool import NullPool
# Базовые настройки блокнота
sns.set()
sns.set_context('paper')
pd.set_option('display.max_column', None)
pd.set_option('display.max_colwidth', None)
# Константа
RANDOM_STATE = 6011994
# Убираем предупреждения для LightGBM
warnings.filterwarnings("ignore", message="X does not have valid feature names.*")
Вспомогательные функции¶
# Функция для получения общей информации о датафрейме
def gen_info(df):
'''
Данная функция выводит общую информацию
о датафрейме, статистическое описание признаков
и 5 рандомных строк.
На ввод функция принимает переменную датафрейма.
'''
# Статистики по количественным признакам
desc = df.describe().T
# Вывод результатов
print(df.info())
display(desc)
display(df.sample(5, random_state=RANDOM_STATE))
# Функция для поиска неявных дубликатов
def hidden_dup_search(df):
'''
Данная функция приводит значения категориальных
столбцов к единому стилю и выводит их уникальные
значения.
На ввод функция принимает переменную датафрейма.
'''
# Список категориальных признаков
df_cat_col = df.select_dtypes(include = 'object').columns.tolist()
# Приводим все значения к единому стилю
# и проверяем уникальные значения для
# точечной проработки при необходимости
for feature in df_cat_col:
df[feature] = df[feature].str.lower().str.replace(' ', '_').str.replace('-', '_')
print(f'Уникальные значения признака: {str(feature)}')
print(df[feature].unique())
print()
# Функция для комплексного анализа количественного признака
def analyzis_quantity(df, x_label, y_label='Частота', target=None,
hue=None, size=None, sizes=None, system=False,
discrete=False, log_scale=False, title_hist=None,
title_scatter=None):
'''
Данная функция выводит "коробочный" график и гистограмму
по указанному столбцу датафрейма и его статистические метрики.
Аргументы функции:
df - данные (pd.Series)
x_label - подпись для оси Х
y_label - подпись для оси Y (по умолчанию "Частота")
target - целевая переменная для scatter-графика (если system=True)
hue - переменная для цветового кодирования (если system=True)
size - переменная кодирования по размеру (если system=True)
sizes - переменная для уточнения диапозона кодировки размеров (если system=True)
system - флаг для построения scatter-графика
discrete - булевое значение, дискретные значение или нет.
log_scale - логарифмическая шкала для гистограммы
title_hist - заголовок для гистограммы и boxplot
title_scatter - заголовок для scatter-графика
'''
# Названия для графиков по умолчанию
if title_hist is None:
title_hist = f"Распределение {df.name if hasattr(df, 'name') else 'входной признак'}"
if title_scatter is None:
target_name = target.name if hasattr(target, 'name') else 'таргета'
feature_name = df.name if hasattr(df, 'name') else 'входной признак'
title_scatter = f"Зависимость {target_name} от {feature_name}"
# Создание составного графика: boxplot + histogram
fig, (ax_box, ax_hist) = plt.subplots(2, sharex=True,
figsize=(8, 5.6),
gridspec_kw={'height_ratios': (.15, .85)})
# Boxplot
sns.boxplot(x=df, orient='h', ax=ax_box)
ax_box.set(xlabel='')
# Histogram
n_bins = round(1 + math.log2(len(df))) if len(df) > 1 else 10
sns.histplot(x=df, bins=n_bins, discrete=discrete, log_scale=log_scale, ax=ax_hist)
# Общие настройки для основной фигуры
ax_hist.set_xlabel(x_label, fontsize=10)
ax_hist.set_ylabel(y_label, fontsize=10)
ax_hist.tick_params(axis='both', which='major', labelsize=10)
fig.suptitle(title_hist, fontsize=12, fontweight='bold', y=0.95)
# Настройка тиков для дискретных значений
if discrete == 'unique':
unique_vals = np.sort(df.dropna().unique())
ax_hist.set_xticks(unique_vals)
elif discrete == 'non_unique':
ax_hist.set_xticks(np.arange(df.min(), df.max() + 1, 1))
# Отображение первой фигуры
fig.tight_layout()
# Scatter plot (если запрошено)
if system:
if hue is not None:
fig_2, ax_2 = plt.subplots(figsize=(10, 5.34))
else:
fig_2, ax_2 = plt.subplots(figsize=(8, 5.34))
sns.scatterplot(x=df, y=target, alpha=0.5, hue=hue, size=size, sizes=sizes, ax=ax_2)
fig_2.suptitle(title_scatter, fontsize=12, fontweight='bold', y=0.95)
ax_2.set_xlabel(x_label, fontsize=10)
ax_2.set_ylabel(target.name if hasattr(target, 'name') else 'Целевая переменная', fontsize=10)
ax_2.tick_params(axis='both', which='major', labelsize=10)
if hue is not None:
ax_2.legend(bbox_to_anchor=(1.025, 1), loc='upper left', fontsize=10)
fig_2.tight_layout()
if log_scale:
plt.xscale('log')
plt.yscale('log')
# Отображение графика
plt.show()
# Вывод статистических метрик
display(df.describe().to_frame().T)
# Проверяем нормальность распределения
result = anderson(df.dropna())
if result.statistic < result.critical_values[2]:
distr = 'Нормальное'
else:
distr = 'Не является нормальным'
test_anderson = {'':['Статистика:',
"Критические значения:",
'Распределение'],
'Тест на нормальность распределения (порог=0.05):': [result.statistic,
result.critical_values,
distr]
}
display(pd.DataFrame(test_anderson).set_index(''))
# Функция для анализа категорийных значений
def analyzis_category(df, title=None, kind='bar', rotation=0, top=None):
'''
Данная функция выводит столбчатый график
по указанному столбцу датафрейма и его значения
в табличном виде.
Аргументы функции:
df - данные
name - название графика
kind - ориентация графика вертикальная или
горизонтальная, принимает значения "bar" и "barh".
'''
# Название для графика по умолчанию
if title is None:
title = f"Соотношение категорий {df.name if hasattr(df, 'name') else 'входного признака'}"
# Подсчитываем количество каждого значения
category_count = df.value_counts(ascending=True)
if top == 'tail':
category_count = category_count.head(10)
elif top == 'head':
category_count = category_count.tail(10)
# Создание столбчатого графика
plot_bar = category_count.plot(kind=kind, figsize=(8, 5.6), grid=True)
# Настройка заголовка и подписей
if kind == 'bar':
plt.title(title, fontweight='bold', fontsize=12)
plt.xlabel('')
plt.ylabel('Частота', fontsize=10)
plt.tick_params(axis='both', which='major', labelsize=10)
plt.xticks(rotation=rotation)
else:
plt.title(title, fontweight='bold', fontsize=12)
plt.xlabel('Частота', fontsize=10)
plt.ylabel('')
plt.tick_params(axis='both', which='major', labelsize=10)
plt.xticks(rotation=rotation)
# Отображаем график
plt.tight_layout()
plt.show()
# Вывод значений в табличном виде
display(pd.DataFrame(category_count).reset_index())
# Построение таблицы с расчитанным VIF
def vif_factor(data, columns):
'''
Данная функция расчитываем VIF-фактор
для количественных признаков датафрейма.
На ввод функция принимает:
data - датафрейм
columns - список количественных признаков.
'''
vif_data = pd.DataFrame()
vif_data['input attribute'] = columns
vif_data['vif'] = ([variance_inflation_factor(data[columns].dropna().values, i)
for i in range(data[columns].shape[1])])
return vif_data
# Функция для получения общей информации о таблице
# с помощью SQL-запроса
def sql_main_info(data, query, con):
'''
Данная функция возвращает общую информацию
о таблице с помощью SQL-запроса, на ввод
функция должна получить:
data - название рассматриваемой таблицы
в одинарных ковычка;
query - SQL-запрос, который расчитывает
количество строк для каждого столбца
в виде:
=============== ПРИМЕР ================
SELECT
COUNT(column_1) AS column_1,
... ... ... ... ...
COUNT(column_n) AS column_n,
COUNT(*) AS total_rows
FROM data
=======================================
con - переменная с соединением к базе
'''
# Запрос основной информации
main_info = f'''
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = '{data}'
'''
df_info = pd.read_sql_query(main_info, con=con)
# Количество строк и пропусков в таблице
df_null = pd.read_sql_query(query, con=con)
df_null = df_null.melt(id_vars=['total_rows'],
var_name='column_name',
value_name='non_null')
# Объединение таблиц
df_info = df_info.merge(df_null, how='left', on='column_name')
df_info['is_null'] = df_info['total_rows'] - df_info['non_null']
# Первые 5 строк таблицы
query_rows = f'''
SELECT *
FROM {data}
LIMIT 5
'''
first_rows = pd.read_sql_query(query_rows, con=con)
# Вывод результата
print('Общая информация о таблице:')
display(df_info)
print('\nПервые 5 строк таблицы:')
display(first_rows)
# Функция для преобразования временных признаков
def cos_sin_feature(X):
X = X.copy()
X['hour_sin'] = np.sin(2 * np.pi * X['value_10__hour_drive'] / 24)
X['hour_cos'] = np.cos(2 * np.pi * X['value_10__hour_drive'] / 24)
X.drop('value_10__hour_drive', axis=1, inplace=True)
X['dow_sin'] = np.sin(2 * np.pi * X['mode__day_of_week'] / 7)
X['dow_cos'] = np.cos(2 * np.pi * X['mode__day_of_week'] / 7)
X.drop('mode__day_of_week', axis=1, inplace=True)
X['dom_sin'] = np.sin(2 * np.pi * X['mode__day_of_month'] / 30)
X['dom_cos'] = np.cos(2 * np.pi * X['mode__day_of_month'] / 30)
X.drop('mode__day_of_month', axis=1, inplace=True)
return X
# Функция для получения названий колонок
def get_feature_names(preprocessor, num_col, ohe_col, loc_col, time_col):
feature_names = []
# StandardScaler
feature_names.extend(num_col)
# OneHotEncoder
ohe = preprocessor.named_transformers_['ohe']
ohe_feature_names = ohe.get_feature_names_out(ohe_col)
feature_names.extend(ohe_feature_names)
# TargetEncoder
feature_names.extend(loc_col)
# Time features (cos/sin)
for col in time_col:
feature_names.extend([f"{col}_sin", f"{col}_cos"])
# Остальные колонки
remainder_cols = [col for col in X.columns
if col not in num_col + ohe_col + loc_col + time_col]
feature_names.extend(remainder_cols)
return feature_names
# Функция для замены пропущеных значений
def replace_none(X):
X = X.copy()
X[value_unknown_col] = X[value_unknown_col].replace({None: np.nan})
X[value_none_col] = X[value_none_col].replace({None: np.nan})
return X
Подключение к базе¶
# Конфигурация для подключения
db_config = {'user': 'praktikum_student',
'pwd': 'Sdf4$2;d-d30pp',
'host': 'rc1b-wcoijxj3yxfsf3fs.mdb.yandexcloud.net',
'port': 6432,
'db': 'data-science-vehicle-db'
}
# Строка для соединение с базой
connection_string = 'postgresql://{}:{}@{}:{}/{}'.format(
db_config['user'],
db_config['pwd'],
db_config['host'],
db_config['port'],
db_config['db']
)
# Создаем соединение
engine = create_engine(connection_string, poolclass=NullPool)
Общая информация о таблицах — первичное исследование¶
Проверка наличия таблиц в базе данных¶
query = '''
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
'''
table = pd.read_sql_query(query, con=engine)
table
| table_name | |
|---|---|
| 0 | case_ids |
| 1 | collisions |
| 2 | parties |
| 3 | vehicles |
Все объявленные таблицы в базе данных присутствуют.
ER - диаграмма¶
Таблица Case_ids¶
# Общая информация о таблице
query = '''
SELECT
COUNT(case_id) AS case_id,
COUNT(db_year) AS db_year,
COUNT(*) AS total_rows
FROM case_ids
'''
sql_main_info(data='case_ids', query=query, con=engine)
Общая информация о таблице:
| column_name | data_type | is_nullable | column_default | total_rows | non_null | is_null | |
|---|---|---|---|---|---|---|---|
| 0 | case_id | text | YES | None | 1400000 | 1400000 | 0 |
| 1 | db_year | text | YES | None | 1400000 | 1400000 | 0 |
Первые 5 строк таблицы:
| case_id | db_year | |
|---|---|---|
| 0 | 0081715 | 2021 |
| 1 | 0726202 | 2021 |
| 2 | 3858022 | 2021 |
| 3 | 3899441 | 2021 |
| 4 | 3899442 | 2021 |
По информации от заказчика самая актуальная информация за 2012 год, что не соотносится с данными из первых строк таблицы, выведем по каким годам есть информация.
query = '''
SELECT DISTINCT db_year
FROM case_ids
'''
table = pd.read_sql_query(query, con=engine)
table
| db_year | |
|---|---|
| 0 | 2021 |
Все объявленные колонки в таблице присутствуют, пропущенные значения в колонках отcутствуют. Но в данной таблице нет информации о необходимом нам годе. К сожалению мы не сможем воспользоваться данной таблицей.
Таблица Vehicles¶
Описание данных:¶
- Таблица
vehicles— информация о пострадавших машинах.
| Описание | Обозначение в таблице | Подробнее |
|---|---|---|
| Индекс текущей таблицы | ID | Номер в таблице |
| Идентификационный номер в базе данных | CASE_ID | Уникальный номер для зарегистрированного происшествия в таблице происшествий |
| Номер участника происшествия | PARTY_NUMBER | От 1 до N — по числу участников происшествия |
| Тип кузова | VEHICLE_TYPE | MINIVAN COUPE SEDAN HATCHBACK OTHER |
| Тип КПП | VEHICLE_TRANSMISSION | auto (Автоматическая) manual (Ручная) - — Not Stated (Не указано) |
| Возраст автомобиля (в годах) | VEHICLE_AGE | Число |
# Общая информация о таблице
query = '''
SELECT
COUNT(id) AS id,
COUNT(case_id) AS case_id,
COUNT(party_number) AS party_number,
COUNT(vehicle_type) AS vehicle_type,
COUNT(vehicle_transmission) AS vehicle_transmission,
COUNT(vehicle_age) AS vehicle_age,
COUNT(*) AS total_rows
FROM vehicles
'''
sql_main_info(data='vehicles', query=query, con=engine)
Общая информация о таблице:
| column_name | data_type | is_nullable | column_default | total_rows | non_null | is_null | |
|---|---|---|---|---|---|---|---|
| 0 | id | integer | YES | None | 1021234 | 1021234 | 0 |
| 1 | party_number | integer | YES | None | 1021234 | 1021234 | 0 |
| 2 | vehicle_age | integer | YES | None | 1021234 | 996652 | 24582 |
| 3 | case_id | text | YES | None | 1021234 | 1021234 | 0 |
| 4 | vehicle_type | text | YES | None | 1021234 | 1021234 | 0 |
| 5 | vehicle_transmission | text | YES | None | 1021234 | 997575 | 23659 |
Первые 5 строк таблицы:
| id | case_id | party_number | vehicle_type | vehicle_transmission | vehicle_age | |
|---|---|---|---|---|---|---|
| 0 | 1175713 | 5305032 | 2 | sedan | manual | 3 |
| 1 | 1 | 3858022 | 1 | sedan | auto | 3 |
| 2 | 1175712 | 5305030 | 1 | sedan | auto | 3 |
| 3 | 1175717 | 5305033 | 3 | sedan | auto | 5 |
| 4 | 1175722 | 5305034 | 2 | sedan | auto | 5 |
Все объявленные колонки в таблице присутствуют, имеет по ~24 тысячи пропусков в признаках vehicle_age и vehicle_transmission.
Таблица Collisions¶
Описание данных:¶
- Таблица
collisions— общая информация о ДТП.
| Описание | Обозначение в таблице | Подробнее |
|---|---|---|
| Идентификационный номер в базе данных | CASE_ID | Уникальный номер для зарегистрированного происшествия в таблице происшествий. |
| Дата происшествия | COLLISION_DATE | Формат год/месяц/день |
| Время происшествия | COLLISION_TIME | Формат: 24-часовой |
| Является ли место происшествия перекрёстком |
INTERSECTION | Y — Intersection (перекрёсток) N — Not Intersection (не перекрёсток) - — Not stated (Не указано) |
| Погода | WEATHER_1 | A — Clear (Ясно) B — Cloudy (Облачно) C — Raining (Дождь) D — Snowing (Снегопад) E — Fog (Туман) F — Other (Другое) G — Wind (Ветер) - — Not Stated (Не указано) |
| Серьёзность происшествия | COLLISION_DAMAGE | 1 — FATAL ТС (Не подлежит восстановлению) 2 — SEVERE DAMAGE (Серьёзный ремонт, большая часть под замену/Серьёзное повреждение капитального строения) 3 — MIDDLE DAMAGE (Средний ремонт, машина в целом на ходу/Строение в целом устояло) 4 — SMALL DAMAGE (Отдельный элемент кузова под замену/покраску) 0 – SCRATCH (Царапина) |
| Основной фактор аварии | PRIMARY_COLLISION_FACTOR | A — Code Violation (Нарушение правил ПДД) B — Other Improper Driving (Другое неправильное вождение) C — Other Than Driver (Кроме водителя) D — Unknown (Неизвестно) E — Fell Asleep (Заснул) - — Not Stated (Не указано) |
| Состояние дороги | ROAD_SURFACE | A — Dry (Сухая) B — Wet (Мокрая) C — Snowy or Icy (Заснеженная или обледенелая) D — Slippery (Muddy, Oily, etc.) (Скользкая, грязная, маслянистая и т. д.) - — Not Stated (Не указано) |
| Освещение | LIGHTING | A — Daylight (Дневной свет) B — Dusk-Dawn (Сумерки-Рассвет) C — Dark-Street Lights (Темно-Уличные фонари) D — Dark-No Street Lights (Темно-Нет уличных фонарей) E — Dark-Street Lights Not Functioning (Темно-Уличные фонари не работают) - — Not Stated (Не указано) |
| Номер географических районов, где произошло ДТП | COUNTY_CITY_LOCATION | Число |
| Названия географических районов, где произошло ДТП | COUNTY_LOCATION | Список разных названий, категориальный тип данных |
| Направление движения | DIRECTION | N — North (Север) E — East (Восток) S — South (Юг) W — West (Запад) - or blank — Not State (Не указано) на перекрёстке |
| Расстояние от главной дороги (метры) | DISTANCE | Число |
| Тип дороги | LOCATION_TYPE | H — Highway (Шоссе) I — Intersection (Перекрёсток) R — Ramp (or Collector) (Рампа) - or blank — Not State Highway (Не указано) |
| Количество участников | PARTY_COUNT | Число |
| Категория нарушения | PCF_VIOLATION_CATEGORY | 01 — Driving or Bicycling Under the Influence of Alcohol or Drug (Вождение или езда на велосипеде в состоянии алкогольного или наркотического опьянения) 02 — Impeding Traffic (Препятствие движению транспорта) 03 — Unsafe Speed (Превышение скорости) 04 — Following Too Closely (Опасное сближение) 05 — Wrong Side of Road (Неправильная сторона дороги) 06 — Improper Passing (Неправильное движение) 07 — Unsafe Lane Change (Небезопасная смена полосы движения) 08 — Improper Turning (Неправильный поворот) 09 — Automobile Right of Way (Автомобильное право проезда) 10 — Pedestrian Right of Way (Пешеходное право проезда) 11 — Pedestrian Violation (Нарушение пешеходами) 12 — Traffic Signals and Signs (Дорожные сигналы и знаки) 13 — Hazardous Parking (Неправильная парковка) 14 — Lights (Освещение) 15 — Brakes (Тормоза) 16 — Other Equipment (Другое оборудование) 17 — Other Hazardous Violation (Другие нарушения) 18 — Other Than Driver (or Pedestrian) (Кроме водителя или пешехода) 19 — Speeding (Скорость) 20 — Pedestrian dui (Нарушение пешехода) 21 — Unsafe Starting or Backing (Опасный старт) 22 — Other Improper Driving (Другое неправильное вождение) 23 — Pedestrian or “Other” Under the Influence of Alcohol or Drug (Пешеход или «Другой» в состоянии алкогольного или наркотического опьянения) 24 — Fell Asleep (Заснул) 00 — Unknown (Неизвестно) - — Not Stated (Не указано) |
| Тип аварии | TYPE_OF_COLLISION | A — Head-On (Лоб в лоб) B — Sideswipe (Сторона) C — Rear End (Столкновение задней частью) D — Broadside (Боковой удар) E — Hit Object (Удар объекта) F — Overturned (Опрокинутый) G — Vehicle (транспортное средство/ Пешеход) H — Other (Другое) - — Not Stated (Не указано) |
| Дополнительные участники ДТП | MOTOR_VEHICLE_INVOLVED_WITH | Other motor vehicle (Другой автомобиль) Fixed object (Неподвижный объект) Parked motor vehicle (Припаркованный автомобиль) Pedestrian (Пешеход) Bicycle (Велосипедист) Non-collision (Не столкновение) Other object (Другой объект) Motor vehicle on other roadway (Автомобиль на другой проезжей) Animal (Животное) Train (Поезд) |
| Дорожное состояние | ROAD_CONDITION_1 | A — Holes, Deep Ruts (Ямы, глубокая колея) B — Loose Material on Roadway (Сыпучий материал на проезжей части) C — Obstruction on Roadway (Препятствие на проезжей части) D — Construction or Repair Zone (Зона строительства или ремонта) E — Reduced Roadway Width (Уменьшенная ширина проезжей части) F — Flooded (Затоплено) G — Other (Другое) H — No Unusual Condition (Нет ничего необычного) - — Not Stated (Не указано) |
# Общая инофрмация о таблице
query = '''
SELECT
COUNT(case_id) AS case_id,
COUNT(collision_date) AS collision_date,
COUNT(collision_time) AS collision_time,
COUNT(intersection) AS intersection,
COUNT(weather_1) AS weather_1,
COUNT(collision_damage) AS collision_damage,
COUNT(primary_collision_factor) AS primary_collision_factor,
COUNT(road_surface) AS road_surface,
COUNT(lighting) AS lighting,
COUNT(control_device) AS control_device,
COUNT(county_city_location) AS county_city_location,
COUNT(county_location) AS county_location,
COUNT(direction) AS direction,
COUNT(distance) AS distance,
COUNT(location_type) AS location_type,
COUNT(party_count) AS party_count,
COUNT(pcf_violation_category) AS pcf_violation_category,
COUNT(type_of_collision) AS type_of_collision,
COUNT(motor_vehicle_involved_with) AS motor_vehicle_involved_with,
COUNT(road_condition_1) AS road_condition_1,
COUNT(*) AS total_rows
FROM collisions
'''
sql_main_info(data='collisions', query=query, con=engine)
Общая информация о таблице:
| column_name | data_type | is_nullable | column_default | total_rows | non_null | is_null | |
|---|---|---|---|---|---|---|---|
| 0 | party_count | integer | YES | None | 1400000 | 1400000 | 0 |
| 1 | intersection | integer | YES | None | 1400000 | 1387781 | 12219 |
| 2 | distance | real | YES | None | 1400000 | 1400000 | 0 |
| 3 | collision_date | date | YES | None | 1400000 | 1400000 | 0 |
| 4 | collision_time | time without time zone | YES | None | 1400000 | 1387692 | 12308 |
| 5 | location_type | text | YES | None | 1400000 | 518779 | 881221 |
| 6 | collision_damage | text | YES | None | 1400000 | 1400000 | 0 |
| 7 | case_id | text | YES | None | 1400000 | 1400000 | 0 |
| 8 | pcf_violation_category | text | YES | None | 1400000 | 1372046 | 27954 |
| 9 | type_of_collision | text | YES | None | 1400000 | 1388176 | 11824 |
| 10 | motor_vehicle_involved_with | text | YES | None | 1400000 | 1393181 | 6819 |
| 11 | road_surface | text | YES | None | 1400000 | 1386907 | 13093 |
| 12 | road_condition_1 | text | YES | None | 1400000 | 1388012 | 11988 |
| 13 | lighting | text | YES | None | 1400000 | 1391407 | 8593 |
| 14 | control_device | text | YES | None | 1400000 | 1391593 | 8407 |
| 15 | primary_collision_factor | text | YES | None | 1400000 | 1391834 | 8166 |
| 16 | county_city_location | text | YES | None | 1400000 | 1400000 | 0 |
| 17 | county_location | text | YES | None | 1400000 | 1400000 | 0 |
| 18 | direction | text | YES | None | 1400000 | 1059358 | 340642 |
| 19 | weather_1 | text | YES | None | 1400000 | 1392741 | 7259 |
Первые 5 строк таблицы:
| case_id | county_city_location | county_location | distance | direction | intersection | weather_1 | location_type | collision_damage | party_count | primary_collision_factor | pcf_violation_category | type_of_collision | motor_vehicle_involved_with | road_surface | road_condition_1 | lighting | control_device | collision_date | collision_time | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 4083072 | 1942 | los angeles | 528.0 | north | 0 | cloudy | highway | small damage | 2 | vehicle code violation | unsafe lane change | sideswipe | other motor vehicle | wet | normal | daylight | none | 2009-01-22 | 07:25:00 |
| 1 | 4083075 | 4313 | santa clara | 0.0 | None | 1 | clear | None | small damage | 1 | vehicle code violation | improper passing | hit object | fixed object | dry | normal | dark with street lights | functioning | 2009-01-03 | 02:26:00 |
| 2 | 4083073 | 0109 | alameda | 0.0 | None | 1 | clear | None | scratch | 2 | vehicle code violation | improper turning | broadside | other motor vehicle | dry | normal | dark with street lights | functioning | 2009-01-11 | 03:32:00 |
| 3 | 4083077 | 0109 | alameda | 0.0 | None | 1 | clear | None | scratch | 2 | vehicle code violation | automobile right of way | broadside | other motor vehicle | dry | normal | daylight | functioning | 2009-01-11 | 10:35:00 |
| 4 | 4083087 | 4313 | santa clara | 0.0 | None | 1 | clear | None | scratch | 2 | vehicle code violation | speeding | rear end | other motor vehicle | dry | None | dark with street lights | functioning | 2009-01-02 | 22:43:00 |
Все объявленные колонки в таблице присутствуют, у нас отсутствует описание признака control_device, в 75% признаков есть около 10 тысяч пропусков, наибольшее количество в признаке location_type — 881 тысяч пропусков и direction — 340 тысяч пропусков. Что может говорить о том, что у нас не получится достаточно точно сформулировать правило, что конкретный маршрут повышает вероятность ДТП, только ориентируясь на районы города.
Таблица Parties¶
Описание данных:¶
- Таблица
parties— информация об участниках ДТП.
| Описание | Обозначение в таблице | Подробнее |
|---|---|---|
| Идентификационный номер в базе данных | CASE_ID | Уникальный номер для зарегистрированного происшествия в таблице происшествий |
| Номер участника происшествия | PARTY_NUMBER | От 1 до N — по числу участников происшествия |
| Тип участника происшествия | PARTY_TYPE | 1 — Car (Авто) 2 — Road bumper (Отбойник) 3 — Building (Строения) 4 — Road signs (Дорожные знаки) 5 — Other (Другое) 6 — Operator (Оператор) - — Not Stated (Не указано) |
| Виновность участника | AT_FAULT | 0/1 |
| Сумма страховки (тыс. $) | INSURANCE_PREMIUM | Число |
| Состояние участника: физическое или с учётом принятых лекарств | PARTY_DRUG_PHYSICAL | E — Under Drug Influence (Под воздействием лекарств) F — Impairment — Physical (Ухудшение состояния) G — Impairment Unknown (Не известно) H — Not Applicable (Не оценивался) I — Sleepy/Fatigued (Сонный/Усталый) - — Not Stated (Не указано) |
| Трезвость участника | PARTY_SOBRIETY | A — Had Not Been Drinking (Не пил) B — Had Been Drinking, Under Influence (Был пьян, под влиянием) C — Had Been Drinking, Not Under Influence (Был пьян, не под влиянием) D — Had Been Drinking, Impairment Unknown (Был пьян, ухудшение неизвестно) G — Impairment Unknown (Неизвестно ухудшение) H — Not Applicable (Не оценивался) - — Not Stated (Не указано) |
| Наличие телефона в автомобиле (возможности разговаривать по громкой связи) | CELLPHONE_IN_USE | 0/1 |
# Общая информация о таблице
query = '''
SELECT
COUNT(id) AS id,
COUNT(case_id) AS case_id,
COUNT(party_number) AS party_number,
COUNT(party_type) AS party_type,
COUNT(at_fault) AS at_fault,
COUNT(insurance_premium) AS insurance_premium,
COUNT(party_drug_physical) AS party_drug_physical,
COUNT(party_sobriety) AS party_sobriety,
COUNT(cellphone_in_use) AS cellphone_in_use,
COUNT(*) AS total_rows
FROM parties
'''
sql_main_info(data='parties', query=query, con=engine)
Общая информация о таблице:
| column_name | data_type | is_nullable | column_default | total_rows | non_null | is_null | |
|---|---|---|---|---|---|---|---|
| 0 | cellphone_in_use | integer | YES | None | 2752408 | 2240771 | 511637 |
| 1 | party_number | integer | YES | None | 2752408 | 2752408 | 0 |
| 2 | at_fault | integer | YES | None | 2752408 | 2752408 | 0 |
| 3 | insurance_premium | integer | YES | None | 2752408 | 2347006 | 405402 |
| 4 | id | integer | YES | None | 2752408 | 2752408 | 0 |
| 5 | case_id | text | YES | None | 2752408 | 2752408 | 0 |
| 6 | party_drug_physical | text | YES | None | 2752408 | 432288 | 2320120 |
| 7 | party_type | text | YES | None | 2752408 | 2748786 | 3622 |
| 8 | party_sobriety | text | YES | None | 2752408 | 2678453 | 73955 |
Первые 5 строк таблицы:
| id | case_id | party_number | party_type | at_fault | insurance_premium | party_sobriety | party_drug_physical | cellphone_in_use | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 22 | 3899454 | 1 | road signs | 1 | 29.0 | had not been drinking | None | 0 |
| 1 | 23 | 3899454 | 2 | road signs | 0 | 7.0 | had not been drinking | None | 0 |
| 2 | 29 | 3899462 | 2 | car | 0 | 21.0 | had not been drinking | None | 0 |
| 3 | 31 | 3899465 | 2 | road signs | 0 | 24.0 | had not been drinking | None | 0 |
| 4 | 41 | 3899478 | 2 | road bumper | 0 | NaN | not applicable | not applicable | 0 |
Все объявленные колонки в таблице присутствуют, у трети признаков количество пропусков измеряется от 14% до 85% от всех объектов в таблице.
Вывод:¶
Все объявленные таблицы присутствуют в базе и имеют набор данных.
Во всех таблицах есть общий ключ —
case_id, который указывает на уникальное ДТП, а так же есть ключ —party_number, который указывает на уникального участника каждого ДТП, в таблицахvehiclesиpartiesуникальное значение для каждой строки является сочетание этих ключей —case_idиparty_number.case_ids: Все объявленные колонки в таблице присутствуют, пропущенные значения в колонках отcутствуют.vehicles: Все объявленные колонки в таблице присутствуют, имеет по~24 тысячипропусков в признакахvehicle_ageиvehicle_transmission.collisions: Все объявленные колонки в таблице присутствуют, у нас отсутствует описание признакаcontrol_device, в75%признаков есть около10 тысячпропусков, наибольшее количество в признакеlocation_type—881 тысячпропусков иdirection—340 тысячпропусков. Что может говорить о том, что у нас не получится достаточно точно сформулировать правило, что конкретный маршрут повышает вероятность ДТП, только ориентируясь на районы города.parties: Все объявленные колонки в таблице присутствуют, у трети признаков количество пропусков измеряется от14%до85%от всех объектов в таблице.
Статистический анализ факторов ДТП¶
В какие месяцы наибольшее количество ДТП¶
# Выгрузка данных
query = '''
SELECT
EXTRACT(YEAR FROM collision_date)::integer AS year_case,
EXTRACT(MONTH FROM collision_date)::integer AS month_case,
COUNT(case_id) AS count_case
FROM collisions
GROUP BY
EXTRACT(YEAR FROM collision_date),
EXTRACT(MONTH FROM collision_date)
ORDER BY year_case, month_case
'''
count_case_per_month = pd.read_sql_query(query, con=engine)
count_case_per_month.head()
| year_case | month_case | count_case | |
|---|---|---|---|
| 0 | 2009 | 1 | 35062 |
| 1 | 2009 | 2 | 34480 |
| 2 | 2009 | 3 | 36648 |
| 3 | 2009 | 4 | 35239 |
| 4 | 2009 | 5 | 36916 |
# Построение графика для анализа полученных данных
plt.figure(figsize=(8, 5.6))
sns.lineplot(data=count_case_per_month,
x='month_case',
y='count_case',
hue='year_case',
palette='tab10')
# Настройка подписей и заголовка
plt.title('Количество ДТП по годам и месяцам', fontweight='bold', fontsize=12)
plt.xlabel('Номер месяца в году', fontsize=10)
plt.ylabel('Количество ДТП', fontsize=10)
# Вывод графика
plt.tight_layout()
plt.show()
# Вывод рейтинга ТОП-10 самых аварийных месяц
count_case_per_month.sort_values(by='count_case', ascending=False).head(10)
| year_case | month_case | count_case | |
|---|---|---|---|
| 9 | 2009 | 10 | 37835 |
| 21 | 2010 | 10 | 37480 |
| 23 | 2010 | 12 | 37070 |
| 4 | 2009 | 5 | 36916 |
| 2 | 2009 | 3 | 36648 |
| 33 | 2011 | 10 | 36618 |
| 11 | 2009 | 12 | 36060 |
| 14 | 2010 | 3 | 35803 |
| 8 | 2009 | 9 | 35555 |
| 22 | 2010 | 11 | 35460 |
Наиболее аварийным месяцем можем считать Октябрь, самым аварийным годом является 2009.
Данные по 2013 и 2020 практически отсутствуют, так же у нас не полные данные по 2012, в виду того, что у нас по ТЗ от заказчика есть ограничение по использованию данных (для модели мы должны использовать только 2012 год), мы должны сделать следующие выводы по их использованию:
Начиная с
5 месяца 2012 годау нас недостаток данных, так как количество ДТП уже существенно отклонено от исторических данных, следовательно использовать можем толькос 1 по 4 месяцыдля обучения модели, так как добавление данных остальных месяцев может внести смещение, что в последствии исказит решение модели и наши выводы.При выборе признаков для модели мы не сможем использовать информацию о времени года или месяце, так как у нас данные не за весь год.
P.S. (Если бы была возможность, заказчику было бы предложено взять данные немного смещенные, а именно с
05.2011по04.2012)
Подготовка к совещанию¶
Задачи для рассмотрения перед совещанием:¶
- Есть ли зависимость между суммой страховки и вероятностью быть виноватым ДТП?
- Какая основная причина происшествий ДТП?
- В какое состояние дороги и погоды произойдет ДТП вероятнее всего?
- Повышает ли риск быть виновником ДТП время и день недели?
- Есть ли зависимость между серьёзности повреждений транспортного средства, исходя из состояния дороги в момент ДТП?
- Какой самый аварийный район?
Рассмотрим 4 и 5 задачи.
Задача №4¶
Повышает ли риск быть виновником ДТП время и день недели?
Порядок решения задачи:
- Отфильтровать таблицу
parties, оставив только автомобили и виноватых; - Получить
case_idтолько этих происшествий; - Отфильтровать таблицу
collisionsпо полученным значениямcase_id; - Отфильтровать таблицу
до 04.2012включительно, чтобы не создавать смещений в полученных результатах; - Выделить из времени происшествия только часы;
- Выделить из даты происшествия день недели;
- Посчитать количество ДТП для каждого часа в каждом дне недели;
- Построить линейный график с делением по дням недели.
# Запрос в базу данных
query = '''
WITH
table_1 AS (
SELECT case_id
FROM parties
WHERE party_type = 'car'
AND at_fault = 1
)
SELECT EXTRACT(ISODOW FROM collision_date)::integer AS day_of_week,
EXTRACT(HOUR FROM collision_time)::integer AS hour_collision,
COUNT(case_id) AS count_collisions
FROM collisions
WHERE case_id IN (SELECT case_id FROM table_1)
AND collision_date < '2012-05-01'
AND collision_time IS NOT NULL
GROUP BY EXTRACT(ISODOW FROM collision_date),
EXTRACT(HOUR FROM collision_time)
'''
task_4 = pd.read_sql_query(query, con=engine)
# Проверка полученного результата
task_4.sample(10, random_state=RANDOM_STATE)
| day_of_week | hour_collision | count_collisions | |
|---|---|---|---|
| 4 | 1 | 4 | 1394 |
| 106 | 5 | 10 | 7797 |
| 88 | 4 | 16 | 13618 |
| 56 | 3 | 8 | 12645 |
| 109 | 5 | 13 | 11814 |
| 74 | 4 | 2 | 2249 |
| 153 | 7 | 9 | 4201 |
| 143 | 6 | 23 | 6323 |
| 72 | 4 | 0 | 2411 |
| 55 | 3 | 7 | 11976 |
# Построение графика для анализа результатов выгрузки
plt.figure(figsize=(8, 5.6))
sns.lineplot(data=task_4,
x='hour_collision',
y='count_collisions',
hue='day_of_week',
palette='tab10')
# Настройка заголовка и подписей
plt.title('Кол-во ДТП по вине водителя в зависимости от времени суток и дня недели',
fontweight='bold', fontsize=12)
plt.xlabel('Час происшествия ДТП (24-часовой формат)', fontsize=10)
plt.ylabel('Количество ДТП')
# Вывод графика
plt.tight_layout()
plt.show()
Проанализировав график, можем сделать вывод, что у нас есть 2 патерна, это ДТП происходящее в будний день либо на выходных.
В будние дни наибольшее количество ДТП происходит в часы-пик, когда люди едут на работу и с работы, наибольший пик находится в промежутке с 15 до 17 часов, в пятницу он немного смещен влево, в виду того, что с работы отпускают раньше. Так же большее количество аварий вечером объясняется усталостью людей после работы.
В выходные картина немного отличается, в дневное время ярких пиков нет и вероятность ДТП примерно одинакова, но так же присутствует отличительная черта от будних дней — это то, что есть ярко выраженный пик в ночное время, приблизительно находящийся с 2 до 3 часов ночи, далее вероятность аварии резко снижается.
Общий вывод: час поездки так и день недели имеет очень много полезной информации, которую стоит использовать в построении модели.
Задача №5¶
Есть ли зависимость между серьёзности повреждений транспортного средства, исходя из состояния дороги в момент ДТП?
Порядок решения задачи:
- Отфильтровать таблицу
parties, оставив только автомобили; - Получить
case_idтолько этих происшествий; - Посчитать количество ДТП, сгруппировав данные по уровню повреждений и состоянию дороги;
- Расчитать вероятность уровня повреждения для каждого состояния дороги;
- Отмасштабировать значения, чтобы можно было сравнивать между собой значения для каждого уровня повреждений;
- На основе данной таблицы построить
heatmap.
# Запрос в базу данных
query = '''
WITH
table_1 AS (
SELECT case_id
FROM parties
WHERE party_type = 'car'
),
table_2 AS (
SELECT collision_damage,
road_surface,
COUNT(case_id)
FROM collisions
WHERE case_id IN (SELECT case_id FROM table_1)
GROUP BY collision_damage,
road_surface
)
SELECT collision_damage,
road_surface,
count / SUM(count) OVER (PARTITION BY road_surface) AS prob_damage
FROM table_2
WHERE road_surface IS NOT NULL
'''
task_5 = pd.read_sql_query(query, con=engine)
# Проверка полученного результата
task_5.sample(10, random_state=RANDOM_STATE)
| collision_damage | road_surface | prob_damage | |
|---|---|---|---|
| 12 | middle damage | snowy | 0.105784 |
| 1 | middle damage | dry | 0.119379 |
| 14 | small damage | snowy | 0.691289 |
| 0 | severe damage | dry | 0.021870 |
| 6 | severe damage | slippery | 0.044150 |
| 13 | severe damage | snowy | 0.021603 |
| 5 | scratch | slippery | 0.223694 |
| 8 | middle damage | slippery | 0.165563 |
| 3 | scratch | dry | 0.248679 |
| 10 | scratch | snowy | 0.174913 |
# Подготовка таблицы для графика
task_5['scaled_prob'] = task_5.groupby('collision_damage')['prob_damage']\
.transform(lambda x: (x - x.mean()) / x.std())
pivot_task_5 = task_5.pivot_table(index='collision_damage',
columns='road_surface',
values='scaled_prob')
# Построение графика для анализа результатов
plt.figure(figsize=(11, 8))
sns.heatmap(data=pivot_task_5, annot=True, fmt='.3f',
linewidths=.5, cmap='cividis')
# Настройка подписей и заголовка
plt.title('Зависимость между состоянием дороги и серьезности повреждений у транспортного средства',
fontweight='bold', fontsize=12)
plt.yticks(rotation=0)
# Вывод графика
plt.tight_layout()
plt.show()
Наиболее опасное состояние дорожного покрытия — SLIPPERY, именно в такую ситуацию чаще всего происходят повреждения от среднего до фатального.
Малые повреждения чаще всего происходят, когда дорога покрыта снегом SNOWY, получение царапин происходят чаще всего, когда дорожное покрытие сухое или мокрое DRY или WET.
Рассмотрим дорожное состояние:
# Запрос в базу данных
query = '''
WITH
table_1 AS (
SELECT case_id
FROM parties
WHERE party_type = 'car'
),
table_2 AS (
SELECT collision_damage,
road_condition_1,
COUNT(case_id)
FROM collisions
WHERE case_id IN (SELECT case_id FROM table_1)
GROUP BY collision_damage,
road_condition_1
)
SELECT collision_damage,
road_condition_1,
count / SUM(count) OVER (PARTITION BY road_condition_1) AS prob_damage
FROM table_2
WHERE road_condition_1 IS NOT NULL
'''
task_5_2 = pd.read_sql_query(query, con=engine)
# Подготовка таблицы для графика
task_5_2['scaled_prob'] = task_5_2.groupby('collision_damage')['prob_damage']\
.transform(lambda x: (x - x.mean()) / x.std())
pivot_task_5_2 = task_5_2.pivot_table(index='collision_damage',
columns='road_condition_1',
values='scaled_prob')
# Построение графика для анализа результатов
plt.figure(figsize=(11, 8))
sns.heatmap(data=pivot_task_5_2, annot=True, fmt='.3f',
linewidths=.5, cmap='cividis')
# Настройка подписей и заголовка
plt.title('Зависимость между состоянием дороги и серьезности повреждений у транспортного средства',
fontweight='bold', fontsize=12)
plt.yticks(rotation=0)
# Вывод графика
plt.tight_layout()
plt.show()
Наиболее опасное состояние дороги — LOOSE MATERIAL, при таком состоянии дорогие вероятнее всего транспортное средство получит от среднего до фатального уровня повреждений.
Малые повреждения происхдят чаще всего, когда уменьшена ширина проезжей части (REDUCED WIDTH), царапину скорее всего можно получить при нормальном, затопленном или с ямами дорожном покрытии (NORMAL, FLOODED, HOLES).
Вывод:¶
Задача №4:
Проанализировав график, можем сделать вывод, что у нас есть 2 паттерна в данных, это ДТП происходящее в будний день либо на выходных.
В будние дни наибольшее количество ДТП происходит в часы-пик, когда люди едут на работу и с работы, наибольший пик находится в промежутке с 15 до 17 часов, в пятницу он немного смещен влево, в виду того, что с работы отпускают раньше. Так же большее количество аварий вечером объясняется усталостью людей после работы.
В выходные картина немного отличается, в дневное время ярких пиков нет и вероятность ДТП примерно одинакова, но так же присутствует отличительная черта от будних дней — это то, что есть ярко выраженный пик в ночное время, приблизительно находящийся с 2 до 3 часов ночи, далее вероятность аварии резко снижается.
Общий вывод: час поездки так и день недели имеет очень много полезной информации, которую стоит использовать в построении модели.
Задача №5(1):
Наиболее опасное состояние дорожного покрытия — SLIPPERY, именно в такую ситуацию чаще всего происходят повреждения от среднего до фатального.
Малые повреждения чаще всего происходят, когда дорога покрыта снегом SNOWY, получение царапин происходят чаще всего, когда дорожное покрытие сухое или мокрое DRY или WET.
Задача №5(2):
Наиболее опасное состояние дороги — LOOSE MATERIAL, при таком состоянии дорогие вероятнее всего транспортное средство получит от среднего до фатального уровня повреждений.
Малые повреждения происхдят чаще всего, когда уменьшена ширина проезжей части (REDUCED WIDTH), царапину скорее всего можно получить при нормальном, затопленном или с ямами дорожном покрытии (NORMAL, FLOODED, HOLES).
Общий вывод:
На данные момент мы можем предложить MVP — что может повышать стоимость за аренду автомобиля при таких условиях:
Среднее повышение цены:
1. Дневное время во все дни или ночное в выходные; 2. Маршрут будет проходить, где состояние дорожного покрытия SNOWY и/или REDUCED WIDTHВысокое повышение цены:
1. Время с 7 до 9 часов утра или с 15 до 17 часов в будние дни; 2. Маршрут будет проходить, где состояние дорожного покрытия SLIPPERY и/или LOOSE MATERIAL
Градацию можно обсудить на совещании, например сделать, чтобы каждый фактор добавлял свой процент к цене на аренду автомобиля, чтобы оценка была более гибкой.
Построение модели для оценки водительского риска¶
Выгрузка данных¶
При выгрузке данных нам важно взять только те признаки, которые нам будут доступны только в момент того, как водитель сел автомобиль и построил маршрут, а так же необходимо отфильтровать полученную таблицу, исходя из ТЗ от заказчика и полноты данных.
ТЗ от заказчика:
Создать модель предсказания ДТП (целевое значение — at_fault (виновник) в таблице parties);
- Для модели выбрать тип виновника — только машина (
car); - Выбрать случаи, когда ДТП привело к любым повреждениям транспортного средства, кроме типа
SCRATCH(царапина); - Для моделирования ограничиться данными за 2012 год — они самые свежие;
- Обязательное условие — учесть фактор возраста автомобиля.
Рассматриваемые признаки:
В данном пункте мы опишем те признаки, которые будут рассмотрены/использваны для обучения модели.
Таблица vehicles — информация о пострадавших машинах.
vehicle_type— тип кузова автомобиля;vehicle_transmission— тип КПП автомобиля;vehicle_age— возраст автомобиля (в годах).
Таблица collisions — общая информация о ДТП.
collision_date— дата (формат: год/месяц/день) (Будет выражен, как день недели и число месяца);collision_time— время (формат: 24-часовой) (Будет выражен, как час в сутках);intersection— есть ли на маршруте аварийные перекрестки;weather_1— погода;road_surface— состояние дороги в связи с погодными условиями;lighting— освещение на дороге по выбранному маршруту;control_device— описание признака изначально нет, но исходя из названия это говорит о наличии светофора;county_city_location— проходит ли маршрут через аварийный географический район;county_location— название географических районов, через которые проходит маршрут (если данный признак дублирует предыдущий, то от него откажемся);direction— направление движения по маршруту;location_type— тип дороги, через которую проходит маршрут;road_condition_1— состояние дорожного покрытия, через которое проходит маршрут.
Таблица parties — информация об участниках ДТП.
at_fault— виновность участника ДТП (целевой признак);insurance_premium— сумма страховки (тыс. $);cellphone_in_use— наличие телефона в автомобиле (возможности разговаривать по громкой связи).
Остальные признаки описывают случившейся ДТП, что является утечкой данных.
# Выгрузка данных
query = '''
WITH
case_list AS (
SELECT DISTINCT case_id
FROM parties
WHERE party_type = 'car'
),
table_collisions AS (
SELECT case_id,
EXTRACT(DAY FROM collision_date) AS day_of_month,
EXTRACT(ISODOW FROM collision_date) AS day_of_week,
EXTRACT(HOUR FROM collision_time) AS hour_drive,
intersection,
weather_1,
road_surface,
lighting,
control_device,
county_city_location,
county_location,
direction,
location_type,
road_condition_1
FROM collisions
WHERE case_id IN (SELECT case_id FROM case_list)
AND collision_date BETWEEN '2012-01-01' AND '2012-04-30'
AND collision_damage != 'scratch'
),
table_parties AS (
SELECT case_id,
party_number,
at_fault,
insurance_premium,
cellphone_in_use
FROM parties
WHERE case_id IN (SELECT case_id FROM table_collisions)
),
table_vehicles AS (
SELECT case_id,
party_number,
vehicle_type,
vehicle_transmission,
vehicle_age
FROM vehicles
WHERE case_id IN (SELECT case_id FROM table_collisions)
),
main_table AS (
SELECT p.case_id,
p.party_number,
p.at_fault,
p.insurance_premium,
p.cellphone_in_use,
c.day_of_month,
c.day_of_week,
c.hour_drive,
c.intersection,
c.weather_1,
c.road_surface,
c.lighting,
c.control_device,
c.county_city_location,
c.county_location,
c.direction,
c.location_type,
c.road_condition_1
FROM table_parties AS p
LEFT JOIN table_collisions AS c ON p.case_id = c.case_id
)
SELECT m.at_fault,
m.insurance_premium,
m.cellphone_in_use,
m.day_of_month,
m.day_of_week,
m.hour_drive,
m.intersection,
m.weather_1,
m.road_surface,
m.lighting,
m.control_device,
m.county_city_location,
m.county_location,
m.direction,
m.location_type,
m.road_condition_1,
v.vehicle_type,
v.vehicle_transmission,
v.vehicle_age
FROM main_table AS m
LEFT JOIN table_vehicles AS v ON m.case_id = v.case_id AND m.party_number = v.party_number
WHERE v.vehicle_age IS NOT NULL
'''
df = pd.read_sql_query(query, con=engine)
# Общая информация о таблице
gen_info(df)
<class 'pandas.core.frame.DataFrame'> RangeIndex: 46724 entries, 0 to 46723 Data columns (total 19 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 at_fault 46724 non-null int64 1 insurance_premium 45881 non-null float64 2 cellphone_in_use 42115 non-null float64 3 day_of_month 46724 non-null float64 4 day_of_week 46724 non-null float64 5 hour_drive 46660 non-null float64 6 intersection 46551 non-null float64 7 weather_1 46577 non-null object 8 road_surface 46437 non-null object 9 lighting 46579 non-null object 10 control_device 46527 non-null object 11 county_city_location 46724 non-null object 12 county_location 46724 non-null object 13 direction 35402 non-null object 14 location_type 20027 non-null object 15 road_condition_1 46488 non-null object 16 vehicle_type 46724 non-null object 17 vehicle_transmission 46124 non-null object 18 vehicle_age 46724 non-null int64 dtypes: float64(6), int64(2), object(11) memory usage: 6.8+ MB None
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| at_fault | 46724.0 | 0.494286 | 0.499973 | 0.0 | 0.0 | 0.0 | 1.0 | 1.0 |
| insurance_premium | 45881.0 | 37.272466 | 16.768324 | 0.0 | 23.0 | 34.0 | 49.0 | 104.0 |
| cellphone_in_use | 42115.0 | 0.020943 | 0.143194 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| day_of_month | 46724.0 | 15.629634 | 8.681249 | 1.0 | 8.0 | 16.0 | 23.0 | 31.0 |
| day_of_week | 46724.0 | 4.149474 | 1.972577 | 1.0 | 2.0 | 4.0 | 6.0 | 7.0 |
| hour_drive | 46660.0 | 13.372932 | 5.453253 | 0.0 | 10.0 | 14.0 | 17.0 | 23.0 |
| intersection | 46551.0 | 0.231187 | 0.421596 | 0.0 | 0.0 | 0.0 | 0.0 | 1.0 |
| vehicle_age | 46724.0 | 4.841281 | 3.114262 | 0.0 | 3.0 | 4.0 | 7.0 | 19.0 |
| at_fault | insurance_premium | cellphone_in_use | day_of_month | day_of_week | hour_drive | intersection | weather_1 | road_surface | lighting | control_device | county_city_location | county_location | direction | location_type | road_condition_1 | vehicle_type | vehicle_transmission | vehicle_age | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 28318 | 0 | NaN | 0.0 | 11.0 | 6.0 | 7.0 | 0.0 | cloudy | dry | daylight | none | 0709 | contra costa | west | None | normal | sedan | manual | 2 |
| 27653 | 0 | 27.0 | 0.0 | 3.0 | 2.0 | 19.0 | 0.0 | clear | dry | dark with street lights | none | 1942 | los angeles | west | highway | normal | sedan | auto | 3 |
| 22991 | 1 | 21.0 | 0.0 | 14.0 | 6.0 | 18.0 | 0.0 | clear | dry | dark with no street lights | none | 3801 | san francisco | west | highway | normal | sedan | auto | 3 |
| 13008 | 1 | 34.0 | 0.0 | 20.0 | 1.0 | 15.0 | 0.0 | clear | dry | daylight | none | 4803 | solano | east | highway | normal | coupe | manual | 5 |
| 11609 | 0 | 21.0 | 0.0 | 6.0 | 5.0 | 19.0 | 1.0 | clear | dry | dark with street lights | functioning | 3450 | sacramento | None | None | normal | sedan | manual | 2 |
Вывод:¶
В данном пункте работы мы выгрузили необходимые для дальнейшей работы данные, и рассмотрели основную информацию о таблице и можем составить предварительный план по предобработке данных:
- Проанализировать пропуски в данных и составить план их заполнения;
- Проверить проверить данные на явные и неявные дубликаты;
- Рассмотреть признаки
county_city_locationиcounty_location, на возможность их упрощения; - Признак
intersectionможет дублировать значения признаковdirectionилиlocation_type, необходимо проверить; - Скорректировать тип данных.
Предобработка данных¶
Анализ пропущенных значений¶
Вывод графика пропущенных значений¶
# Построение графика
msno.matrix(df)
# Настройка заголовка
plt.title('Анализ пропущенных значений', fontweight='bold', fontsize=16)
# Вывод графика
plt.show()
Пропуски располагаются рандомно, следовательно их образование имеет определенную причину, а не системную ошибку. Рассмотрим каждый столбец и составим план для их последующего системного заполнения в пайплайне.
Insurance_premium¶
# Количество уникальных значений
df['insurance_premium'].nunique()
105
Размер страховки для каждой машины расчитывается индивидуально для каждого водителя и его автомобиля учитывая множество факторов, которые нам не известны, в виду того, что пропуском не такое большое количество, заполним пропуски медианой.
Сellphone_in_use¶
В данном случае заполним пропуски из той логики, что если данную функцию не указали, то скорее всего ее не было, пропущенные значения заполним нулями.
Hour_drive¶
В этом признаке мы уже рассматривали, что каждый час имеет свою вероятность попадания в аварию, по скольку пропусков очень мало, можем заполнить средним значением, но не самого признака, а тем, где среднее значения вероятности попасть в аварию, если вернемся к графику из задачи №4, то увидим, что среднее количество аварий (8000) происзодит в 10 часов утра и через эту точку проходят все дни недели кроме воскресения.
Заполним пропуски значением 10 часов утра.
Intersection¶
Этот признак так же бинарный, заполним пропуски исходя из логики, что если значение не указано, значит там отрицательное значение ответа, заполним пропуски значением 0.
Weather_1¶
В данном случае мы никогда не можем быть уверены в какую погоду произошло ДТП, выделим для пропусков отдельную категорию и заполним пропуски значением — unknown.
Road_surface¶
В данном случае поступим по тому же принципу что и выше, пропуски будем заполнять значением — unknown.
Lighting¶
В данном случае поступим по тому же принципу что и выше, пропуски будем заполнять значением — unknown.
Control_device¶
Описания изначально нет, получим уникальные категории данного признака.
df['control_device'].unique()
array(['functioning', 'none', 'not functioning', None, 'obscured'],
dtype=object)
Получили следующие категории:
functioning— функционирует;none— отсутствует;not functioning— не функционируетobscured— что-то мешает его беспрепятственной видимости.
Категории очень подходят для признака, говорящее о наличии светофора, сделаем предположение, что этот признак говорит именно об этом.
В данном случае наиболее логично пропущенные значения заполнить категорией none, что раз не указали, значит он отсутствовал.
Direction¶
В данном случае пропусков очень много, поэтому пропуски заполним заглушкой unknown.
Location_type¶
Ситуация аналогична предыдущей, заполним пропуски заглушкой unknown.
Road_condition_1¶
В данном случае так же мы не сможем оценить точно категорию состояния дорожного покрытия, пропуски будем заполнять значением — unknown.
Vehicle_transmission¶
В данном случае поступим по тому же принципу что и выше, пропуски будем заполнять значением — unknown.
Выше мы расписали план по заполнению пропусков каждого из признаков, ниже полученный итог:
insurance_premium— медианное значение;cellphone_in_use— 0;hour_drive— 10;intersection— 0;wheather_1—unknown;road_surface—unknown;lighting—unknown;control_device—none;direction—unknown;location_type—unknown;road_condition_1—unknown;vehicle_transmossion—unknown.
Остальные признаки:
day_of_month—unknown;day_of_week—unknown;county_city_location—unknown;vehicle_type—unknown;vehicle_age— медианное значение.
Неявные дубликаты¶
# Функция для поиска скрытых дубликатов
hidden_dup_search(df)
Уникальные значения признака: weather_1 ['clear' 'cloudy' 'raining' 'fog' None 'snowing' 'other' 'wind'] Уникальные значения признака: road_surface ['dry' 'wet' 'snowy' None 'slippery'] Уникальные значения признака: lighting ['dusk_or_dawn' 'daylight' 'dark_with_no_street_lights' 'dark_with_street_lights' 'dark_with_street_lights_not_functioning' None] Уникальные значения признака: control_device ['functioning' 'none' 'not_functioning' None 'obscured'] Уникальные значения признака: county_city_location ['1920' '0700' '1005' '1942' '1952' '5400' '4313' '3612' '3001' '0900' '1939' '0600' '1992' '3607' '0105' '0100' '3801' '1900' '3024' '3600' '3010' '3700' '3400' '4806' '5000' '3014' '4000' '1915' '3105' '3604' '3900' '4006' '0106' '3335' '4803' '3009' '2900' '2700' '1502' '1965' '1941' '4203' '4300' '0734' '3045' '3011' '5100' '1990' '1000' '3004' '4303' '1975' '0101' '3404' '3300' '5604' '3705' '5002' '1977' '3019' '3005' '1956' '4005' '0198' '4200' '3701' '3394' '3602' '2400' '0109' '1200' '3015' '3781' '4202' '3002' '0500' '3605' '3708' '3003' '1948' '1700' '1953' '5405' '0702' '3713' '3100' '1910' '4117' '3711' '4114' '4905' '2708' '3313' '3302' '3610' '0400' '5202' '3702' '1925' '3315' '3609' '5607' '0405' '4113' '3016' '2800' '1401' '0300' '1979' '5704' '3318' '4400' '4900' '1500' '3616' '0104' '5600' '4312' '0112' '4316' '1906' '1944' '3018' '5200' '3310' '1914' '1973' '1922' '3026' '1907' '3040' '0701' '1949' '3710' '3103' '1932' '1947' '5609' '3640' '4500' '1013' '3029' '5701' '5800' '1954' '3709' '1964' '1936' '0107' '1300' '3017' '2100' '1919' '1912' '2300' '2109' '4110' '4106' '4302' '2600' '1934' '4109' '3920' '1926' '3496' '4004' '5603' '3618' '1950' '1935' '1921' '3450' '4101' '1012' '3906' '4980' '1955' '3000' '3780' '1931' '3308' '3306' '1928' '0704' '5601' '2303' '3008' '2901' '5608' '3619' '0708' '3905' '1946' '2401' '1203' '3021' '1901' '3101' '4807' '3309' '1985' '3401' '1967' '1918' '2706' '0103' '3782' '3314' '0792' '1400' '3712' '2000' '3611' '4008' '1976' '5500' '3022' '3490' '3341' '2200' '1995' '4102' '1917' '3028' '0111' '4802' '0800' '3342' '1800' '5406' '3345' '3630' '5005' '1961' '4314' '3007' '3601' '0108' '1927' '4502' '4904' '1100' '0791' '2908' '1015' '0790' '4307' '4311' '3720' '4100' '1959' '3020' '1913' '1201' '1600' '4214' '4402' '0710' '1506' '1962' '3307' '2106' '3904' '4116' '1989' '5700' '1933' '3603' '1943' '3706' '1938' '3048' '3392' '3311' '4403' '2002' '5690' '3200' '2406' '1602' '1923' '3902' '2802' '0113' '4404' '0709' '1905' '0711' '0712' '1908' '1969' '4906' '4204' '3317' '4308' '3106' '2707' '3325' '4700' '1902' '5407' '0402' '5007' '3050' '1503' '4104' '1994' '1304' '5001' '4111' '5703' '4801' '3305' '1963' '5102' '2101' '3312' '3051' '1991' '0715' '4315' '3344' '3301' '2805' '4800' '1970' '1937' '5300' '3703' '3500' '1008' '5004' '3337' '4908' '5801' '1510' '4310' '4103' '2709' '1971' '1972' '3903' '0404' '3707' '3704' '1004' '3608' '2102' '0707' '3615' '0303' '2405' '3783' '3013' '0706' '0102' '3006' '1960' '4120' '3049' '5602' '5605' '1603' '0602' '4127' '5702' '4304' '3680' '1909' '3621' '4001' '5201' '3025' '3316' '3343' '2301' '1601' '3690' '1929' '4901' '1001' '2105' '2108' '0705' '1951' '0403' '4205' '4902' '4115' '1903' '1511' '2703' '4002' '2601' '1916' '3012' '1507' '3631' '4212' '4903' '1801' '3501' '3617' '0305' '5006' '4306' '5501' '2701' '2110' '0714' '5403' '2103' '5606' '1205' '3901' '2404' '1515' '0901' '0601' '4600' '1301' '0200' '2801' '4804' '1508' '4305' '4401' '3104' '4907' '2712' '0716' '4108' '3402' '2304' '4501' '1945' '1509' '3303' '2710' '0302' '3336' '1501' '3023' '2500' '4580' '4709' '5101' '2711' '1930' '3606' '1007' '2902' '1968' '0902' '2001' '5008' '1999' '4805' '1505' '1974' '1002' '1993' '2104' '0501' '2704' '5009' '4206' '4107' '5404' '1980' '2705' '5408' '4706' '1302' '1702' '3613' '1011' '1101' '4119' '4003' '1306' '1924' '0703' '5003' '1690' '1701' '2804' '2803'] Уникальные значения признака: county_location ['los_angeles' 'contra_costa' 'fresno' 'tulare' 'santa_clara' 'san_bernardino' 'orange' 'el_dorado' 'colusa' 'alameda' 'san_francisco' 'san_diego' 'sacramento' 'solano' 'stanislaus' 'san_luis_obispo' 'placer' 'san_joaquin' 'riverside' 'nevada' 'monterey' 'kern' 'santa_barbara' 'sutter' 'ventura' 'merced' 'humboldt' 'calaveras' 'lake' 'san_mateo' 'sonoma' 'butte' 'tehama' 'napa' 'inyo' 'amador' 'yolo' 'santa_cruz' 'shasta' 'yuba' 'imperial' 'marin' 'mendocino' 'mono' 'madera' 'tuolumne' 'mariposa' 'del_norte' 'lassen' 'glenn' 'kings' 'plumas' 'siskiyou' 'trinity' 'san_benito' 'sierra' 'alpine' 'modoc'] Уникальные значения признака: direction ['east' 'west' 'north' 'south' None] Уникальные значения признака: location_type [None 'ramp' 'highway' 'intersection'] Уникальные значения признака: road_condition_1 ['normal' 'construction' 'other' 'obstruction' None 'holes' 'reduced_width' 'flooded' 'loose_material'] Уникальные значения признака: vehicle_type ['sedan' 'coupe' 'minivan' 'hatchback' 'other'] Уникальные значения признака: vehicle_transmission ['auto' 'manual' None]
Неявные дубликаты отсутствуют.
Признаки County_location и Сounty_city_location¶
Рассматривая значения выше мы поняли, что признак county_city_location дает нам больше информации, но это просто код, мы не можем перевести его в численное значение, а при таком количество категориальных признаков, наша таблица очень сильно разрастется, попробуем упростить эти значения.
# Количество уникальных значений
df['county_city_location'].nunique()
488
# Количество строк после группировки
df.groupby('county_location')['county_city_location'].value_counts().shape
(488,)
Кодировка локации не повторятся внутри каждого города, посмотрим, сколько кодовых значений для каждого города максимально.
df.groupby('county_location', as_index=False)['county_city_location'].nunique()\
.sort_values(by='county_city_location', ascending=False)\
.head(10)
| county_location | county_city_location | |
|---|---|---|
| 18 | los_angeles | 83 |
| 29 | orange | 35 |
| 32 | riverside | 29 |
| 35 | san_bernardino | 25 |
| 6 | contra_costa | 20 |
| 40 | san_mateo | 19 |
| 36 | san_diego | 19 |
| 42 | santa_clara | 15 |
| 0 | alameda | 14 |
| 26 | monterey | 12 |
При OneHotEncoding даже если сокращать значения минимально получится около 60 колонок, с учетом названия города, это очень много.
Принято решение: использовать TargetEncoding, оставив только признак county_city_location, так как он более информативен. Таким образом мы получим только один столбец для модели и без утечки данных.
# Удаляем лишний столбец
df.drop('county_location', axis=1, inplace=True)
Дублирование информации в признака¶
В данном пункте рассмотрим признаки которые могут дублировать у себя информацию:
intersectiondirectionlocation_type
# Связь с признаком direction
df.groupby('direction')['intersection'].mean()
direction east 0.000760 north 0.000821 south 0.000832 west 0.000371 Name: intersection, dtype: float64
В данной паре значения распределены по всем категориям, ничего не меняем.
# Связь с признаком location_type
df.groupby('location_type')['intersection'].mean()
location_type highway 0.009773 intersection 0.760151 ramp 0.154220 Name: intersection, dtype: float64
Во обоих случая 100% дублирования информации нет, оставляем все признаки без изменений.
Корректировка типов данных¶
Проверим признаки без пропусков с типом данных float64 на возможность их перевести в целочиленные значения.
num_col = ['insurance_premium',
'cellphone_in_use',
'day_of_month',
'day_of_week',
'hour_drive',
'intersection']
# Проверка, все ли значения целочисленные
for col in num_col:
if df[col].isna().sum() == 0:
if df[col].eq(df[col].astype(int)).all():
df[col] = df[col].astype(int)
print(f'Признак {col} целочисленный.')
else:
print(f'Признак {col} имеет значения с дробной частью.')
Признак day_of_month целочисленный. Признак day_of_week целочисленный.
Явные дубликаты¶
# Проверяем данные на явные дубликаты
df.duplicated().sum()
np.int64(53)
# Удаляем явные дубликаты из датафрейма
df = df.drop_duplicates().reset_index(drop=True)
Вывод:¶
На данном этапе мы выполнили следующие шаги:
- Рассмотрели каждый признак с пропусками и составили план по дальнейшиму их заполнению в пайплайне;
- Проверили категориальные признаки на наличие неявных дубликатов, они отсутствовали;
- Приняли решение по работе с признаками
county_city_locationиcounty_location, оставили только признакcounty_city_locationи решили при обучении модели использоватьTargetEncoding. - Проанализировали зависимость значений в признаках, которые могли бы дублировать информацию о месте ДТП,
100%дублирования информации не обнаружили, признаки оставили без изменений. - Удалены все явные дубликаты из датасета.
Исследовательский анализ¶
Количественные признаки¶
Insurance_premium¶
analyzis_quantity(df['insurance_premium'],
x_label='Размер страховки (тыс. $)',
system=True, target=df['vehicle_age'])
display(df.groupby('at_fault', as_index=False)['insurance_premium'].agg(['mean', 'median']))
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| insurance_premium | 45833.0 | 37.273231 | 16.767987 | 0.0 | 23.0 | 34.0 | 49.0 | 104.0 |
| Тест на нормальность распределения (порог=0.05): | |
|---|---|
| Статистика: | 820.638389 |
| Критические значения: | [0.576, 0.656, 0.787, 0.918, 1.092] |
| Распределение | Не является нормальным |
| at_fault | mean | median | |
|---|---|---|---|
| 0 | 0 | 39.400729 | 37.0 |
| 1 | 1 | 35.120237 | 30.0 |
Распределение страхового взноса insurance_premium положительно скошено: большинство значений сосредоточено в низком диапазоне, а «хвост» тянется в сторону высоких премий. Это отражает структуру автомобильного парка — дешёвых и подержанных машин значительно больше, чем новых и дорогих.
На первый взгляд может показаться, что низкий страховой взнос связан с более агрессивным вождением (поскольку среди виновников ДТП средняя премия ниже). Однако это следствие скрытой переменной — стоимости и возраста автомобиля.
В действительности:
- До ~10 лет страховой взнос в основном определяется рыночной стоимостью авто, а также стажем водителя и его историей ДТП.
- После 10 лет наблюдается обратный тренд: премия начинает расти с возрастом автомобиля, несмотря на его удешевление. Это связано с повышенным риском технических неисправностей, что увеличивает вероятность аварий.
Таким образом, высокий страховой взнос часто косвенно указывает на зрелого, опытного водителя, управляющего относительно новым или дорогим автомобилем — а значит, с меньшей вероятностью быть признанным виновным в ДТП. Обратное — низкая премия — часто ассоциируется с молодыми или малоопытными водителями на старых/дешёвых машинах, у которых риск ДТП по их вине выше.
Vehicle_age¶
analyzis_quantity(df['vehicle_age'], x_label='Возраст автомобиля', discrete='non_unique')
display(df.groupby('at_fault', as_index=False)['vehicle_age'].agg(['mean', 'median']))
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| vehicle_age | 46671.0 | 4.840929 | 3.113953 | 0.0 | 3.0 | 4.0 | 7.0 | 19.0 |
| Тест на нормальность распределения (порог=0.05): | |
|---|---|
| Статистика: | 896.896069 |
| Критические значения: | [0.576, 0.656, 0.787, 0.918, 1.092] |
| Распределение | Не является нормальным |
| at_fault | mean | median | |
|---|---|---|---|
| 0 | 0 | 5.050687 | 5.0 |
| 1 | 1 | 4.626436 | 4.0 |
Признак имеет ассимитричное положительно скошенное распределение, исходя из графиков можно лишь подтвердить наши догадки, что вероятнее всего мы здесь видим зависимость водительского стажа и количество аварий, считается, что наиболее опасный период считается от 2 до 5 лет стажа, в этот период водитель уже начинает чувствовать уверненность за рулем, но еще не хватает опыта вождения.
Либо есть другой фактор, что просто автомобилей таким возрастом больше, чем остальных.
Но точно мы не можем связать возраст автомобля, на влияние вероятности попасть в ДТП.
Категориальные признаки¶
Сellphone_in_use¶
analyzis_category(df['cellphone_in_use'])
display(df.groupby('cellphone_in_use', as_index=False)['at_fault'].agg(['mean', 'median']))
| cellphone_in_use | count | |
|---|---|---|
| 0 | 1.0 | 882 |
| 1 | 0.0 | 41192 |
| cellphone_in_use | mean | median | |
|---|---|---|---|
| 0 | 0.0 | 0.495849 | 0.0 |
| 1 | 1.0 | 0.520408 | 1.0 |
Возможность разговаривать по телефону с громкой связью имело только 2% автомобилей попавших в ДТП, что интересно, что и среднее и медианные значения по целевому признаку говорят, что это может быть фактором, который повышает риск стать виновником ДТП. Вероятно, это связано с тем, что разговор по телефону может отвлекать водителя от дороги.
Day_of_month¶
analyzis_category(df['day_of_month'])
display(df.groupby('day_of_month', as_index=False)['at_fault']
.agg(['mean', 'median'])
.sort_values(by='mean', ascending=False))
| day_of_month | count | |
|---|---|---|
| 0 | 31 | 755 |
| 1 | 30 | 1106 |
| 2 | 26 | 1359 |
| 3 | 19 | 1415 |
| 4 | 6 | 1445 |
| 5 | 12 | 1449 |
| 6 | 22 | 1459 |
| 7 | 2 | 1463 |
| 8 | 11 | 1465 |
| 9 | 1 | 1470 |
| 10 | 8 | 1473 |
| 11 | 29 | 1479 |
| 12 | 5 | 1497 |
| 13 | 15 | 1499 |
| 14 | 18 | 1502 |
| 15 | 16 | 1506 |
| 16 | 28 | 1509 |
| 17 | 25 | 1521 |
| 18 | 20 | 1571 |
| 19 | 24 | 1575 |
| 20 | 7 | 1575 |
| 21 | 9 | 1586 |
| 22 | 4 | 1587 |
| 23 | 3 | 1625 |
| 24 | 27 | 1645 |
| 25 | 10 | 1646 |
| 26 | 23 | 1664 |
| 27 | 17 | 1678 |
| 28 | 13 | 1678 |
| 29 | 21 | 1727 |
| 30 | 14 | 1742 |
| day_of_month | mean | median | |
|---|---|---|---|
| 16 | 17 | 0.528605 | 1.0 |
| 21 | 22 | 0.524332 | 1.0 |
| 0 | 1 | 0.523129 | 1.0 |
| 10 | 11 | 0.518771 | 1.0 |
| 24 | 25 | 0.517423 | 1.0 |
| 25 | 26 | 0.517292 | 1.0 |
| 17 | 18 | 0.514647 | 1.0 |
| 14 | 15 | 0.503669 | 1.0 |
| 20 | 21 | 0.500290 | 1.0 |
| 22 | 23 | 0.499399 | 0.0 |
| 11 | 12 | 0.497585 | 0.0 |
| 29 | 30 | 0.496383 | 0.0 |
| 15 | 16 | 0.496016 | 0.0 |
| 6 | 7 | 0.495873 | 0.0 |
| 27 | 28 | 0.494367 | 0.0 |
| 28 | 29 | 0.493577 | 0.0 |
| 18 | 19 | 0.493286 | 0.0 |
| 12 | 13 | 0.492253 | 0.0 |
| 13 | 14 | 0.491389 | 0.0 |
| 3 | 4 | 0.490863 | 0.0 |
| 23 | 24 | 0.486984 | 0.0 |
| 26 | 27 | 0.485714 | 0.0 |
| 1 | 2 | 0.483937 | 0.0 |
| 19 | 20 | 0.482495 | 0.0 |
| 30 | 31 | 0.478146 | 0.0 |
| 4 | 5 | 0.474282 | 0.0 |
| 7 | 8 | 0.472505 | 0.0 |
| 5 | 6 | 0.471972 | 0.0 |
| 2 | 3 | 0.470154 | 0.0 |
| 9 | 10 | 0.469623 | 0.0 |
| 8 | 9 | 0.460277 | 0.0 |
На данный момент явной зависимости по дню месяца и вероятности попасть в ДТП или стать виновником ДТП я не наблюдаю, видим, что 31 числа меньше всего ДТП, но это связано с тем, что не во всех месяцах есть это число, а вероятность стать виновником ДТП варьируется в одном диапозоне.
Day_of_week¶
analyzis_category(df['day_of_week'])
display(df.groupby('day_of_week', as_index=False)['at_fault'].agg(['mean', 'median']))
| day_of_week | count | |
|---|---|---|
| 0 | 2 | 5902 |
| 1 | 1 | 6032 |
| 2 | 3 | 6226 |
| 3 | 4 | 6385 |
| 4 | 7 | 6687 |
| 5 | 6 | 7625 |
| 6 | 5 | 7814 |
| day_of_week | mean | median | |
|---|---|---|---|
| 0 | 1 | 0.497513 | 0.0 |
| 1 | 2 | 0.476618 | 0.0 |
| 2 | 3 | 0.496948 | 0.0 |
| 3 | 4 | 0.476429 | 0.0 |
| 4 | 5 | 0.478756 | 0.0 |
| 5 | 6 | 0.511475 | 1.0 |
| 6 | 7 | 0.521011 | 1.0 |
Наибольшее количество ДТП происходит по пятницам и субботам, но вероятность стать виновником ДТП выше на выходных, вероятно, это связано с тем пиком, который появляется в ночное время, который мы наблюдали ранее при статистическом анализе дня недели и времени ДТП.
Hour_drive¶
analyzis_category(df['hour_drive'], rotation=45)
display(df.groupby('hour_drive', as_index=False)['at_fault']
.agg(['mean', 'median'])
.sort_values(by='mean', ascending=False))
| hour_drive | count | |
|---|---|---|
| 0 | 4.0 | 395 |
| 1 | 5.0 | 522 |
| 2 | 3.0 | 565 |
| 3 | 0.0 | 811 |
| 4 | 1.0 | 888 |
| 5 | 2.0 | 905 |
| 6 | 6.0 | 911 |
| 7 | 23.0 | 1037 |
| 8 | 22.0 | 1276 |
| 9 | 21.0 | 1503 |
| 10 | 9.0 | 1709 |
| 11 | 20.0 | 1800 |
| 12 | 10.0 | 1925 |
| 13 | 8.0 | 2165 |
| 14 | 7.0 | 2194 |
| 15 | 19.0 | 2275 |
| 16 | 11.0 | 2365 |
| 17 | 12.0 | 2682 |
| 18 | 13.0 | 2978 |
| 19 | 18.0 | 3228 |
| 20 | 14.0 | 3441 |
| 21 | 16.0 | 3464 |
| 22 | 17.0 | 3543 |
| 23 | 15.0 | 4025 |
| hour_drive | mean | median | |
|---|---|---|---|
| 3 | 3.0 | 0.748673 | 1.0 |
| 4 | 4.0 | 0.713924 | 1.0 |
| 1 | 1.0 | 0.708333 | 1.0 |
| 2 | 2.0 | 0.703867 | 1.0 |
| 0 | 0.0 | 0.660912 | 1.0 |
| 23 | 23.0 | 0.623915 | 1.0 |
| 5 | 5.0 | 0.603448 | 1.0 |
| 6 | 6.0 | 0.547750 | 1.0 |
| 22 | 22.0 | 0.528997 | 1.0 |
| 21 | 21.0 | 0.506321 | 1.0 |
| 8 | 8.0 | 0.484988 | 0.0 |
| 7 | 7.0 | 0.479945 | 0.0 |
| 11 | 11.0 | 0.479070 | 0.0 |
| 9 | 9.0 | 0.475717 | 0.0 |
| 14 | 14.0 | 0.470503 | 0.0 |
| 16 | 16.0 | 0.469977 | 0.0 |
| 12 | 12.0 | 0.467189 | 0.0 |
| 10 | 10.0 | 0.465455 | 0.0 |
| 13 | 13.0 | 0.463398 | 0.0 |
| 19 | 19.0 | 0.460220 | 0.0 |
| 20 | 20.0 | 0.458889 | 0.0 |
| 17 | 17.0 | 0.458369 | 0.0 |
| 18 | 18.0 | 0.456320 | 0.0 |
| 15 | 15.0 | 0.454907 | 0.0 |
Наибольшее количество аварий происходит в время с 15 до 17 часов дня, но вероятность стать виновником ДТП выше в темное время суток с 21 часов вечера до 8 часов утра.
Intersection¶
analyzis_category(df['intersection'])
display(df.groupby('intersection', as_index=False)['at_fault'].agg(['mean', 'median']))
| intersection | count | |
|---|---|---|
| 0 | 1.0 | 10749 |
| 1 | 0.0 | 35749 |
| intersection | mean | median | |
|---|---|---|---|
| 0 | 0.0 | 0.513721 | 1.0 |
| 1 | 1.0 | 0.430273 | 0.0 |
Приблизительно четверь ДТП происходит на перекрестке, вероятность стать виновником ДТП выше не на перекрестке.
Weather_1¶
analyzis_category(df['weather_1'])
display(df.groupby('weather_1', as_index=False)['at_fault'].agg(['mean', 'median']))
| weather_1 | count | |
|---|---|---|
| 0 | wind | 10 |
| 1 | other | 23 |
| 2 | snowing | 150 |
| 3 | fog | 165 |
| 4 | raining | 2152 |
| 5 | cloudy | 7605 |
| 6 | clear | 36419 |
| weather_1 | mean | median | |
|---|---|---|---|
| 0 | clear | 0.481287 | 0.0 |
| 1 | cloudy | 0.532413 | 1.0 |
| 2 | fog | 0.581818 | 1.0 |
| 3 | other | 0.695652 | 1.0 |
| 4 | raining | 0.565985 | 1.0 |
| 5 | snowing | 0.673333 | 1.0 |
| 6 | wind | 0.600000 | 1.0 |
Наибольшее количество аварий происходит в погоду clear, но вероятность стать виновником ДТП выше 50% почти во всех остальных случаях погоды, самая высокая вероятность в категорию погоды other — 70%.
Road_surface¶
analyzis_category(df['road_surface'])
display(df.groupby('road_surface', as_index=False)['at_fault'].agg(['mean', 'median']))
| road_surface | count | |
|---|---|---|
| 0 | slippery | 33 |
| 1 | snowy | 354 |
| 2 | wet | 5129 |
| 3 | dry | 40868 |
| road_surface | mean | median | |
|---|---|---|---|
| 0 | dry | 0.482676 | 0.0 |
| 1 | slippery | 0.424242 | 0.0 |
| 2 | snowy | 0.686441 | 1.0 |
| 3 | wet | 0.579060 | 1.0 |
Наибольшее количество ДТП происходит, когда дорожное покрытие dry, но вероятность стать виновником ДТП выше всего, когда дорога snowy — 69%.
Lighting¶
analyzis_category(df['lighting'], rotation=45)
display(df.groupby('lighting', as_index=False)['at_fault'].agg(['mean', 'median']))
| lighting | count | |
|---|---|---|
| 0 | dark_with_street_lights_not_functioning | 114 |
| 1 | dusk_or_dawn | 1548 |
| 2 | dark_with_no_street_lights | 4214 |
| 3 | dark_with_street_lights | 9932 |
| 4 | daylight | 30718 |
| lighting | mean | median | |
|---|---|---|---|
| 0 | dark_with_no_street_lights | 0.614855 | 1.0 |
| 1 | dark_with_street_lights | 0.515304 | 1.0 |
| 2 | dark_with_street_lights_not_functioning | 0.526316 | 1.0 |
| 3 | daylight | 0.472329 | 0.0 |
| 4 | dusk_or_dawn | 0.476744 | 0.0 |
Наибольшее количество ДТП происходит в дневное время, но вероятность стать виновником ДТП выше в темное время суток, самая высокая веротность, когда в ночное время водитель на проезжей части без освещения — 61%.
Control_device¶
analyzis_category(df['control_device'], rotation=45)
display(df.groupby('control_device', as_index=False)['at_fault'].agg(['mean', 'median']))
| control_device | count | |
|---|---|---|
| 0 | obscured | 27 |
| 1 | not_functioning | 115 |
| 2 | functioning | 15761 |
| 3 | none | 30571 |
| control_device | mean | median | |
|---|---|---|---|
| 0 | functioning | 0.436647 | 0.0 |
| 1 | none | 0.524222 | 1.0 |
| 2 | not_functioning | 0.469565 | 0.0 |
| 3 | obscured | 0.481481 | 0.0 |
Наибольшее количество ДТП произошло там, где светофор отсутствовал, там же и наиболее высокая вероятность стать виновником ДТП — 52%.
Сounty_city_location¶
analyzis_category(df['county_city_location'], top='head')
stat_location = df.groupby('county_city_location')['at_fault']\
.agg(['mean', 'median', 'count'])\
.sort_values(by='mean', ascending=False)
display(stat_location.head(10))
| county_city_location | count | |
|---|---|---|
| 0 | 3001 | 643 |
| 1 | 1500 | 687 |
| 2 | 3700 | 695 |
| 3 | 3300 | 732 |
| 4 | 4313 | 735 |
| 5 | 3600 | 801 |
| 6 | 3400 | 956 |
| 7 | 3711 | 1169 |
| 8 | 1900 | 1836 |
| 9 | 1942 | 6313 |
| mean | median | count | |
|---|---|---|---|
| county_city_location | |||
| 1701 | 1.0 | 1.0 | 1 |
| 1515 | 1.0 | 1.0 | 2 |
| 4003 | 1.0 | 1.0 | 2 |
| 5702 | 1.0 | 1.0 | 3 |
| 1924 | 1.0 | 1.0 | 1 |
| 4580 | 1.0 | 1.0 | 1 |
| 2404 | 1.0 | 1.0 | 1 |
| 5003 | 1.0 | 1.0 | 1 |
| 2104 | 1.0 | 1.0 | 1 |
| 0501 | 1.0 | 1.0 | 1 |
Наибольшее количество ДТП произошло в регионе 1942, а так же есть регионы, где произошли ДТП только по вине водителя, но их количество мало, это может в дальнейшем переобучить модель. Нужно объединить регионы с малым количеcтвом ДТП.
# Количество регионов c меньше 30 случившиемся ДТП
filter_count = stat_location[stat_location['count'] < 30]
count_location = filter_count.shape[0]
sum_collision = filter_count['count'].sum()
print(f'Количество регионов с малым количество ДТП: {count_location}')
print(f'Количество ДТП в этих регионах: {sum_collision}')
Количество регионов с малым количество ДТП: 220 Количество ДТП в этих регионах: 2480
Мы выбрали минимально количество ДТП в регионе 30 штук в виду того, что если зафиксированных случаев меньше, то статистики будут смещены и давать искаженную оценку.
# Замена значений
df['county_city_location'] = \
np.where(df['county_city_location'].isin(filter_count.index),
'small_region',
df['county_city_location'])
Построим график снова
analyzis_category(df['county_city_location'], top='head')
stat_location = df.groupby('county_city_location')['at_fault']\
.agg(['mean', 'median', 'count'])\
.sort_values(by='mean', ascending=False)
display(stat_location.head(10))
| county_city_location | count | |
|---|---|---|
| 0 | 1500 | 687 |
| 1 | 3700 | 695 |
| 2 | 3300 | 732 |
| 3 | 4313 | 735 |
| 4 | 3600 | 801 |
| 5 | 3400 | 956 |
| 6 | 3711 | 1169 |
| 7 | 1900 | 1836 |
| 8 | small_region | 2480 |
| 9 | 1942 | 6313 |
| mean | median | count | |
|---|---|---|---|
| county_city_location | |||
| 2200 | 0.744186 | 1.0 | 43 |
| 2300 | 0.741379 | 1.0 | 116 |
| 4500 | 0.737226 | 1.0 | 137 |
| 1700 | 0.736111 | 1.0 | 72 |
| 1400 | 0.733333 | 1.0 | 45 |
| 2900 | 0.711111 | 1.0 | 90 |
| 4700 | 0.707317 | 1.0 | 41 |
| 1800 | 0.697674 | 1.0 | 43 |
| 5200 | 0.696970 | 1.0 | 66 |
| 0300 | 0.689655 | 1.0 | 58 |
Рассмотрим нижние границы рейтинга
analyzis_category(df['county_city_location'], top='tail')
display(stat_location.tail(10))
| county_city_location | count | |
|---|---|---|
| 0 | 4202 | 30 |
| 1 | 1995 | 30 |
| 2 | 3903 | 30 |
| 3 | 0405 | 30 |
| 4 | 1909 | 31 |
| 5 | 4904 | 31 |
| 6 | 5701 | 31 |
| 7 | 1933 | 31 |
| 8 | 0108 | 32 |
| 9 | 3720 | 32 |
| mean | median | count | |
|---|---|---|---|
| county_city_location | |||
| 0103 | 0.382353 | 0.0 | 136 |
| 1901 | 0.378378 | 0.0 | 111 |
| 1990 | 0.378049 | 0.0 | 164 |
| 3325 | 0.377778 | 0.0 | 45 |
| 3016 | 0.365854 | 0.0 | 41 |
| 1918 | 0.358025 | 0.0 | 81 |
| 1914 | 0.352381 | 0.0 | 105 |
| 4109 | 0.349206 | 0.0 | 63 |
| 3029 | 0.348837 | 0.0 | 43 |
| 1943 | 0.340909 | 0.0 | 44 |
# Значения статистики для регионов с малым количеством ДТП
stat_location.loc['small_region']
mean 0.511694 median 1.000000 count 2480.000000 Name: small_region, dtype: float64
После объединения регионов с малым количеством ДТП мы поличили следующую оценку:
- Самое больше количество ДТП произошло в регионе
1942—6031случаев; - Самое малое количетство ДТП в регионах
1995, 3472, 5701, 1935, 3301по30случаев; - Регион с самой высокой вероятностью стать виновником ДТП
2300—107случаев, вероятность виновности78%; - Регион с самой низкой вероятностью стать виновником ДТП
3029—42случая, вероятность виновности33%; - Для объединенных регионов получили следующую статистику:
2511случаев, вероятность виновности51%.
Direction¶
analyzis_category(df['direction'])
display(df.groupby('direction', as_index=False)['at_fault'].agg(['mean', 'median']))
| direction | count | |
|---|---|---|
| 0 | east | 7897 |
| 1 | west | 8093 |
| 2 | south | 9628 |
| 3 | north | 9744 |
| direction | mean | median | |
|---|---|---|---|
| 0 | east | 0.511587 | 1.0 |
| 1 | north | 0.518268 | 1.0 |
| 2 | south | 0.509036 | 1.0 |
| 3 | west | 0.516249 | 1.0 |
Самое большое количество ДТП произошло, когда направление автомобиля северное или южное, но вероятность стать виновником ДТП во всех случая примерно одинаковая ~51%.
Location_type¶
analyzis_category(df['location_type'])
display(df.groupby('location_type', as_index=False)['at_fault'].agg(['mean', 'median']))
| location_type | count | |
|---|---|---|
| 0 | intersection | 1061 |
| 1 | ramp | 2683 |
| 2 | highway | 16267 |
| location_type | mean | median | |
|---|---|---|---|
| 0 | highway | 0.489396 | 0.0 |
| 1 | intersection | 0.451461 | 0.0 |
| 2 | ramp | 0.543422 | 1.0 |
Самое большое количество ДТП, когда тип локации highway, самое малое на перекрестке, самая высокая вероятность стать виновником ДТП в локации ramp — 54%.
Road_condition_1¶
analyzis_category(df['road_condition_1'])
display(df.groupby('road_condition_1', as_index=False)['at_fault'].agg(['mean', 'median']))
| road_condition_1 | count | |
|---|---|---|
| 0 | flooded | 32 |
| 1 | reduced_width | 58 |
| 2 | loose_material | 104 |
| 3 | holes | 150 |
| 4 | obstruction | 188 |
| 5 | other | 192 |
| 6 | construction | 716 |
| 7 | normal | 44995 |
| road_condition_1 | mean | median | |
|---|---|---|---|
| 0 | construction | 0.459497 | 0.0 |
| 1 | flooded | 0.625000 | 1.0 |
| 2 | holes | 0.453333 | 0.0 |
| 3 | loose_material | 0.615385 | 1.0 |
| 4 | normal | 0.495322 | 0.0 |
| 5 | obstruction | 0.473404 | 0.0 |
| 6 | other | 0.520833 | 1.0 |
| 7 | reduced_width | 0.603448 | 1.0 |
96% ДТП произошли на нормальном дорожном покрытии, наибольшая вероятность стать виновником ДТП, когда дорожное покрытие относится к категориям reduced_width, loose_material, flooded — от 60% до 63%.
Vehicle_type¶
analyzis_category(df['vehicle_type'])
display(df.groupby('vehicle_type', as_index=False)['at_fault'].agg(['mean', 'median']))
| vehicle_type | count | |
|---|---|---|
| 0 | other | 42 |
| 1 | minivan | 1784 |
| 2 | hatchback | 2239 |
| 3 | coupe | 13998 |
| 4 | sedan | 28608 |
| vehicle_type | mean | median | |
|---|---|---|---|
| 0 | coupe | 0.614802 | 1.0 |
| 1 | hatchback | 0.391246 | 0.0 |
| 2 | minivan | 0.536996 | 1.0 |
| 3 | other | 0.285714 | 0.0 |
| 4 | sedan | 0.441240 | 0.0 |
Наибольшее количество ДТП произошло на автомобилях с типом кузова sedan, на автомобилях с типом кузова coupe чаще всего становятся виновниками ДТП, вероятность этого — 61%.
Vehicle_transmission¶
analyzis_category(df['vehicle_transmission'])
display(df.groupby('vehicle_transmission', as_index=False)['at_fault'].agg(['mean', 'median']))
| vehicle_transmission | count | |
|---|---|---|
| 0 | auto | 21281 |
| 1 | manual | 24790 |
| vehicle_transmission | mean | median | |
|---|---|---|---|
| 0 | auto | 0.445186 | 0.0 |
| 1 | manual | 0.537071 | 1.0 |
Автомобили с ручной и автоматической коробкой передач делятся примерно пополам, но водители с автомобилей на ручной коробке передач чаще становятся виновниками ДТП, вероятность такого события — 54%.
At_fault¶
analyzis_category(df['at_fault'])
| at_fault | count | |
|---|---|---|
| 0 | 1 | 23075 |
| 1 | 0 | 23596 |
Целевой признак почти идеально сбалансирован, соотношение классов ~1:1.
Вывод:¶
Количественные признаки:
*Insurance_premium*
Распределение страхового взноса insurance_premium положительно скошено: большинство значений сосредоточено в низком диапазоне, а «хвост» тянется в сторону высоких премий. Это отражает структуру автомобильного парка — дешёвых и подержанных машин значительно больше, чем новых и дорогих.
На первый взгляд может показаться, что низкий страховой взнос связан с более агрессивным вождением (поскольку среди виновников ДТП средняя премия ниже). Однако это следствие скрытой переменной — стоимости и возраста автомобиля.
В действительности:
До ~10 лет страховой взнос в основном определяется рыночной стоимостью авто, а также стажем водителя и его историей ДТП. После 10 лет наблюдается обратный тренд: премия начинает расти с возрастом автомобиля, несмотря на его удешевление. Это связано с повышенным риском технических неисправностей, что увеличивает вероятность аварий. Таким образом, высокий страховой взнос часто косвенно указывает на зрелого, опытного водителя, управляющего относительно новым или дорогим автомобилем — а значит, с меньшей вероятностью быть признанным виновным в ДТП. Обратное — низкая премия — часто ассоциируется с молодыми или малоопытными водителями на старых/дешёвых машинах, у которых риск ДТП по их вине выше.
*Vehicle_age*
Признак имеет ассимитричное положительно скошенное распределение, исходя из графиков можно лишь подтвердить наши догадки, что вероятнее всего мы здесь видим зависимость водительского стажа и количество аварий, считается, что наиболее опасный период считается от 2 до 5 лет стажа, в этот период водитель уже начинает чувствовать уверненность за рулем, но еще не хватает опыта вождения.
Либо есть другой фактор, что просто автомобилей таким возрастом больше, чем остальных.
Но точно мы не можем связать возраст автомобля, на влияние вероятности попасть в ДТП.
Категориальные признаки:
*Сellphone_in_use*
Возможность разговаривать по телефону с громкой связью имело только 2% автомобилей попавших в ДТП, что интересно, что и среднее и медианные значения по целевому признаку говорят, что это может быть фактором, который повышает риск стать виновником ДТП. Вероятно, это связано с тем, что разговор по телефону может отвлекать водителя от дороги.
*Day_of_month*
На данный момент явной зависимости по дню месяца и вероятности попасть в ДТП или стать виновником ДТП я не наблюдаю, видим, что 31 числа меньше всего ДТП, но это связано с тем, что не во всех месяцах есть это число, а вероятность стать виновником ДТП варьируется в одном диапозоне.
*Day_of_week*
Наибольшее количество ДТП происходит по пятницам и субботам, но вероятность стать виновником ДТП выше на выходных, вероятно, это связано с тем пиком, который появляется в ночное время, который мы наблюдали ранее при статистическом анализе дня недели и времени ДТП.
*Hour_drive*
Наибольшее количество аварий происходит в время с 15 до 17 часов дня, но вероятность стать виновником ДТП выше в темное время суток с 21 часов вечера до 8 часов утра.
*Intersection*
Приблизительно четверь ДТП происходит на перекрестке, вероятность стать виновником ДТП выше не на перекрестке.
*Weather_1*
Наибольшее количество аварий происходит в погоду clear, но вероятность стать виновником ДТП выше 50% почти во всех остальных случаях погоды, самая высокая вероятность в категорию погоды other — 70%.
*Road_surface*
Наибольшее количество ДТП происходит, когда дорожное покрытие dry, но вероятность стать виновником ДТП выше всего, когда дорога snowy — 69%.
*Lighting*
Наибольшее количество ДТП происходит в дневное время, но вероятность стать виновником ДТП выше в темное время суток, самая высокая веротность, когда в ночное время водитель на проезжей части без освещения — 61%.
*Control_device*
Наибольшее количество ДТП произошло там, где светофор отсутствовал, там же и наиболее высокая вероятность стать виновником ДТП — 52%.
*Сounty_city_location*
После объединения регионов с малым количеством ДТП (меньше 30 случаев на регион) мы поличили следующую оценку:
- Самое больше количество ДТП произошло в регионе
1942—6031случаев; - Самое малое количетство ДТП в регионах
1995, 3472, 5701, 1935, 3301по30случаев; - Регион с самой высокой вероятностью стать виновником ДТП
2300—107случаев, вероятность виновности78%; - Регион с самой низкой вероятностью стать виновником ДТП
3029—42случая, вероятность виновности33%; - Для объединенных регионов получили следующую статистику:
2511случаев, вероятность виновности51%.
*Direction*
Самое большое количество ДТП произошло, когда направление автомобиля северное или южное, но вероятность стать виновником ДТП во всех случая примерно одинаковая ~51%.
*Location_type*
Самое большое количество ДТП, когда тип локации highway, самое малое на перекрестке, самая высокая вероятность стать виновником ДТП в локации ramp — 54%.
*Road_condition_1*
96% ДТП произошли на нормальном дорожном покрытии, наибольшая вероятность стать виновником ДТП, когда дорожное покрытие относится к категориям reduced_width, loose_material, flooded — от 60% до 63%.
*Vehicle_type*
Наибольшее количество ДТП произошло на автомобилях с типом кузова sedan, на автомобилях с типом кузова coupe чаще всего становятся виновниками ДТП, вероятность этого — 61%.
*Vehicle_transmission*
Автомобили с ручной и автоматической коробкой передач делятся примерно пополам, но водители с автомобилей на ручной коробке передач чаще становятся виновниками ДТП, вероятность такого события — 54%.
*At_fault*
Целевой признак почти идеально сбалансирован, соотношение классов ~1:1.
Корреляционный анализ¶
В виду того, что у нас 90% признаков категориальные, а после фильтрации датасета возраст автомобиля можно отнести так же к категориальным признакам, для изучения зависимостей признаков рассмотрим матрицу корреляции Phik.
# Строим тепловую карту
plt.figure(figsize=(12, 12))
sns.heatmap(df.phik_matrix(interval_cols=['insurance_premium']), annot=True,
fmt='.2f', linewidths=.5, cmap='cividis')
# Настройка заголовка и подписей
plt.title('Матрица Phik для анализа взаимосвязей признаков', fontweight='bold', fontsize=12)
plt.yticks(rotation=0)
# Отображаем график
plt.tight_layout()
plt.show()
# Анализ наличие мультиколинеарности с помощью vif_фактора
vif_factor(df, ['insurance_premium', 'vehicle_age'])
| input attribute | vif | |
|---|---|---|
| 0 | insurance_premium | 6.329102 |
| 1 | vehicle_age | 6.329102 |
Вывод:¶
Все входные признаки с целевой переменной имеют слабую зависимость, самый высокий показатель показывает страховой взнос insurance_premium, ранее мы обсуждали, что данный признак включает в себя очеь много факторов, оттуда и более высокая зависимость, по сравнению с остальными отдельными факторами.
Так же в нашей матрице есть несколько пар, которые указывают на умеренную или сильную зависимость:
- Ранее мы уже обсуждали, что возраст автомобиля — это один из факторов размера страхового взноса, оттуда и логична такая зависимость —
0.84, но они друг друга не дублируют, возраст отдельно все таки несет дополнительную информацию; - Зависимость освещение дороги и времени, тоже вполне логична, но в признаке освещения дороги есть дополнительная информация, которую нельзя получить из времени суток в полной мере;
- Состояние дороги и погода, тоже вполне себе объяснимая зависимость, данные признаки друг друга не дублируют, а формируют уникальные комбинации;
- Признаки, которые говорят о месте ДТП, мы проверяли ранее,
100%дублирования инфомормации там нет. - Зависимость признаков говорящих о состоянии светофора и на перекрестке ли произошло ДТП тоже логично объясняется, дублирование информации отсутствует, признак
control_deviceнесет дополнительную информацию.
Итог:
- Утечки данных не обнаружено;
- Мультиколлинеарность отсутствует, в виду того, что у нас получился только 1 количественный признак, возраст автомобиля в виду малого количества значений в нашем случае переводит данный признак в категориальный;
- От дополнительное преобразование признаков и применение техник
feature engineeringвоздержимся, в виду того, что нам требуется максимальная интерпретация модели для дальнейших выводов и бизнес-решений.
Подготовка данных к обучению модели¶
Деление данных на тренировочную и тестовую выборки¶
# Входные признаки
X = df.drop('at_fault', axis=1)
# Таргет
y = df['at_fault']
# Деление на тренировочную и тестовую выборки
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.25,
random_state=RANDOM_STATE,
stratify=y
)
# Проверка результатов деления на выборки
print(f'Размер тренировочной выборки: {X_train.shape, y_train.shape}')
print(f'Размер тестовой выборки: {X_test.shape, y_test.shape}')
Размер тренировочной выборки: ((35003, 17), (35003,)) Размер тестовой выборки: ((11668, 17), (11668,))
Построение пайплайна для заполнения пропусков¶
На данном этапе построим пайплайн для заполнения пропусков в данных, исходя из плана, который мы описали ранее. Ниже представлены название признака и значение, которым пропуски будут заполнены.
insurance_premium— медианное значение;cellphone_in_use— 0;hour_drive— 10;intersection— 0;weather_1—unknown;road_surface—unknown;lighting—unknown;control_device—none;direction—unknown;location_type—unknown;road_condition_1—unknown;vehicle_transmission—unknown.
Остальные признаки:
day_of_month—unknown;day_of_week—unknown;county_city_location—unknown;vehicle_type—unknown;vehicle_age— медианное значение.
# Списки признаков
median_col = ['insurance_premium',
'vehicle_age']
mode_col = ['day_of_month',
'day_of_week']
value_0_col = ['cellphone_in_use',
'intersection']
value_10_col = ['hour_drive']
value_none_col = ['control_device']
value_unknown_col = ['weather_1',
'road_surface',
'lighting',
'direction',
'location_type',
'road_condition_1',
'vehicle_transmission',
'county_city_location',
'vehicle_type']
# Трансформер для заполнения пропусков
imputer_transformer = ColumnTransformer([
('median', SimpleImputer(strategy='median'), median_col),
('mode', SimpleImputer(strategy='most_frequent'), mode_col),
('value_0', SimpleImputer(strategy='constant', fill_value=0), value_0_col),
('value_10', SimpleImputer(strategy='constant', fill_value=10), value_10_col),
('value_none', SimpleImputer(strategy='constant', fill_value='none'), value_none_col),
('value_unknown', SimpleImputer(strategy='constant', fill_value='unknown'), value_unknown_col)
], remainder='drop')
# Пайплайн для заполнения пропусков
imputer_pipe = Pipeline([
('replace', FunctionTransformer(replace_none, validate=False)),
('imputer', imputer_transformer)
]).set_output(transform='pandas')
Вывод:
В данном этапе построили пайплайн для заполнения пропусков, для всех признаков, и подготовили данные для дальнейшей обработке.
Построение пайплайна для подготовки данных к обучению моделей¶
Ключевые моменты, которые стоит учесть при подготовке пайплайна, это то, что у нас есть циклические временные признаки, их стоит иначе закодировать, чтобы модель понимала, что они цикличны.
Так же ранее мы уже подсвечивали, что признак указывающий на район, где произошло ДТП стоит закодировать через TargetEncoder, в виду того, что там большое количество категорий.
Количественный признак мы масштабируем.
Остальные категориальные признаки кодируем через OneHotEncoder.
# Списки признаков
num_col = ['median__insurance_premium',
'median__vehicle_age']
ohe_col = ['value_unknown__weather_1',
'value_unknown__road_surface',
'value_unknown__lighting',
'value_none__control_device',
'value_unknown__direction',
'value_unknown__location_type',
'value_unknown__road_condition_1',
'value_unknown__vehicle_type',
'value_unknown__vehicle_transmission']
time_col = ['mode__day_of_month',
'mode__day_of_week',
'value_10__hour_drive']
loc_col = ['value_unknown__county_city_location']
# Пайплайн для TargetEncoder
target_pipe = Pipeline([
('target', TargetEncoder(target_type='binary', cv=3, random_state=RANDOM_STATE)),
('scaler', StandardScaler())
])
# Подготовка данных
preprocessor = ColumnTransformer([
('num', StandardScaler(), num_col),
('ohe', OneHotEncoder(handle_unknown='ignore',
drop='first',
sparse_output=False), ohe_col),
('loc', target_pipe, loc_col),
('time', FunctionTransformer(cos_sin_feature, validate=False), time_col)
], remainder='passthrough')
Вывод:¶
В данном пункте мы разделили данные на тренировочную и тестовую выборку и построили пайплайн для подготовки данных к обучению моделей.
Обучение моделей¶
Выбор метрики:
Поскольку классы сбалансированы, а бизнес-стоимость ошибок I и II рода в поставленной задаче неизвестна, в качестве основной метрики выбран F1-score — как гармоническое среднее между precision и recall, отражающее баланс между ложными срабатываниями и пропущенными положительными случаями.
Выбор моделей:
У нас 95% признаков категориальные, лучше всего с такими данными справляются модели основанные на деревьях решений, от этого и исходил наш выбор:
DummyClassifier— baseline;LogisticRegression— самая простая из выбранных модель, ее очень легко интерпретировать, очень быстро работает, легко ввести в продакшен;RandomForestClassifier—baggingмодель основанная на деревьях решений, может быстро дать хороший результат;LightGBM—boostingмодель основанная на деревьях решений, самая быстрая модель среди своего класса.
Пайплайн для моделей¶
# Итоговый пайплайн
model_pipe = Pipeline([
('imputer', imputer_pipe),
('processor', preprocessor),
('model', DummyClassifier(strategy='stratified', random_state=RANDOM_STATE))
])
Инициализация поиска гиперпараметров и словари к ним¶
DummyClassifier¶
dummy = cross_val_score(model_pipe, X_train, y_train, cv=3, scoring='f1')
LinearRegression¶
# Словарь поиска гиперпараметров
param_lr = {
'model': [LogisticRegression(max_iter=5000,
solver='saga',
penalty='elasticnet',
random_state=RANDOM_STATE)],
'model__l1_ratio': Real(0, 1, prior='uniform'),
'model__C': Real(0.01, 100, prior='log-uniform'),
'processor__num': Categorical([StandardScaler(), MinMaxScaler()])
}
# Инициализация поиска гиперпараметров
bayes_lr = BayesSearchCV(
model_pipe,
param_lr,
n_iter=10,
cv=3,
scoring='f1',
n_jobs=-1,
random_state=RANDOM_STATE
)
RandomForestClassifier¶
# Словарь поиска гиперпараметров
param_rf = {
'model': [RandomForestClassifier(n_estimators=100,
random_state=RANDOM_STATE)],
'model__max_depth': Integer(3, 15),
'model__min_samples_split': Integer(3, 30),
'model__min_samples_leaf': Integer(1, 30),
'processor__num': Categorical(['passthrough'])
}
# Инициализация поиска гиперпараметров
bayes_rf = BayesSearchCV(
model_pipe,
param_rf,
n_iter=30,
cv=3,
scoring='f1',
n_jobs=-1,
random_state=RANDOM_STATE
)
LGBMClassifier¶
# Словарь поиска гиперпараметров
param_lgbm = {
'model': [LGBMClassifier(subsample=0.8,
colsample_bytree=0.8,
min_child_samples=None,
verbosity=-1,
random_state=RANDOM_STATE)],
'model__max_depth': Integer(2, 12),
'model__num_leaves': Integer(3, 127),
'model__n_estimators': Integer(100, 1000),
'model__learning_rate': Real(0.01, 0.1, prior='log-uniform'),
'model__min_data_in_leaf': Integer(100, 300),
'processor__num': Categorical(['passthrough'])
}
# Инициализация поиска гиперпараметров
bayes_lgbm = BayesSearchCV(
model_pipe,
param_lgbm,
n_iter=40,
cv=3,
scoring='f1',
n_jobs=-1,
random_state=RANDOM_STATE
)
Поиск лучших гиперпараметров и выбор лучшей модели¶
# Словарь и список необходимые для обучения моделей
result_data = []
models = {
'Dummy': dummy,
'LogisticRegression': bayes_lr,
'RandomForest': bayes_rf,
'LightGBM': bayes_lgbm,
}
# Цикл обучения моделей
for name, search in models.items():
# Обучение моделей и поиск гиперпараметров
if name == 'Dummy':
print('=' * 75)
print(f'Обучение модели: {name}')
print(f'Метрика F1-score: {np.mean(dummy):.4f} ± {np.std(dummy):.4f}')
# Сохраняем результат
result_data.append({
'Model': name,
'Best_CV_score': f'{np.mean(dummy):.4f} ± {np.std(dummy):.4f}',
'Best_params': ' — '
})
continue
print('=' * 75)
print(f'Обучение модели: {name}')
search.fit(X_train, y_train)
# Сохраняем лучшие результаты
result_data.append({
'Model': name,
'Best_CV_score': f'{search.best_score_:.4f}',
'Best_params': search.best_params_
})
# Логирование результатов обучения моделей
model_result = pd.DataFrame(search.cv_results_)
print(f'Лучшая метрика F1-score на кросс-валидации: {search.best_score_:.4f}')
print('\nПодробнее:')
display(model_result.sort_values(by='rank_test_score')
.loc[:, 'split0_test_score':'std_test_score']
.head(3))
# Вывод итоговой таблицы результатов
result_data = pd.DataFrame(result_data)
display(result_data.sort_values(by='Best_CV_score', ascending=False))
=========================================================================== Обучение модели: Dummy Метрика F1-score: 0.4958 ± 0.0021 =========================================================================== Обучение модели: LogisticRegression Лучшая метрика F1-score на кросс-валидации: 0.5937 Подробнее:
| split0_test_score | split1_test_score | split2_test_score | mean_test_score | std_test_score | |
|---|---|---|---|---|---|
| 6 | 0.588866 | 0.594980 | 0.597125 | 0.593657 | 0.003499 |
| 3 | 0.589318 | 0.593155 | 0.597662 | 0.593378 | 0.003410 |
| 0 | 0.588741 | 0.589610 | 0.597128 | 0.591827 | 0.003766 |
=========================================================================== Обучение модели: RandomForest Лучшая метрика F1-score на кросс-валидации: 0.6097 Подробнее:
| split0_test_score | split1_test_score | split2_test_score | mean_test_score | std_test_score | |
|---|---|---|---|---|---|
| 14 | 0.611238 | 0.604715 | 0.613215 | 0.609723 | 0.003632 |
| 16 | 0.611238 | 0.604715 | 0.613215 | 0.609723 | 0.003632 |
| 0 | 0.608119 | 0.606760 | 0.610737 | 0.608539 | 0.001650 |
=========================================================================== Обучение модели: LightGBM Лучшая метрика F1-score на кросс-валидации: 0.6161 Подробнее:
| split0_test_score | split1_test_score | split2_test_score | mean_test_score | std_test_score | |
|---|---|---|---|---|---|
| 13 | 0.613326 | 0.613544 | 0.621465 | 0.616112 | 0.003786 |
| 26 | 0.613437 | 0.615175 | 0.618570 | 0.615727 | 0.002132 |
| 2 | 0.614257 | 0.610798 | 0.620583 | 0.615213 | 0.004051 |
| Model | Best_CV_score | Best_params | |
|---|---|---|---|
| 3 | LightGBM | 0.6161 | {'model': LGBMClassifier(colsample_bytree=0.8, min_child_samples=None, random_state=6011994, subsample=0.8, verbosity=-1), 'model__learning_rate': 0.09976300404573386, 'model__max_depth': 3, 'model__min_data_in_leaf': 168, 'model__n_estimators': 266, 'model__num_leaves': 126, 'processor__num': 'passthrough'} |
| 2 | RandomForest | 0.6097 | {'model': RandomForestClassifier(random_state=6011994), 'model__max_depth': 13, 'model__min_samples_leaf': 30, 'model__min_samples_split': 3, 'processor__num': 'passthrough'} |
| 1 | LogisticRegression | 0.5937 | {'model': LogisticRegression(max_iter=5000, penalty='elasticnet', random_state=6011994, solver='saga'), 'model__C': 0.010202028607228303, 'model__l1_ratio': 0.48579505990672556, 'processor__num': StandardScaler()} |
| 0 | Dummy | 0.4958 ± 0.0021 | — |
Лучшей моделью стала LightGBM ее средняя метрика F1-score на кросс-валидации — 0.6161.
Однако все 3 модели показали результат очень близкий друг к другу, если бы нашей целью не была максимальная интерпретируемость, то мы могли бы построить ансамблевую модель из 3х ранее нами обученных для повышения точности прогноза.
Прогноз лучшей модели на тестовой выборки¶
# Присваиваем переменную для лучшей модели
model = bayes_lgbm.best_estimator_
# Прогнозирование на тестовой выборке
test_pred = model.predict(X_test)
test_proba = model.predict_proba(X_test)[:, 1]
Анализ результатов¶
Проанализируем метрики и матрицу ошибок для прогноза на тестовой выборке.
# Матрица ошибок
cm = confusion_matrix(y_test, test_pred)
plt.figure(figsize=(7, 7))
matrix_plot = sns.heatmap(cm, annot=True, fmt='d', cmap='Blues_r')
# Настройка заголовка и подписей
plt.title('Матрица ошибок', fontweight='bold', fontsize=12)
plt.xlabel('Predicted')
plt.ylabel('True')
# Вывод результатов
plt.show()
# Вывод отчета по метрикам
print(classification_report(y_test, test_pred))
print(f" {'roc_auc':<33} {roc_auc_score(y_test, test_proba):.2f} {len(y_test):>9}")
precision recall f1-score support
0 0.63 0.70 0.66 5899
1 0.65 0.58 0.61 5769
accuracy 0.64 11668
macro avg 0.64 0.64 0.64 11668
weighted avg 0.64 0.64 0.64 11668
roc_auc 0.69 11668
Вывод:¶
На данном этапе мы нашли наилучшие гиперпараметры для выбранных моделей, и выбрали из них лучшую:
result_data.sort_values(by='Best_CV_score', ascending=False)
| Model | Best_CV_score | Best_params | |
|---|---|---|---|
| 3 | LightGBM | 0.6161 | {'model': LGBMClassifier(colsample_bytree=0.8, min_child_samples=None, random_state=6011994, subsample=0.8, verbosity=-1), 'model__learning_rate': 0.09976300404573386, 'model__max_depth': 3, 'model__min_data_in_leaf': 168, 'model__n_estimators': 266, 'model__num_leaves': 126, 'processor__num': 'passthrough'} |
| 2 | RandomForest | 0.6097 | {'model': RandomForestClassifier(random_state=6011994), 'model__max_depth': 13, 'model__min_samples_leaf': 30, 'model__min_samples_split': 3, 'processor__num': 'passthrough'} |
| 1 | LogisticRegression | 0.5937 | {'model': LogisticRegression(max_iter=5000, penalty='elasticnet', random_state=6011994, solver='saga'), 'model__C': 0.010202028607228303, 'model__l1_ratio': 0.48579505990672556, 'processor__num': StandardScaler()} |
| 0 | Dummy | 0.4958 ± 0.0021 | — |
Лучшая модель — LightGBM
Лучшие гиперпараметры модели:
{'model': LGBMClassifier(n_estimators=266,
learning_rate=0.09976300404573386,
max_depth=3,
min_data_in_leaf=168,
num_leaves=126,
subsample=0.8,
colsample_bytree=0.8,
random_state=6011994,
verbosity=-1)
Метрики качества модели на тестовых данных:
recall—0.58precision—0.65F1-score—0.61roc-auc—0.69accuracy—0.64
Пояснение для графика и полученных метрик:
Наша задача — предсказывать, был ли водитель виновником ДТП (класс 1 = виновен, класс 0 = не виновен).
Для наглядности обозначим:
- Истинно положительные (TP) — модель правильно идентифицировала виновных водителей
~3357. - Ложно положительные (FP) — модель ошибочно назвала невиновного водителя виновным
~1796. - Ложно отрицательные (FN) — модель пропустила реально виновного водителя (сказала «не виновен», хотя это не так)
~2412. - Истинно отрицательные (TN) — модель правильно определила, что водитель не виновен
~4103.
- Recall (полнота) по классу 1:
58%
Модель находит
58%реально виновных водителей. Остальные42%виновников остаются незамеченными — модель ошибочно считает их невиновными.Бизнес-последствие: упущенные риски. Модель пропускает (считает не виновным) каждого 5го виновника ДТП, что является существенным риском для автомобилей каршеринговой компании.
- Precision (точность) по классу 1:
65%
Из всех водителей, которых модель назвала виновниками, только
65%действительно виновны. Остальные35%— ложные срабатывания.Бизнес-последствие: излишняя осторожность. Если на основе предсказания мы намеренны повышать тариф, то каждый 3й клиент может получить необоснованное удорожание, что вызовет недовольство и отток.
- F1-score и ROC AUC:
F1 для класса 1:
0.61— показывает умеренную сбалансированность точности и полноты, но нигде нет высокого качества, при необходимости можно подобрать порог для минимизации ошибокI(ложное обвинение) илиII(пропуск виновного) рода, тем самым повысив качество точности прогноза модели или полноты отбора объектов 1 класса.ROC AUC:
0.69— модель немного лучше случайного угадывания при всех возможных порогах классификации.
Итог:
Модель умеренно полезна, но недостаточно надёжна для автономного принятия решений. На данный момент ее можно использовать только в качестве статистического анализа факторов влияющих на выявление виновности водителя в ДТП.
Для повышения ее качества необходимо получение информации о финансовых издержках при ошибках I(ложное обвинение) и II(пропуск виновного) рода, таким образом подобрать порог классификации, чтобы минимизировать вероятные издержки компании.
Либо получение дополнительной информации, которая могла бы более точно выявлять вероятность риска ДТП. Далее мы расмотрим какие из факторов нам в этом помогут.
Анализ важности факторов ДТП с помощью SHAP¶
Подготовка даннных и расчет SHAP-значений¶
# Получим подвыборку из тестовых данных для SHAP
X_shap, _, y_shap, _ = train_test_split(
X_test, y_test,
train_size=1000,
stratify=y_test,
random_state=RANDOM_STATE
)
# Подготавливаем данные
X_shap_processed = Pipeline(model.steps[:-1]).transform(X_shap)
# Выделяем модель в переменную
model_shap = model.named_steps['model']
# Названия колонок
feature_names = get_feature_names(
preprocessor=model.named_steps['processor'],
num_col=num_col,
ohe_col=ohe_col,
loc_col=loc_col,
time_col=time_col
)
# Расчитываем SHAP-значения
explainer = shap.TreeExplainer(model_shap)
shap_values = explainer(X_shap_processed)
Построение графиков и анализ результатов:¶
# Строим график важности признаков
shap.summary_plot(
shap_values,
X_shap_processed,
rng=np.random.default_rng(RANDOM_STATE),
feature_names=feature_names,
show=False
)
# Получаем текущие оси
ax = plt.gca()
# Меняем размер графика
plt.gcf().set_size_inches(12, 10)
# Настройка заголовка и подписей
plt.title('Анализ важности признаков методом SHAP', fontweight='bold', fontsize=12)
plt.xlabel('SHAP-значение')
# Вывод графика
plt.tight_layout()
plt.show()
# Строим график важности признаков (столбчатый график)
shap.summary_plot(
shap_values,
X_shap_processed,
plot_type='bar',
rng=np.random.default_rng(RANDOM_STATE),
feature_names=feature_names,
show=False
)
# Получаем текущие оси
ax = plt.gca()
# Меняем размер графика
plt.gcf().set_size_inches(10, 10)
# Настройка заголовка и подписей
plt.title('Анализ важности признаков методом SHAP', fontweight='bold', fontsize=12)
plt.xlabel('Важность признаков')
# Вывод графика
plt.tight_layout()
plt.show()
Наиболее информативным признаком для модели стал — vehicle_type. Этот признак закодированный через OneHotEncoder, поэтому должны складывать вклад каждой категории этого признака.
Наиболее важные признаки для модели:
vehicle_type— тип автомобильного кузова;insurance_premium— размер страхового взноса;county_city_location— регион, где находится водитель, вероятно это связано с загруженностью дорог.
Ранее заказчик просил обязательно включить возраст автомобиля для построения модели, мы видим, что этот признак попадает в ТОП-10 признаков по важности, рассмотрим его зависимость с целевым признаком.
Исследование признака vehicle_age:¶
# Получение зависимости возраста автомобиля и вероятности стать виновноком ДТП
research_age = df.groupby('vehicle_age', as_index=False)['at_fault'].mean()
# Построение графика
plt.figure(figsize=(8, 5.6))
sns.lineplot(data=research_age,
x='vehicle_age',
y='at_fault')
# Настройка подписей и заголовка
plt.title('Зависимость возраста автомобиля и вероятности стать виновноком ДТП',
fontweight='bold', fontsize=12)
plt.xlabel('Возраст автомобиля')
plt.ylabel('Вероятность стать виновником ДТП')
plt.xticks(np.sort(research_age['vehicle_age']))
# Вывод графика
plt.tight_layout()
plt.show()
# Количество автомобилей попавших в ДТП для каждого возраста
df['vehicle_age'].value_counts().to_frame()
| count | |
|---|---|
| vehicle_age | |
| 3 | 9116 |
| 4 | 5920 |
| 2 | 5570 |
| 5 | 4639 |
| 7 | 3342 |
| 6 | 3301 |
| 8 | 3065 |
| 0 | 2610 |
| 9 | 2384 |
| 1 | 2350 |
| 10 | 1669 |
| 11 | 1211 |
| 12 | 741 |
| 13 | 471 |
| 14 | 233 |
| 15 | 36 |
| 16 | 9 |
| 17 | 3 |
| 19 | 1 |
Вывод о графике:
Как я и говорил ранее, связь с тем, что водители на автомобиле с возрастом в 3 года чаще становятся виновниками ДТП является не причиной, а следствием. Так как период с 2 до 4 лет является самым опасным временем для водителя, а влияние автомобиля на это событие мы косвенно уже увидели при анализе размере страхового взноса.
Tолько после 10 лет возраст автомобиля начинает свое влияние, которые не используются в каршеринговых компаниях, но и полученные значения вероятностей там достаточно смещены, в виду того, что в наших данных достаточно мало происшествий с автомобилями такого возраста, например ДТП с автомобилем с возрастом 19 лет всего один.
Тут встает вопрос, а как мы можем объективно оценить настоящий стаж водителя, ведь права он мог получить, но так и не сесть за руль, по множеству причин, от отсутствия автомобиля до непреодоления страха перед тем, как самостоятельно выехать на автомобиле.
Предложения о допольнительном оборудовании автомобиля:
На данный момент мы можем начать оценивать стиль вождения автомобилиста — Установка датчика на рулевую рейку.
Гипотеза: автомобилист, у которого агрессивный или неадекватный стиль вождения, будет часто и необоснованно перестраиваться из ряда в ряд, что повышает риск столкновения с другими участниками дорожного движения.
Какие данные для этого нужны?
- Данные с датчика рулевой рейки;
- Скорость автомобиля (прямой доступ или через GPS);
- Местоположение автомобиля (GPS).
Подробнее:
Данный датчик будет получать информацию о том, как активно и как часто водитель проводит маневры, получая дополнительную информацию о скорости вождения, о месте его положения, а именно это была перестройка в другой ряд или это просто поворот на перекрестке, как часто он это делает, а действительно ли это было необходимо? Эта система сработает не сразу, но в дальнейшем с накоплением данных мы обучим модель, которая сможет:
- Уберечь нас не только от агрессивных или неопытных водителей, но так же и от пьяного вождения;
- Получим историю о каждом водителе, о характере его вождения, что поможет сделать оценку наиболее объективной.
Именно получая эти данные и обучая на них модели, мы сможем оценить вождение каждого автомобилиста и сформировать для каждого свою цену для аренды автомобиля.
Общий вывод:¶
Описание проекта:
От каршеринговой компании поступил заказ: нужно создать систему, которая могла бы оценить риск ДТП по выбранному маршруту движения. Под риском понимается вероятность ДТП с любым повреждением транспортного средства. Как только водитель забронировал автомобиль, сел за руль и выбрал маршрут, система должна оценить уровень риска. Если уровень риска высок, водитель увидит предупреждение и рекомендации по маршруту.
Идея создания такой системы находится в стадии предварительного обсуждения и проработки. Чёткого алгоритма работы и подобных решений на рынке ещё не существует.
Цель проекта: понять, возможно ли предсказывать ДТП, опираясь на исторические данные одного из регионов.
ТЗ от заказчика:
Создать модель предсказания ДТП (целевое значение —
at_fault(виновник) в таблицеparties);- Для модели выбрать тип виновника — только машина (
car); - Выбрать случаи, когда ДТП привело к любым повреждениям транспортного средства, кроме типа
SCRATCH(царапина); - Для моделирования ограничиться данными за 2012 год — они самые свежие;
- Обязательное условие — учесть фактор возраста автомобиля.
- Для модели выбрать тип виновника — только машина (
На основе модели исследовать основные факторы ДТП и понять, помогут ли результаты моделирования и анализ важности факторов ответить на вопросы:
- Возможно ли создать адекватную системы оценки водительского риска при выдаче авто?
- Какие ещё факторы нужно учесть?
- Нужно ли оборудовать автомобиль какими-либо датчиками или камерой?
Заказчик предлагает поработать с базой данных по происшествиям и сформировать свои идеи создания такой системы.
Описание данных:
collisions— общая информация о ДТП. Имеет уникальныйcase_id. Эта таблица описывает общую информацию о ДТП. Например, где оно произошло и когда.parties— информация об участниках ДТП. Имеет неуникальныйcase_id, который сопоставляется с соответствующим ДТП в таблицеcollisions. Каждая строка здесь описывает одну из сторон, участвующих в ДТП. Если столкнулись две машины, в этой таблице должно быть две строки с совпадениемcase_id. Если нужен уникальный идентификатор, этоcase_idиparty_number.vehicles— информация о пострадавших машинах. Имеет неуникальныеcase_idи неуникальныеparty_number, которые сопоставляются с таблицейcollisionsи таблицейparties. Если нужен уникальный идентификатор, этоcase_idиparty_number.
Ход исследования:
Подготовка данных: загрузка и изучение общей информации из представленных датасетов.Статистический анализ факторов ДТП: рассматриваются вопросы, которые помогут лучше понимать данные и их взаимосвязи.Предобработка данных: выгрузка датасета из базы данных, необходимого для обучения модели и дальнейшая его предобработка — заполнение пропусков, обработка явных и неявных дубликатов, корректировка типов данных.Исследовательский анализ данных: изучение признаков, их распределение, поиск выбросов/аномалий в данных.Корреляционный анализ: изучение взимосвязей между входными признаками и целевыми, а также и между ними.Обучение моделей и выбор лучшей: подготовка данных для обучения моделей с помощью построенных пайплайнов, использованиеBayesSearchCVдля поиска лучших гиперпараметров для моделей, сравнение лучших метрик моделей на кросс-валидации и выбор лучшей, лучей моделью делаем прогноз на тестовых данных и проводит анализ результатов.Анализ важности факторов ДТП с помощью SHAP: анализ степени важности признаков их влияния на принятие решений моделью с помощью методаSHAP, и проводим дополнительное исследование одного из признаков.Общий вывод: резюмирование полученных результатов, формулировка ключевых выводов и рекомендаций.
Результаты работы:
Обучена и выбрана лучшая модель, на тестовой выборке модель показала тот же результат, что и при обучении.
Модель обнаруживает 58% случаев, когда водитель виновен в ДТП, с точностью в 65%.
Лучшая модель — LightGBM
Лучшие гиперпараметры модели:
{'model': LGBMClassifier(n_estimators=266,
learning_rate=0.09976300404573386,
max_depth=3,
min_data_in_leaf=168,
num_leaves=126,
subsample=0.8,
colsample_bytree=0.8,
random_state=6011994,
verbosity=-1)
Метрики качества модели на тестовых данных:
recall—0.58precision—0.65F1-score—0.61roc-auc—0.69accuracy—0.64
Пояснение для графика и полученных метрик:
Наша задача — предсказывать, был ли водитель виновником ДТП (класс 1 = виновен, класс 0 = не виновен).
Для наглядности обозначим:
- Истинно положительные (TP) — модель правильно идентифицировала виновных водителей
~3357. - Ложно положительные (FP) — модель ошибочно назвала невиновного водителя виновным
~1796. - Ложно отрицательные (FN) — модель пропустила реально виновного водителя (сказала «не виновен», хотя это не так)
~2412. - Истинно отрицательные (TN) — модель правильно определила, что водитель не виновен
~4103.
- Recall (полнота) по классу 1:
58%
Модель находит
58%реально виновных водителей. Остальные42%виновников остаются незамеченными — модель ошибочно считает их невиновными.Бизнес-последствие: упущенные риски. Модель пропускает (считает не виновным) каждого 5го виновника ДТП, что является существенным риском для автомобилей каршеринговой компании.
- Precision (точность) по классу 1:
65%
Из всех водителей, которых модель назвала виновниками, только
65%действительно виновны. Остальные35%— ложные срабатывания.Бизнес-последствие: излишняя осторожность. Если на основе предсказания мы намеренны повышать тариф, то каждый 3й клиент может получить необоснованное удорожание, что вызовет недовольство и отток.
- F1-score и ROC AUC:
F1 для класса 1:
0.61— показывает умеренную сбалансированность точности и полноты, но нигде нет высокого качества, при необходимости можно подобрать порог для минимизации ошибокI(ложное обвинение) илиII(пропуск виновного) рода, тем самым повысив качество точности прогноза модели или полноты отбора объектов 1 класса.ROC AUC:
0.69— модель немного лучше случайного угадывания при всех возможных порогах классификации.
Итог:
Модель умеренно полезна, но недостаточно надёжна для автономного принятия решений. На данный момент ее можно использовать только в качестве статистического анализа факторов влияющих на выявление виновности водителя в ДТП.
Для повышения ее качества необходимо получение информации о финансовых издержках при ошибках I(ложное обвинение) и II(пропуск виновного) рода, таким образом подобрать порог классификации, чтобы минимизировать вероятные издержки компании.
Либо получение дополнительной информации, которая могла бы более точно выявлять вероятность риска ДТП. Далее мы расмотрим какие из факторов нам в этом помогут.
Анализ важности факторов ДТП:
Наиболее информативным признаком для модели стал — vehicle_type. Этот признак закодированный через OneHotEncoder, поэтому должны складывать вклад каждой категории этого признака.
Наиболее важные признаки для модели:
vehicle_type— тип автомобильного кузова;insurance_premium— размер страхового взноса;county_city_location— регион, где находится водитель, вероятно это связано с загруженностью дорог.
Дополнительное исследование:
Ранее заказчик просил обязательно включить возраст автомобиля для построения модели, мы видим, что этот признак попадает в ТОП-10 признаков по важности, рассмотрим его зависимость с целевым признаком.
Как я и говорил ранее, связь с тем, что водители на автомобиле с возрастом в 3 года чаще становятся виновниками ДТП является не причиной, а следствием. Так как период с 2 до 4 лет является самым опасным временем для водителя, а влияние автомобиля на это событие мы косвенно уже увидели при анализе размере страхового взноса.
Tолько после 10 лет возраст автомобиля начинает свое влияние, которые не используются в каршеринговых компаниях, но и полученные значения вероятностей там достаточно смещены, в виду того, что в наших данных достаточно мало происшествий с автомобилями такого возраста, например ДТП с автомобилем с возрастом 19 лет всего один.
Тут встает вопрос, а как мы можем объективно оценить настоящий стаж водителя, ведь права он мог получить, но так и не сесть за руль, по множеству причин, от отсутствия автомобиля до непреодоления страха перед тем, как самостоятельно выехать на автомобиле.
Предложения о допольнительном оборудовании автомобиля:
На данный момент мы можем начать оценивать стиль вождения автомобилиста — Установка датчика на рулевую рейку.
Гипотеза: автомобилист, у которого агрессивный или неадекватный стиль вождения, будет часто и необоснованно перестраиваться из ряда в ряд, что повышает риск столкновения с другими участниками дорожного движения.
Какие данные для этого нужны?
- Данные с датчика рулевой рейки;
- Скорость автомобиля (прямой доступ или через GPS);
- Местоположение автомобиля (GPS).
Подробнее:
Данный датчик будет получать информацию о том, как активно и как часто водитель проводит маневры, получая дополнительную информацию о скорости вождения, о месте его положения, а именно это была перестройка в другой ряд или это просто поворот на перекрестке, как часто он это делает, а действительно ли это было необходимо? Эта система сработает не сразу, но в дальнейшем с накоплением данных мы обучим модель, которая сможет:
- Уберечь нас не только от агрессивных или неопытных водителей, но так же и от пьяного вождения;
- Получим историю о каждом водителе, о характере его вождения, что поможет сделать оценку наиболее объективной.
Именно получая эти данные и обучая на них модели, мы сможем оценить вождение каждого автомобилиста и сформировать для каждого свою цену для аренды автомобиля.
Ответ на поставленную задачу:
Возможно ли предсказывать ДТП, опираясь на исторические данные одного из регионов?
На данном этапе модель делает очень грубый отбор и мы рискуем потерять клиентов, при необоснованном поднятии цены на аренду автомобиля, либо излишними предупреждениями автомобилистам о том, чтобы они были аккуратнее на дороге. Но при построении системы, которая была описана выше, мы можем на одном регионе получить данные о водителях и их стиле вождении, модель научится более точно определять паттерны в манере вождения автомобилистов, которые ведут к риску ДТП, которую в последствии можно будет внедрить на остальные регионы, после успешного А/Б-тестирования нового подхода.