🔗 Репозиторий на GitHub

Проект: Оценка риска ДТП по выбранному маршруту движения¶

Описание проекта:

От каршеринговой компании поступил заказ: нужно создать систему, которая могла бы оценить риск ДТП по выбранному маршруту движения. Под риском понимается вероятность ДТП с любым повреждением транспортного средства. Как только водитель забронировал автомобиль, сел за руль и выбрал маршрут, система должна оценить уровень риска. Если уровень риска высок, водитель увидит предупреждение и рекомендации по маршруту.

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

Цель проекта: понять, возможно ли предсказывать ДТП, опираясь на исторические данные одного из регионов.

ТЗ от заказчика:

  1. Создать модель предсказания ДТП (целевое значение — at_fault (виновник) в таблице parties);

    • Для модели выбрать тип виновника — только машина (car);
    • Выбрать случаи, когда ДТП привело к любым повреждениям транспортного средства, кроме типа SCRATCH (царапина);
    • Для моделирования ограничиться данными за 2012 год — они самые свежие;
    • Обязательное условие — учесть фактор возраста автомобиля.
  2. На основе модели исследовать основные факторы ДТП и понять, помогут ли результаты моделирования и анализ важности факторов ответить на вопросы:

    • Возможно ли создать адекватную системы оценки водительского риска при выдаче авто?
    • Какие ещё факторы нужно учесть?
    • Нужно ли оборудовать автомобиль какими-либо датчиками или камерой?

Заказчик предлагает поработать с базой данных по происшествиям и сформировать свои идеи создания такой системы.

Описание данных:

  • collisions — общая информация о ДТП. Имеет уникальный case_id. Эта таблица описывает общую информацию о ДТП. Например, где оно произошло и когда.

  • parties — информация об участниках ДТП. Имеет неуникальный case_id, который сопоставляется с соответствующим ДТП в таблице collisions. Каждая строка здесь описывает одну из сторон, участвующих в ДТП. Если столкнулись две машины, в этой таблице должно быть две строки с совпадением case_id. Если нужен уникальный идентификатор, это case_id и party_number.

  • vehicles — информация о пострадавших машинах. Имеет неуникальные case_id и неуникальные party_number, которые сопоставляются с таблицей collisions и таблицей parties. Если нужен уникальный идентификатор, это case_id и party_number.

No description has been provided for this image

Описание признаков:

  • Таблица 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, и проводим дополнительное исследование одного из признаков.

  • Общий вывод: резюмирование полученных результатов, формулировка ключевых выводов и рекомендаций.

Подготовка рабочей среды и вспомогательные функции¶

Импорт библиотек и базовые настройки блокнота¶

In [1]:
# Стандартные библиотеки
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.*")

Вспомогательные функции¶

In [2]:
# Функция для получения общей информации о датафрейме
def gen_info(df):
    '''
    Данная функция выводит общую информацию 
    о датафрейме, статистическое описание признаков
    и 5 рандомных строк.
    На ввод функция принимает переменную датафрейма.
    '''
    # Статистики по количественным признакам
    desc = df.describe().T

    # Вывод результатов
    print(df.info())
    display(desc)
    display(df.sample(5, random_state=RANDOM_STATE))
In [3]:
# Функция для поиска неявных дубликатов
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()
In [4]:
# Функция для комплексного анализа количественного признака
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(''))
In [5]:
# Функция для анализа категорийных значений

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())
In [6]:
# Построение таблицы с расчитанным 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
In [7]:
# Функция для получения общей информации о таблице
# с помощью 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)
In [8]:
# Функция для преобразования временных признаков
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
In [9]:
# Функция для получения названий колонок
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
In [10]:
# Функция для замены пропущеных значений
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

Подключение к базе¶

In [11]:
# Конфигурация для подключения
db_config = {'user': 'praktikum_student', 
             'pwd': 'Sdf4$2;d-d30pp', 
             'host': 'rc1b-wcoijxj3yxfsf3fs.mdb.yandexcloud.net', 
             'port': 6432, 
             'db': 'data-science-vehicle-db'
} 
In [12]:
# Строка для соединение с базой
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)

Общая информация о таблицах — первичное исследование¶

Проверка наличия таблиц в базе данных¶

In [13]:
query = '''

SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
'''

table = pd.read_sql_query(query, con=engine)
table
Out[13]:
table_name
0 case_ids
1 collisions
2 parties
3 vehicles

Все объявленные таблицы в базе данных присутствуют.

ER - диаграмма¶

No description has been provided for this image

Таблица Case_ids¶

In [14]:
# Общая информация о таблице
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 год, что не соотносится с данными из первых строк таблицы, выведем по каким годам есть информация.

In [15]:
query = '''

SELECT DISTINCT db_year
FROM case_ids
'''

table = pd.read_sql_query(query, con=engine)
table
Out[15]:
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 Число
In [16]:
# Общая информация о таблице
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 (Не указано)
In [17]:
# Общая инофрмация о таблице
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
In [18]:
# Общая информация о таблице
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% от всех объектов в таблице.

Статистический анализ факторов ДТП¶

В какие месяцы наибольшее количество ДТП¶

In [19]:
# Выгрузка данных
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()
Out[19]:
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
In [20]:
# Построение графика для анализа полученных данных
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)
No description has been provided for this image
Out[20]:
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)

Подготовка к совещанию¶

Задачи для рассмотрения перед совещанием:¶

  1. Есть ли зависимость между суммой страховки и вероятностью быть виноватым ДТП?
  2. Какая основная причина происшествий ДТП?
  3. В какое состояние дороги и погоды произойдет ДТП вероятнее всего?
  4. Повышает ли риск быть виновником ДТП время и день недели?
  5. Есть ли зависимость между серьёзности повреждений транспортного средства, исходя из состояния дороги в момент ДТП?
  6. Какой самый аварийный район?

Рассмотрим 4 и 5 задачи.

Задача №4¶

Повышает ли риск быть виновником ДТП время и день недели?

Порядок решения задачи:

  • Отфильтровать таблицу parties, оставив только автомобили и виноватых;
  • Получить case_id только этих происшествий;
  • Отфильтровать таблицу collisions по полученным значениям case_id;
  • Отфильтровать таблицу до 04.2012 включительно, чтобы не создавать смещений в полученных результатах;
  • Выделить из времени происшествия только часы;
  • Выделить из даты происшествия день недели;
  • Посчитать количество ДТП для каждого часа в каждом дне недели;
  • Построить линейный график с делением по дням недели.
In [21]:
# Запрос в базу данных
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)
In [22]:
# Проверка полученного результата
task_4.sample(10, random_state=RANDOM_STATE)
Out[22]:
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
In [23]:
# Построение графика для анализа результатов выгрузки
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()
No description has been provided for this image

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

В будние дни наибольшее количество ДТП происходит в часы-пик, когда люди едут на работу и с работы, наибольший пик находится в промежутке с 15 до 17 часов, в пятницу он немного смещен влево, в виду того, что с работы отпускают раньше. Так же большее количество аварий вечером объясняется усталостью людей после работы.

В выходные картина немного отличается, в дневное время ярких пиков нет и вероятность ДТП примерно одинакова, но так же присутствует отличительная черта от будних дней — это то, что есть ярко выраженный пик в ночное время, приблизительно находящийся с 2 до 3 часов ночи, далее вероятность аварии резко снижается.

Общий вывод: час поездки так и день недели имеет очень много полезной информации, которую стоит использовать в построении модели.

Задача №5¶

Есть ли зависимость между серьёзности повреждений транспортного средства, исходя из состояния дороги в момент ДТП?

Порядок решения задачи:

  • Отфильтровать таблицу parties, оставив только автомобили;
  • Получить case_id только этих происшествий;
  • Посчитать количество ДТП, сгруппировав данные по уровню повреждений и состоянию дороги;
  • Расчитать вероятность уровня повреждения для каждого состояния дороги;
  • Отмасштабировать значения, чтобы можно было сравнивать между собой значения для каждого уровня повреждений;
  • На основе данной таблицы построить heatmap.
In [24]:
# Запрос в базу данных
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)
In [25]:
# Проверка полученного результата
task_5.sample(10, random_state=RANDOM_STATE)
Out[25]:
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
In [26]:
# Подготовка таблицы для графика
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()
No description has been provided for this image

Наиболее опасное состояние дорожного покрытия — SLIPPERY, именно в такую ситуацию чаще всего происходят повреждения от среднего до фатального.

Малые повреждения чаще всего происходят, когда дорога покрыта снегом SNOWY, получение царапин происходят чаще всего, когда дорожное покрытие сухое или мокрое DRY или WET.

Рассмотрим дорожное состояние:

In [27]:
# Запрос в базу данных
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)
In [28]:
# Подготовка таблицы для графика
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()
No description has been provided for this image

Наиболее опасное состояние дороги — 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 — наличие телефона в автомобиле (возможности разговаривать по громкой связи).

Остальные признаки описывают случившейся ДТП, что является утечкой данных.

In [29]:
# Выгрузка данных

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)
In [30]:
# Общая информация о таблице
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, необходимо проверить;
  • Скорректировать тип данных.

Предобработка данных¶

Анализ пропущенных значений¶

Вывод графика пропущенных значений¶
In [31]:
# Построение графика
msno.matrix(df)

# Настройка заголовка
plt.title('Анализ пропущенных значений', fontweight='bold', fontsize=16)

# Вывод графика
plt.show()
No description has been provided for this image

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

Insurance_premium¶
In [32]:
# Количество уникальных значений
df['insurance_premium'].nunique()
Out[32]:
105

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

Сellphone_in_use¶

В данном случае заполним пропуски из той логики, что если данную функцию не указали, то скорее всего ее не было, пропущенные значения заполним нулями.

Hour_drive¶

В этом признаке мы уже рассматривали, что каждый час имеет свою вероятность попадания в аварию, по скольку пропусков очень мало, можем заполнить средним значением, но не самого признака, а тем, где среднее значения вероятности попасть в аварию, если вернемся к графику из задачи №4, то увидим, что среднее количество аварий (8000) происзодит в 10 часов утра и через эту точку проходят все дни недели кроме воскресения.

Заполним пропуски значением 10 часов утра.

Intersection¶

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

Weather_1¶

В данном случае мы никогда не можем быть уверены в какую погоду произошло ДТП, выделим для пропусков отдельную категорию и заполним пропуски значением — unknown.

Road_surface¶

В данном случае поступим по тому же принципу что и выше, пропуски будем заполнять значением — unknown.

Lighting¶

В данном случае поступим по тому же принципу что и выше, пропуски будем заполнять значением — unknown.

Control_device¶

Описания изначально нет, получим уникальные категории данного признака.

In [33]:
df['control_device'].unique()
Out[33]:
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 — медианное значение.

Неявные дубликаты¶

In [34]:
# Функция для поиска скрытых дубликатов
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 дает нам больше информации, но это просто код, мы не можем перевести его в численное значение, а при таком количество категориальных признаков, наша таблица очень сильно разрастется, попробуем упростить эти значения.

In [35]:
# Количество уникальных значений
df['county_city_location'].nunique()
Out[35]:
488
In [36]:
# Количество строк после группировки
df.groupby('county_location')['county_city_location'].value_counts().shape
Out[36]:
(488,)

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

In [37]:
df.groupby('county_location', as_index=False)['county_city_location'].nunique()\
  .sort_values(by='county_city_location', ascending=False)\
  .head(10)
Out[37]:
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, так как он более информативен. Таким образом мы получим только один столбец для модели и без утечки данных.

In [38]:
# Удаляем лишний столбец
df.drop('county_location', axis=1, inplace=True)

Дублирование информации в признака¶

В данном пункте рассмотрим признаки которые могут дублировать у себя информацию:

  • intersection
  • direction
  • location_type
In [39]:
# Связь с признаком direction
df.groupby('direction')['intersection'].mean()
Out[39]:
direction
east     0.000760
north    0.000821
south    0.000832
west     0.000371
Name: intersection, dtype: float64

В данной паре значения распределены по всем категориям, ничего не меняем.

In [40]:
# Связь с признаком location_type
df.groupby('location_type')['intersection'].mean()
Out[40]:
location_type
highway         0.009773
intersection    0.760151
ramp            0.154220
Name: intersection, dtype: float64

Во обоих случая 100% дублирования информации нет, оставляем все признаки без изменений.

Корректировка типов данных¶

Проверим признаки без пропусков с типом данных float64 на возможность их перевести в целочиленные значения.

In [41]:
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 целочисленный.

Явные дубликаты¶

In [42]:
# Проверяем данные на явные дубликаты
df.duplicated().sum()
Out[42]:
np.int64(53)
In [43]:
# Удаляем явные дубликаты из датафрейма
df = df.drop_duplicates().reset_index(drop=True)

Вывод:¶

На данном этапе мы выполнили следующие шаги:

  • Рассмотрели каждый признак с пропусками и составили план по дальнейшиму их заполнению в пайплайне;
  • Проверили категориальные признаки на наличие неявных дубликатов, они отсутствовали;
  • Приняли решение по работе с признаками county_city_location и county_location, оставили только признак county_city_location и решили при обучении модели использовать TargetEncoding.
  • Проанализировали зависимость значений в признаках, которые могли бы дублировать информацию о месте ДТП, 100% дублирования информации не обнаружили, признаки оставили без изменений.
  • Удалены все явные дубликаты из датасета.

Исследовательский анализ¶

Количественные признаки¶

Insurance_premium¶
In [44]:
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']))
No description has been provided for this image
No description has been provided for this image
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¶
In [45]:
analyzis_quantity(df['vehicle_age'], x_label='Возраст автомобиля', discrete='non_unique')

display(df.groupby('at_fault', as_index=False)['vehicle_age'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [46]:
analyzis_category(df['cellphone_in_use'])

display(df.groupby('cellphone_in_use', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [47]:
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))
No description has been provided for this image
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¶
In [48]:
analyzis_category(df['day_of_week'])

display(df.groupby('day_of_week', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [49]:
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))
No description has been provided for this image
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¶
In [50]:
analyzis_category(df['intersection'])

display(df.groupby('intersection', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [51]:
analyzis_category(df['weather_1'])

display(df.groupby('weather_1', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [52]:
analyzis_category(df['road_surface'])

display(df.groupby('road_surface', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [53]:
analyzis_category(df['lighting'], rotation=45)

display(df.groupby('lighting', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [54]:
analyzis_category(df['control_device'], rotation=45)

display(df.groupby('control_device', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [55]:
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))
No description has been provided for this image
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твом ДТП.

In [56]:
# Количество регионов 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 штук в виду того, что если зафиксированных случаев меньше, то статистики будут смещены и давать искаженную оценку.

In [57]:
# Замена значений
df['county_city_location'] = \
    np.where(df['county_city_location'].isin(filter_count.index), 
             'small_region', 
             df['county_city_location'])

Построим график снова

In [58]:
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))
No description has been provided for this image
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

Рассмотрим нижние границы рейтинга

In [59]:
analyzis_category(df['county_city_location'], top='tail')

display(stat_location.tail(10))
No description has been provided for this image
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
In [60]:
# Значения статистики для регионов с малым количеством ДТП
stat_location.loc['small_region']
Out[60]:
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¶
In [61]:
analyzis_category(df['direction'])

display(df.groupby('direction', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [62]:
analyzis_category(df['location_type'])

display(df.groupby('location_type', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [63]:
analyzis_category(df['road_condition_1'])

display(df.groupby('road_condition_1', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [64]:
analyzis_category(df['vehicle_type'])

display(df.groupby('vehicle_type', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [65]:
analyzis_category(df['vehicle_transmission'])

display(df.groupby('vehicle_transmission', as_index=False)['at_fault'].agg(['mean', 'median']))
No description has been provided for this image
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¶
In [66]:
analyzis_category(df['at_fault'])
No description has been provided for this image
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.

In [67]:
# Строим тепловую карту
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()
No description has been provided for this image
In [68]:
# Анализ наличие мультиколинеарности с помощью vif_фактора
vif_factor(df, ['insurance_premium', 'vehicle_age'])
Out[68]:
input attribute vif
0 insurance_premium 6.329102
1 vehicle_age 6.329102

Вывод:¶

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

Так же в нашей матрице есть несколько пар, которые указывают на умеренную или сильную зависимость:

  • Ранее мы уже обсуждали, что возраст автомобиля — это один из факторов размера страхового взноса, оттуда и логична такая зависимость — 0.84, но они друг друга не дублируют, возраст отдельно все таки несет дополнительную информацию;
  • Зависимость освещение дороги и времени, тоже вполне логична, но в признаке освещения дороги есть дополнительная информация, которую нельзя получить из времени суток в полной мере;
  • Состояние дороги и погода, тоже вполне себе объяснимая зависимость, данные признаки друг друга не дублируют, а формируют уникальные комбинации;
  • Признаки, которые говорят о месте ДТП, мы проверяли ранее, 100% дублирования инфомормации там нет.
  • Зависимость признаков говорящих о состоянии светофора и на перекрестке ли произошло ДТП тоже логично объясняется, дублирование информации отсутствует, признак control_device несет дополнительную информацию.

Итог:

  • Утечки данных не обнаружено;
  • Мультиколлинеарность отсутствует, в виду того, что у нас получился только 1 количественный признак, возраст автомобиля в виду малого количества значений в нашем случае переводит данный признак в категориальный;
  • От дополнительное преобразование признаков и применение техник feature engineering воздержимся, в виду того, что нам требуется максимальная интерпретация модели для дальнейших выводов и бизнес-решений.

Подготовка данных к обучению модели¶

Деление данных на тренировочную и тестовую выборки¶

In [69]:
# Входные признаки
X = df.drop('at_fault', axis=1)

# Таргет
y = df['at_fault']
In [70]:
# Деление на тренировочную и тестовую выборки
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 — медианное значение.
In [71]:
# Списки признаков
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']
In [72]:
# Трансформер для заполнения пропусков
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')
In [73]:
# Пайплайн для заполнения пропусков
imputer_pipe = Pipeline([
    
    ('replace', FunctionTransformer(replace_none, validate=False)),
    ('imputer', imputer_transformer)
    
]).set_output(transform='pandas')

Вывод:

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

Построение пайплайна для подготовки данных к обучению моделей¶

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

Так же ранее мы уже подсвечивали, что признак указывающий на район, где произошло ДТП стоит закодировать через TargetEncoder, в виду того, что там большое количество категорий.

Количественный признак мы масштабируем.

Остальные категориальные признаки кодируем через OneHotEncoder.

In [74]:
# Списки признаков
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']
In [75]:
# Пайплайн для TargetEncoder
target_pipe = Pipeline([
    ('target', TargetEncoder(target_type='binary', cv=3, random_state=RANDOM_STATE)),
    ('scaler', StandardScaler())
])
In [76]:
# Подготовка данных
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 модель основанная на деревьях решений, самая быстрая модель среди своего класса.

Пайплайн для моделей¶

In [77]:
# Итоговый пайплайн
model_pipe = Pipeline([
    
    ('imputer', imputer_pipe),
    ('processor', preprocessor),
    ('model', DummyClassifier(strategy='stratified', random_state=RANDOM_STATE))
])

Инициализация поиска гиперпараметров и словари к ним¶

DummyClassifier¶
In [78]:
dummy = cross_val_score(model_pipe, X_train, y_train, cv=3, scoring='f1')
LinearRegression¶
In [79]:
# Словарь поиска гиперпараметров
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()])
}
In [80]:
# Инициализация поиска гиперпараметров
bayes_lr = BayesSearchCV(
    model_pipe, 
    param_lr, 
    n_iter=10, 
    cv=3, 
    scoring='f1', 
    n_jobs=-1, 
    random_state=RANDOM_STATE
)
RandomForestClassifier¶
In [81]:
# Словарь поиска гиперпараметров
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'])
}
In [82]:
# Инициализация поиска гиперпараметров
bayes_rf = BayesSearchCV(
    model_pipe, 
    param_rf, 
    n_iter=30, 
    cv=3, 
    scoring='f1', 
    n_jobs=-1, 
    random_state=RANDOM_STATE
)
LGBMClassifier¶
In [83]:
# Словарь поиска гиперпараметров
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'])
}
In [84]:
# Инициализация поиска гиперпараметров
bayes_lgbm = BayesSearchCV(
    model_pipe, 
    param_lgbm, 
    n_iter=40, 
    cv=3, 
    scoring='f1', 
    n_jobs=-1, 
    random_state=RANDOM_STATE
)

Поиск лучших гиперпараметров и выбор лучшей модели¶

In [85]:
# Словарь и список необходимые для обучения моделей
result_data = []

models = {
    'Dummy': dummy,
    'LogisticRegression': bayes_lr, 
    'RandomForest': bayes_rf, 
    'LightGBM': bayes_lgbm,
}
In [86]:
# Цикл обучения моделей
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х ранее нами обученных для повышения точности прогноза.

Прогноз лучшей модели на тестовой выборки¶

In [87]:
# Присваиваем переменную для лучшей модели
model = bayes_lgbm.best_estimator_
In [88]:
# Прогнозирование на тестовой выборке
test_pred = model.predict(X_test)
test_proba = model.predict_proba(X_test)[:, 1]

Анализ результатов¶

Проанализируем метрики и матрицу ошибок для прогноза на тестовой выборке.

In [89]:
# Матрица ошибок
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}")
No description has been provided for this image
              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

Вывод:¶

На данном этапе мы нашли наилучшие гиперпараметры для выбранных моделей, и выбрали из них лучшую:

In [90]:
result_data.sort_values(by='Best_CV_score', ascending=False)
Out[90]:
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.58
  • precision — 0.65
  • F1-score — 0.61
  • roc-auc — 0.69
  • accuracy — 0.64

Пояснение для графика и полученных метрик:

Наша задача — предсказывать, был ли водитель виновником ДТП (класс 1 = виновен, класс 0 = не виновен).

Для наглядности обозначим:

  • Истинно положительные (TP) — модель правильно идентифицировала виновных водителей ~3357.
  • Ложно положительные (FP) — модель ошибочно назвала невиновного водителя виновным ~1796.
  • Ложно отрицательные (FN) — модель пропустила реально виновного водителя (сказала «не виновен», хотя это не так) ~2412.
  • Истинно отрицательные (TN) — модель правильно определила, что водитель не виновен ~4103.
  1. Recall (полнота) по классу 1: 58%

Модель находит 58% реально виновных водителей. Остальные 42% виновников остаются незамеченными — модель ошибочно считает их невиновными.

Бизнес-последствие: упущенные риски. Модель пропускает (считает не виновным) каждого 5го виновника ДТП, что является существенным риском для автомобилей каршеринговой компании.

  1. Precision (точность) по классу 1: 65%

Из всех водителей, которых модель назвала виновниками, только 65% действительно виновны. Остальные 35% — ложные срабатывания.

Бизнес-последствие: излишняя осторожность. Если на основе предсказания мы намеренны повышать тариф, то каждый 3й клиент может получить необоснованное удорожание, что вызовет недовольство и отток.

  1. F1-score и ROC AUC:

F1 для класса 1: 0.61 — показывает умеренную сбалансированность точности и полноты, но нигде нет высокого качества, при необходимости можно подобрать порог для минимизации ошибок I(ложное обвинение) или II(пропуск виновного) рода, тем самым повысив качество точности прогноза модели или полноты отбора объектов 1 класса.

ROC AUC: 0.69 — модель немного лучше случайного угадывания при всех возможных порогах классификации.

Итог:

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

Для повышения ее качества необходимо получение информации о финансовых издержках при ошибках I(ложное обвинение) и II(пропуск виновного) рода, таким образом подобрать порог классификации, чтобы минимизировать вероятные издержки компании.

Либо получение дополнительной информации, которая могла бы более точно выявлять вероятность риска ДТП. Далее мы расмотрим какие из факторов нам в этом помогут.

Анализ важности факторов ДТП с помощью SHAP¶

Подготовка даннных и расчет SHAP-значений¶

In [91]:
# Получим подвыборку из тестовых данных для SHAP
X_shap, _, y_shap, _ = train_test_split(
    X_test, y_test,
    train_size=1000, 
    stratify=y_test,
    random_state=RANDOM_STATE
)
In [92]:
# Подготавливаем данные
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
)
In [93]:
# Расчитываем SHAP-значения
explainer = shap.TreeExplainer(model_shap)
shap_values = explainer(X_shap_processed)

Построение графиков и анализ результатов:¶

In [94]:
# Строим график важности признаков
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()
No description has been provided for this image
In [95]:
# Строим график важности признаков (столбчатый график)
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()
No description has been provided for this image

Наиболее информативным признаком для модели стал — vehicle_type. Этот признак закодированный через OneHotEncoder, поэтому должны складывать вклад каждой категории этого признака.

Наиболее важные признаки для модели:

  • vehicle_type — тип автомобильного кузова;
  • insurance_premium — размер страхового взноса;
  • county_city_location — регион, где находится водитель, вероятно это связано с загруженностью дорог.

Ранее заказчик просил обязательно включить возраст автомобиля для построения модели, мы видим, что этот признак попадает в ТОП-10 признаков по важности, рассмотрим его зависимость с целевым признаком.

Исследование признака vehicle_age:¶

In [96]:
# Получение зависимости возраста автомобиля и вероятности стать виновноком ДТП
research_age = df.groupby('vehicle_age', as_index=False)['at_fault'].mean()
In [97]:
# Построение графика
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()
No description has been provided for this image
In [98]:
# Количество автомобилей попавших в ДТП для каждого возраста
df['vehicle_age'].value_counts().to_frame()
Out[98]:
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).

Подробнее:

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

  • Уберечь нас не только от агрессивных или неопытных водителей, но так же и от пьяного вождения;
  • Получим историю о каждом водителе, о характере его вождения, что поможет сделать оценку наиболее объективной.

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

Общий вывод:¶

Описание проекта:

От каршеринговой компании поступил заказ: нужно создать систему, которая могла бы оценить риск ДТП по выбранному маршруту движения. Под риском понимается вероятность ДТП с любым повреждением транспортного средства. Как только водитель забронировал автомобиль, сел за руль и выбрал маршрут, система должна оценить уровень риска. Если уровень риска высок, водитель увидит предупреждение и рекомендации по маршруту.

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

Цель проекта: понять, возможно ли предсказывать ДТП, опираясь на исторические данные одного из регионов.

ТЗ от заказчика:

  1. Создать модель предсказания ДТП (целевое значение — at_fault (виновник) в таблице parties);

    • Для модели выбрать тип виновника — только машина (car);
    • Выбрать случаи, когда ДТП привело к любым повреждениям транспортного средства, кроме типа SCRATCH (царапина);
    • Для моделирования ограничиться данными за 2012 год — они самые свежие;
    • Обязательное условие — учесть фактор возраста автомобиля.
  2. На основе модели исследовать основные факторы ДТП и понять, помогут ли результаты моделирования и анализ важности факторов ответить на вопросы:

    • Возможно ли создать адекватную системы оценки водительского риска при выдаче авто?
    • Какие ещё факторы нужно учесть?
    • Нужно ли оборудовать автомобиль какими-либо датчиками или камерой?

Заказчик предлагает поработать с базой данных по происшествиям и сформировать свои идеи создания такой системы.

Описание данных:

  • 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.58
  • precision — 0.65
  • F1-score — 0.61
  • roc-auc — 0.69
  • accuracy — 0.64

Пояснение для графика и полученных метрик:

Наша задача — предсказывать, был ли водитель виновником ДТП (класс 1 = виновен, класс 0 = не виновен).

Для наглядности обозначим:

  • Истинно положительные (TP) — модель правильно идентифицировала виновных водителей ~3357.
  • Ложно положительные (FP) — модель ошибочно назвала невиновного водителя виновным ~1796.
  • Ложно отрицательные (FN) — модель пропустила реально виновного водителя (сказала «не виновен», хотя это не так) ~2412.
  • Истинно отрицательные (TN) — модель правильно определила, что водитель не виновен ~4103.
  1. Recall (полнота) по классу 1: 58%

Модель находит 58% реально виновных водителей. Остальные 42% виновников остаются незамеченными — модель ошибочно считает их невиновными.

Бизнес-последствие: упущенные риски. Модель пропускает (считает не виновным) каждого 5го виновника ДТП, что является существенным риском для автомобилей каршеринговой компании.

  1. Precision (точность) по классу 1: 65%

Из всех водителей, которых модель назвала виновниками, только 65% действительно виновны. Остальные 35% — ложные срабатывания.

Бизнес-последствие: излишняя осторожность. Если на основе предсказания мы намеренны повышать тариф, то каждый 3й клиент может получить необоснованное удорожание, что вызовет недовольство и отток.

  1. 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).

Подробнее:

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

  • Уберечь нас не только от агрессивных или неопытных водителей, но так же и от пьяного вождения;
  • Получим историю о каждом водителе, о характере его вождения, что поможет сделать оценку наиболее объективной.

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

Ответ на поставленную задачу:

Возможно ли предсказывать ДТП, опираясь на исторические данные одного из регионов?

На данном этапе модель делает очень грубый отбор и мы рискуем потерять клиентов, при необоснованном поднятии цены на аренду автомобиля, либо излишними предупреждениями автомобилистам о том, чтобы они были аккуратнее на дороге. Но при построении системы, которая была описана выше, мы можем на одном регионе получить данные о водителях и их стиле вождении, модель научится более точно определять паттерны в манере вождения автомобилистов, которые ведут к риску ДТП, которую в последствии можно будет внедрить на остальные регионы, после успешного А/Б-тестирования нового подхода.