Лондон – это столица Великобритании и один из крупнейших городов мира. Этот город также обладает одной из самых развитых транспортных систем в мире. Лондонский метрополитен – один из крупнейших и старейших в мире, общая протяженность его сети превышает 400 км и насчитывает 270 станций. Автобусная сеть Лондона также является одной из крупнейших в мире. Она состоит из 675 маршрутов, которые обслуживают около 9300 автобусов.
Примечательно, что, в отличие от многих мировых столиц, лондонские автобусы пользуются большей популярностью у местных жителей, чем метро. Если метро пользуется в среднем 3 миллиона человек в день, то автобусами – более 6 миллионов человек в день.
Разумеется, при таком огромном потоке пассажиров невозможно избежать инцидентов: столкновений с автомобилями, пешеходами и велосипедистами, травм пассажиров и т. д. Все данные об этих происшествиях тщательно собираются городской администрацией.
Мне показалось, что эти данные очень интересно проанализировать, поискать в них корреляции. Я загрузил эти данные в свой Jupyter Notebook и нашел много занятных инсайтов.
Загрузим библиотеку Pandas и датасет.
import pandas as pd
from pandas import Series, DataFrame
df = pd.read_csv('tfl_bus_safety.csv', sep=';', encoding='cp1251')
df.head()
Рис. 1: Датасет с данными об инцидентами автобусов Лондона
Посмотрим сколько всего строк в таблице.
print('В датасете строк/колонок:', df.shape)
В датасете строк/колонок: (23158, 12)
Посмотрим параметры столбцов и их типы данных.
df.info()
<class ‘pandas.core.frame.DataFrame’>
RangeIndex: 23158 entries, 0 to 23157
Data columns (total 12 columns):
# Column Non-Null Count Dtype
— —— ————– —–
0 Year 23158 non-null int64
1 Date Of Incident 23158 non-null object
2 Route 23158 non-null object
3 Operator 23158 non-null object
4 Group Name 23158 non-null object
5 Bus Garage 23158 non-null object
6 Borough 23158 non-null object
7 Injury Result Description 23158 non-null object
8 Incident Event Type 23158 non-null object
9 Victim Category 23158 non-null object
10 Victims Sex 23158 non-null object
11 Victims Age 23158 non-null object
dtypes: int64(1), object(11)
memory usage: 2.1+ MB
Из информации выше делаем вывод что датасет состоит почти во всех столбцах из нечисловых данных. Это может быть проблемой, но мы решим ее позже.
Что касаемо качества заполнен датасет хорошо, пропусков нет. Однако названия столбцов требуют корректировки.
df.columns = ['year', 'date', 'route', 'operator',
'group_name', 'bus_garage', 'district',
'injury_result', 'incident_type',
'victim_category', 'victim_sex', 'victim_age']
df.head()
Рис. 2: Датасет с переименованными столбцами
Давайте посмотрим по каким маршрутам было больше всего инцидентов.
df['route'].value_counts().head(8)
OOS 321
18 191
55 177
24 165
73 156
96 155
149 154
207 153
Name: route, dtype: int64
И в процентном соотношении.
df['route'].value_counts(normalize=True).head(8)
OOS 0.013861
18 0.008248
55 0.007643
24 0.007125
73 0.006736
96 0.006693
149 0.006650
207 0.006607
Name: route, dtype: float64
Теперь проанализируем по каким автобусным операторам было больше всего инцидентов.
operators = DataFrame(df.groupby('group_name')['operator'].value_counts())
operators = operators.rename(columns={'operator': 'accidents'})
operators.loc[operators.accidents > 100]
Рис. 3: Статистика инцидентов по автобусным операторам Лондона
Также глянем в процентном соотношении.
df['operator'].value_counts(normalize=True).head(8)
Metroline 0.149279
Arriva London North 0.138527
East London 0.103722
London United 0.097720
Selkent 0.078072
Arriva London South 0.075266
London General 0.074402
London Central 0.059029
Name: operator, dtype: float64
Посмотрим каково среднее количество инцидентов на автобусного оператора.
df['operator'].apply(len).mean()
13.224976250107954
Теперь давайте посмотрим в каких районах Лондона больше всего нарушителей правил дорожного движения.
depos = DataFrame(df.groupby('district')['bus_garage'].value_counts())
depos = depos.rename(columns={'bus_garage': 'accidents'})
depos.loc[depos.accidents > 200]
Рис. 4: Статистика районов Лондона по числу нарушений ПДД с автобусами
Также глянем в процентном соотношении.
df['district'].value_counts(normalize=True).head(8)
Westminster 0.067838
Southwark 0.047802
Lambeth 0.047802
Croydon 0.044563
Lewisham 0.043441
Barnet 0.039813
Camden 0.038863
Brent 0.038302
Name: district, dtype: float64
Теперь проанализируем какие типы травм были у пассажиров и водителей чаще всего.
incid = DataFrame(df.groupby('incident_type')['injury_result'].value_counts())
incid = incid.rename(columns={'injury_result': 'accidents'})
incid.loc[incid.accidents > 200]
incid
Рис. 5: Таблица с типами травм и предоставленным лечением в результате аварий автобусов Лондона
Видим что самой частой травмой было Slip Trip Fall / Injuries treated on scene. То есть пассажиры подскользнулись и им сразу оказали помощь на месте.
Теперь посмотрим кто чаще всего становился “жертвой” автобусов?
df['victim_category'].value_counts().head(7)
Passenger 18828
Pedestrian 1612
Bus Driver 1484
3rd Party driver / Occupant 573
Cyclist 275
Member Of Public 127
Motorcyclist 102
Name: victim_category, dtype: int64
Чаще всего это были пассажиры автобусов, на 2-м месте – пешеходы, на 3-м – сам водитель.
А каков был пол пострадавших?
df[['victim_sex', 'victim_age']].value_counts().head()
victim_sex victim_age
Female Adult 6168
Male Adult 4427
Unknown Unknown 2891
Female Unknown 2757
Elderly 1879
dtype: int64
Видим что взрослые женщины чаще всего попадали в инциденты с автобусным транспортом. Меньше всего пострадавших среди пожилых людей.
Теперь давайте посчитаем сколько всего случилось инцидентов с автобусами в Лондоне.
operators['accidents'].sum()
23158
Посчитаем число инцидентов по годам.
df['year'].value_counts(dropna=False, ascending=True)
2018 4777
2015 5715
2016 6093
2017 6573
Name: year, dtype: int64
Давайте таже построим сводную таблицу по районам и годам чтобы понять где случалось наибольшее число инцидентов.
df.pivot_table(index='year', columns='district', values='date',
aggfunc='count', fill_value=0, margins=True)
Рис. 6: Сводная таблица по годам и районам Лондона, где происходили случаи нарушений ПДД с участием автобусов
Таблица получилась довольно шумной. Построим тепловую карту (хитмап).
import seaborn as sns
forheat = df.pivot_table(index='district', columns='year',
values='date', aggfunc='count', fill_value=0)
sns.heatmap(forheat, cmap="BuPu")
Рис. 7: Тепловая карта количества автобусных инцидентов по годам и районам Лондона
Видим что некоторые районы Лондона из года в год являются наиболее опасными в плане инцидентов. Это Westminster, Lewisham и другие.
Имеет смысл также проанализировать инциденты по автобусным компаниям.
df.pivot_table(index='year', columns='group_name', values='date',
aggfunc='count', fill_value=0, margins=True)
Рис. 8: Сводная таблица инцидентов по автобусным компаниям Лондона
Давайте тоже посмотрим на тепловую карту этих данных.
forheat = df.pivot_table(index='group_name', columns='year',
values='date', aggfunc='count', fill_value=0)
sns.heatmap(forheat, cmap="BuPu")
Рис. 9: Тепловая карта количества автобусных инцидентов по годам и автобусным компаниям
И здесь снова тепловая карта выглядит гораздо лучше для анализа. По этому хитмапу хорошо видны лидеры и аутсайдеры инцидентов. Чаще всего в ДТП попадали автобусы компаний Arriva London, Go Ahead, Metroline, Stagecoach. Примечательно что этот список стабилен по годам, то есть компании ничего не делают чтобы уменьшить количество аварий.
Можно также провести анализ по причинам инцидентов.
df.pivot_table(index='year', columns='incident_type', values='date',
aggfunc='count', fill_value=0, margins=True)
Рис. 10: Таблица с типами инцидентов по годам
И здесь уже можно углубиться в анализ. Например, посмотреть когда и в каких автобусных компаниях случались пожары.
df[df['incident_type'] == 'Fire']
Рис. 11: Статистика по пожарам в автобусах Лондона
Или ограбления пассажиров.
df[df['incident_type'] == 'Robbery']
Рис. 12: Статистика по ограблениям в автобусах Лондона
Можно также посмотреть на статистику пола пострадавших.
df.pivot_table(index='year', columns='victim_sex', values='date',
aggfunc='count', fill_value=0, margins=True)
Рис. 13: Таблица инцидентов в автобусах в разрезе пола пострадавших людей
Наконец мы можем визуализировать данные через графики.
df['incident_type'].value_counts(dropna=False).plot(kind='bar', title='Incident Types')
Рис. 14: Диаграмма количества автобусных инцидентов по их типам
df['victim_category'].value_counts(dropna=False).plot(kind='bar', title='Victim Category')
Рис. 15: Диаграмма по классам пострадавших людей
df['victim_age'].value_counts(dropna=False).plot(kind='bar', title='Victim Age')
Рис. 16: Диаграмма по возрасту пострадавших людей
Для более детального анализа мы можем присвоить числовые значения строковым. И взять за правило: чем больше цифра – тем серьезнее инцидент. Начнем со столбца injury_result.
df['injury_result'].unique()
array([‘Injuries treated on scene’,
‘Taken to Hospital – Reported Serious Injury or Severity Unknown’,
‘Reported Minor Injury – Treated at Hospital’, ‘Fatal’],
dtype=object)
Теперь создадим новый датафрейм df_new для подстановки чисел вместо строк.
df_new = df
values = {'Fatal': 10,
'Taken to Hospital – Reported Serious Injury or Severity Unknown': 7,
'Reported Minor Injury - Treated at Hospital': 5,
'Injuries treated on scene': 1}
df_new['injury_result'] = df_new['injury_result'].map(values)
df_new.head()
Рис. 17: Новая таблица с добавленным столбцом injury_result, обозначающим тяжесть травмы при автобусном ДТП
Что мы тут сделали? Мы присвоили оценку 10 всем инцидентам с фатальным исходом, 7 – для случаев, когда пассажир или пешеход были вынуждены лечиться в больнице с серьезными травмами, 5 – для случаев когда люди обратились в больницу с ушибами и незначительными травмами, 1 – когда последствия были незначительными и когда пассажиру или пешеходу была оказана помощь на месте.
df['incident_type'].unique()
array([‘Onboard Injuries’, ‘Collision Incident’, ‘Assault’,
‘Vandalism Hooliganism’, ‘Safety Critical Failure’,
‘Personal Injury’, ‘Slip Trip Fall’, ‘Activity Incident Event’,
‘Fire’, ‘Robbery’], dtype=object)
Поменяем таким же образом значения в других столбцах.
values = {'Fire': 10,
'Robbery': 9,
'Assault': 8,
'Collision Incident': 7,
'Vandalism Hooliganism': 6,
'Safety Critical Failure': 5,
'Onboard Injuries': 4,
'Activity Incident Event': 3,
'Slip Trip Fall': 2,
'Personal Injury': 1}
df_new['incident_type'] = df_new['incident_type'].map(values)
df['victim_category'].unique()
array([‘Passenger’, ‘Pedestrian’, ‘Conductor’, ‘Bus Driver’,
‘Member Of Public’, ‘Cyclist’, ‘Motorcyclist’,
‘3rd Party driver / Occupant’, ‘Other’, ‘Non-Operational Staff’,
‘Operational Staff’, ‘Contractor Staff’, ‘TfL Staff’,
‘Operations staff (other)’, ‘Cyclist ‘, ‘Motorcyclist ‘,
‘Insufficient Data’], dtype=object)
Поменяем таким же образом значения в других столбцах.
values = {'Pedestrian': 10,
'Cyclist': 9, 'Cyclist ': 9,
'Motorcyclist': 9, 'Motorcyclist ': 9,
'3rd Party driver / Occupant': 8,
'Passenger': 7,
'Bus Driver': 6,
'Conductor': 5,
'Member Of Public': 4,
'Operational Staff': 3, 'Operations staff (other)': 3,
'Non-Operational Staff': 2, 'TfL Staff': 2, 'Contractor Staff': 2,
'Other': 1, 'Insufficient Data': 1}
df_new['victim_category'] = df_new['victim_category'].map(values)
df['victim_age'].unique()
array([‘Child’, ‘Unknown’, ‘Elderly’, ‘Adult’, ‘Youth’], dtype=object)
values = {'Child': 10,
'Elderly': 8,
'Youth': 6,
'Adult': 4,
'Unknown': 2}
df_new['victim_age'] = df_new['victim_age'].map(values)
Теперь начнем искать корреляции. Но для начала оставим только важные для корреляционного анализа столбцы.
df_new[['year', 'date', 'route', 'operator', 'group_name', 'district',
'injury_result', 'incident_type', 'victim_category', 'victim_age']]
Рис. 18: Новая таблица со сгенерированными столбцами injury_result, incident_type, victim_category, victim_age, обозначающими степень тяжести инцидента по шкале от 0 до 10
Обратите внимание на значения в последних 4х столбцах: они изменились на числа. Однако даты нам по большому счету тоже теперь не нужны. Уберем даты (столбцы date и year) и заменим их на столбец c ID.
df_new['id'] = pd.Series(range(1, df_new.shape[0]+1))
df_new.id = df_new.id.astype(int)
df_new = df_new[['id', 'route', 'operator', 'group_name', 'district',
'injury_result', 'incident_type', 'victim_category', 'victim_age']]
df_new.head()
Рис. 19: Таблица с удаленными датами
В итоге у нас получилась готовая таблица для матанализа.
Что дальше? Разумеется, искать причинно-следственные закономерности! Мы не будем использовать здесь классические алгоритмы, типа коэффициентов Спирмена. Загрузим библиотеку Phik для поиска нелинейных корреляций. На мой взгляд, это одна из лучших библиотек Python для этой цели.
! pip install phik
import phik
from phik.report import plot_correlation_matrix
from phik import report
И посмотрим какие корреляции у нас получились.
phik_overview = df_new.phik_matrix()
phik_overview
Рис. 20: Таблица корреляций столбцов и строк методом Phik
Какие выводы мы можем сделать из этой таблицы корреляций? Их несколько. Напомню, что все что ближе к 1 – сильная корреляция, ближе к 0 – слабая.
На первый взгляд, сильных корреляций между столбцами и строками здесь нет. Хотя, если пробежаться по столбцам мы можем заметить:
- victim_age довольно тесно связан с operator (0.438);
- есть также заметная корреляция между incident_type и route (0.425);
- аналогично есть довольно много совпадений между victim_category и route (0.468).
Давайте также посмотрим тепловую карту этих значений.
sns.heatmap(phik_overview, cmap="BuPu")
Рис. 21: Тепловая карта (хитмап) нелинейных корреляций Phik
Тепловая карта также не дала новых инсайтов. Ну, что-ж. Давайте копать глубже в те инсайты которые уже есть.
1. victim_age довольно тесно связан с operator (0.438)
phik_overview['victim_age'].sort_values(ascending=False)
victim_age 1.000000
route 0.513006
operator 0.438542
victim_category 0.357949
group_name 0.354556
district 0.297448
incident_type 0.294422
id 0.134915
injury_result 0.120843
Name: victim_age, dtype: float64
Действительно ли здесь есть какая-то закономерность? Давайте посмотрим на сравнение средних и абсолютных показателей.
df_new.groupby('operator', dropna=False)['victim_age'].agg(['count','mean'])
Рис. 22: Таблица с числом автобусных инцидентов и средним возрастом пострадавших
Видим, что больше всего инцидентов с детьми и пожилыми людьми было у Metrobus, Abellio West, Arriva, Kent Thameside, H R Richmond, London General.
2. Есть заметная корреляция между incident_type и route (0.425)
Ранее мы уже анализировали самые частые в плане инцидентов маршруты. Давайте сейчас посмотрим на каких маршрутах чаще происходили самые опасные инциденты (пожар, ограбление, нападение итд).
df_new[df_new['incident_type'] >= 5].groupby(['route'])['id'].size().sort_values(ascending=False).head(8)
route
OOS 109
25 43
205 42
55 39
86 36
73 36
254 36
43 33
Name: id, dtype: int64
Видим, что самые опасные для жизни и здоровья людей маршруты это: OOS, 25, 205, 55, 86, 73, 254, 43. Причем первый маршрут выделяется с большим отрывом. Очевидно, что мэрии Лондона стоит обратить на это внимание.
3. Есть некоторая зависимость между victim_category и route (0.468)
Напомним, что значения victim_category определяют в порядке критичности тех, кто пострадал от инцидента с автобусом (пешеход, велосипедист, мотоциклист, пассажиры другого транпорта, пассажиры автобуса итд). Давайте посмотрим какие маршруты чаще всего приводили к таким инцидентам.
df_new[df_new['victim_category'] >= 6].groupby(['route'])['id'].size().sort_values(ascending=False).head(8)
route
OOS 275
18 191
55 176
24 157
73 155
96 155
149 154
207 153
Name: id, dtype: int64
Видим, что чаще всего к травмам посторонних людей приводили автобусы на маршрутах: OOS (повт.), 18, 55 (повт.), 24, 73 (повт.), 96, 149, 207.
Маршруты с пометкой “повт.” повторно попали в наш анти-рейтинг. Очевидно на них мэрии надо обратить пристальное внимание.
Интересно также посмотреть на самые опасные в плане автобусного движения районы Лондона.
df_new[df_new['incident_type'] >= 5].groupby(['district'])['id'].size().sort_values(ascending=False).head(8)
district
Westminster 360
Lambeth 248
Southwark 228
Lewisham 218
Barnet 212
Croydon 209
Camden 201
Greenwich 192
Name: id, dtype: int64
Видим, что это Westminster, Lambeth, Southwark.
Интересно также посмотреть статистику опасности районов с учетом тяжести травм. Мы отфильтруем и оставим только травмы с тяжестью 6 по 10-балльной шкале и больше.
df_new[df_new['victim_category'] >= 6].groupby(['district'])['id'].size().sort_values(ascending=False).head(8)
district
Westminster 1549
Lambeth 1099
Southwark 1093
Croydon 1027
Lewisham 987
Barnet 916
Camden 883
Brent 881
Name: id, dtype: int64
Вот так да! Королевский район Westminster является самым опасным районом Лондона в плане автобусного транспорта. Еще небезопасно переходить дороги или двигаться рядом с автобусами в районах Lambeth, Southwark, Croydon, Lewisham.
А что с автобусными компаниями? Есть ли среди них те, кто более опасен для пассажиров и других людей на дороге?
df_new[df_new['victim_category'] >= 6].groupby(['operator'])['id'].size().sort_values(ascending=False).head(8)
operator
Metroline 3409
Arriva London North 3205
East London 2386
London United 2227
Selkent 1801
Arriva London South 1743
London General 1691
London Central 1340
Name: id, dtype: int64
Здесь однозначные антилидеры в плане безопасности людей, это: Metroline, East London, Arriva London North.
Ну и, в заключение этого исследования, предлагаю построить графики по типам инцидентов с автобусами Лондона.
import matplotlib.pyplot as plt
sns.countplot('incident_type', data=df_new)
plt.title('Типы инцидентов')
plt.show()
Рис. 23: График частоты автобусных инцидентов по классам пострадавших
Видим что в целом автобусный транспорт Лондона безопасен. Хотя есть неприятно большое число инцидентов под номерами: 7 – пассажиры, 8 – водители и пассажиры другого транспорта.
Теперь давайте построим аналогичную визуализацию по типам травм пострадавших.
sns.countplot('injury_result', data=df_new)
plt.title('Типы травм')
plt.show()
Рис. 24: График частоты автобусных инцидентов по степени тяжести травм пострадавших
В основном, автобусные ДТП приводят к небольшим травмам, хотя мы также видим немалое число случаев с цифрами: 7 – Инцидент со столкновением, 5 – Критический сбой в системе безопасности.
Также давайте построим визуализацию по классам пострадавших.
sns.countplot('victim_category', data=df_new)
plt.title('Категории пострадавших')
plt.show()
Рис. 25: График категорий пострадавших людей в автобусных ДТП Лондона
Здесь ожидаемо самой частой категорией пострадавших людей являются пассажиры автобуса (номер 7). Однако мы также видим что также в инциденты попадают часто и другие люди: 10 – Велосипедисты, 9 – Мотоциклисты, 8 – Водители и пассажиры других автомобилей, 6 – и сам водитель(и) автобуса!
На этом наше исследование завершено. Благодарю за внимание!