Car Crash Prediction in NZ - Machine Learning Pipeline¶
1. Introduction¶
The dataset we use for this post is New Zealand Crash Analysis Dataset which is updated on a quarterly basis by the Transport Agency. The dataset was last updated on October 2018 (from January 2000). It contains all traffic crashes as reported to the Transport Agency by the NZ police. However, not all crashes are reported NZ police. A big portion of minor car crashes are settled on site by the parties without reporting to the police. The level of reporting increases with the severity of the crash. Due to the nature of non-fatal crashes it is believed that these are under-reported.
The dataset is available in many different formats and APIs. It is easy to grab them through API interfaces, instead of downloading to your local machine. In this way, we can have the newest version of the dataset every time we run it. If you want to download the dataset to your local machine, it can be found here. The crashes shown in the dataset are strongly related to geography, so we will use the geojson format. So, we can also perform geographic data analysis without creating geometries from latitude and longtitude and deal with coordinate reference systems and projections.
And we will also use different machine learning models to predict future car crashes based on the available data. Now, let's get into it.
This post is a self-learning project, and the original content can be found here
2. Exploratory Data Analysis (EDA)¶
Import Libraries
# Import libraries
import pandas as pd
import numpy as np
import geopandas as gpd
import matplotlib.pyplot as plt
import folium
import requests
import seaborn as sns
Import Data
Import the dataset through API. This process can take a while.
url = 'https://opendata.arcgis.com/datasets/a163c5addf2c4b7f9079f08751bd2e1a_0.geojson'
geojson = requests.get(url).json()
crs = {'init': 'epsg:3851'} #Coordinate reference system for New Zealand
gdf = gpd.GeoDataFrame.from_features(geojson['features'], crs = crs)
print(gdf.shape)
gdf.head()
gdf.columns
Important Crash Information¶
Crash information on death and injuries are very important to us. We will first explore the dataset on this part of information.
crashSeverity: the severity of a car crash. Possible values are 'F'-fatal, 'S'-serious, 'M'-minor, 'N'-non-injury. This is determined by the worst injury sustained in the crash at the time of entry.
seriousInjuryCount: the number of serious injuries associated with a crash.
minorInjuryCount: the number of minor injuries associated with a crash.
fatalCount: the number of fatal casualties associated with a crash.
start_year = gdf.crashYear.min()
end_year = gdf.crashYear.max()
fatal = gdf['fatalCount'].sum()
serious = gdf['seriousInjuryCount'].sum()
minor = gdf['minorInjuryCount'].sum()
print("The total death in car crash accident in New Zealand from {} to {} is {}.".format(start_year,
end_year, fatal))
print("While the total number of serious injuries and minor injuries in car crashes is {}, and {}, respectively.".format(
serious, minor))
import warnings
warnings.simplefilter('ignore')
fig, ax = plt.subplots(1, 3, figsize = (25, 5))
sns.barplot(x = 'crashYear', y = 'fatalCount', data = gdf, ax = ax[0], color = 'k')
ax[0].set_title("The number of total death from year 2000 to 2018", fontsize = 14)
sns.barplot(x = 'crashYear', y = 'seriousInjuryCount', data = gdf, ax = ax[1], color = 'r')
ax[1].set_title("The number of total serious injuries from year 2000 to 2018", fontsize = 14)
sns.barplot(x = 'crashYear', y = 'minorInjuryCount', data = gdf, ax = ax[2], color = 'g')
ax[2].set_title("The number of total minor injuries from year 2000 to 2018", fontsize = 14)
[ax[i].set_xlabel('Crash Year') for i in range(3)]
plt.tight_layout()
plt.show()
fig, ax = plt.subplots(1, 3, figsize = (12,4))
sns.countplot(x = 'fatalCount', data = gdf, ax = ax[0], color = 'k')
sns.countplot(x = 'seriousInjuryCount', data = gdf, ax = ax[1], color = 'r')
sns.countplot(x = 'minorInjuryCount', data = gdf, ax = ax[2], color = 'g')
plt.tight_layout()
plt.show()
fig, ax = plt.subplots(figsize = (10, 4))
sns.countplot(y="crashSeverity",data=gdf, order='NMSF',ax = ax) # Non-injury, Minor, Seriours, Fatal
plt.show()
fig, ax = plt.subplots(1, 3, figsize = (25, 4))
sns.lineplot(x = 'crashYear', y = 'fatalCount', data = gdf, ax = ax[0])
ax[0].set_title("The number of total death from year 2000 to 2018", fontsize = 14)
sns.lineplot(x = 'crashYear', y = 'seriousInjuryCount', data = gdf, ax = ax[1])
ax[1].set_title("The number of total serious injuries from year 2000 to 2018", fontsize = 14)
sns.lineplot(x = 'crashYear', y = 'minorInjuryCount', data = gdf, ax = ax[2])
ax[2].set_title("The number of total minor injuries from year 2000 to 2018", fontsize = 14)
[ax[i].set_xlabel('Crash Year') for i in range(3)]
plt.tight_layout()
plt.show()
Road Information¶
These variables, including roadworks, junctionType, roadCharacter, roadCurvature, roadLane, roadMarkings, roadSurface, roadWet, numberOfLanes, intersection, intersectionMidblock, flatHill, darkLight, provide information about the road condition when an accident occurs.
Let's plot some of them out to see their patterns.
fig, ax = plt.subplots(3, 3, figsize = (20, 10))
sns.countplot(x='roadCurvature',data=gdf, ax=ax[0,0])
sns.countplot(x="junctionType",data=gdf, ax=ax[0,1])
sns.countplot(x='roadLane',data=gdf, ax=ax[0,2])
sns.countplot(x='numberOfLanes',data=gdf, ax=ax[1,0])
sns.countplot(x='roadWet',data=gdf, ax=ax[1,1])
sns.countplot(x='roadMarkings',data=gdf, ax=ax[1,2])
sns.countplot(x='roadSurface',data=gdf, ax=ax[2,0])
sns.countplot(x='darkLight',data=gdf, ax=ax[2,1])
sns.countplot(x='intersection',data=gdf, ax=ax[2,2])
#intersectionMidblock
plt.tight_layout()
NumberOfLanes influence on fatal and serious injuries.
plt.figure(figsize = (12,5))
sns.barplot(x = 'numberOfLanes', y = 'fatalCount', data = gdf, palette = 'Reds_d')
plt.figure(figsize = (12, 5))
sns.barplot(x = 'numberOfLanes', y = 'seriousInjuryCount', data = gdf, palette = 'Blues_d')
plt.show()
JunctionType influence on fatal and serious injuries.
fig, ax = plt.subplots(1, 2, figsize = (12, 4))
sns.barplot(x= 'junctionType', y= 'fatalCount', data = gdf, palette = 'Reds_d', ax = ax[0])
sns.barplot(x= 'junctionType', y= 'seriousInjuryCount', data = gdf, palette = 'Blues_d', ax = ax[1])
plt.tight_layout()
plt.show()
RoadWet influence on fatal and serious injuries.
fig, ax = plt.subplots(1, 2, figsize = (8, 4))
sns.barplot(x= 'roadWet', y= 'fatalCount', data = gdf, palette = 'Reds_d', ax = ax[0])
sns.barplot(x= 'roadWet', y= 'seriousInjuryCount', data = gdf, palette = 'Blues_d', ax = ax[1])
plt.tight_layout()
plt.show()
DarkLight influence. Dark Light condition also has a significant influence on fatal and serious injury accident, observed from the figure shown below.
fig, ax = plt.subplots(1, 2, figsize = (8, 4))
sns.barplot(x= 'darkLight', y= 'fatalCount', data = gdf, palette = 'Reds_d', ax = ax[0])
sns.barplot(x= 'darkLight', y= 'seriousInjuryCount', data = gdf, palette = 'Blues_d', ax = ax[1])
plt.tight_layout()
plt.show()
RoadSurface influence. From the figure shown below, it can be easily observed that unsealed road surface has a much higher fatal or serious injury rate than sealed roads.
fig, ax = plt.subplots(1, 2, figsize = (8, 4))
sns.barplot(x= 'roadSurface', y= 'fatalCount', data = gdf, palette = 'Reds_d', ax = ax[0])
sns.barplot(x= 'roadSurface', y= 'seriousInjuryCount', data = gdf, palette = 'Blues_d', ax = ax[1])
plt.tight_layout()
plt.show()
Traffic Conditions¶
SpeedLimit with fatal and serious injuries.
fig, ax = plt.subplots(1, 2, figsize = (20, 4))
sns.barplot(x= 'speedLimit', y= 'fatalCount', data = gdf, palette = 'Reds_d', ax = ax[0])
sns.barplot(x= 'speedLimit', y= 'seriousInjuryCount', data = gdf, palette = 'Blues_d', ax = ax[1])
plt.tight_layout()
plt.show()
StreetLight with fatal and serious injuries.
fig, ax = plt.subplots(1, 2, figsize = (20, 4))
sns.barplot(x= 'streetLight', y= 'fatalCount', data = gdf, palette = 'Reds_d', ax = ax[0])
sns.barplot(x= 'streetLight', y= 'seriousInjuryCount', data = gdf, palette = 'Blues_d', ax = ax[1])
plt.tight_layout()
plt.show()
AdvisorySpeed with fatal and serious injuries.
sns.catplot(x="advisorySpeed", y="crashSeverity",data=gdf, kind= 'bar');
plt.show()
Weather Conditions¶
Total Weather Counts, weatherA and weatherB. And their influence on fatal and serious injuries.
fig, ax = plt.subplots(1, 2, figsize=(15, 5))
sns.countplot(x="weatherA",data=gdf, ax = ax[0], palette="Greens_d");
sns.countplot(x="weatherB",data=gdf, ax = ax[1], palette="Greens_d");
plt.tight_layout()
plt.show()
fig, ax = plt.subplots(1, 2, figsize=(15, 5))
sns.barplot(x="weatherA", y = 'fatalCount', data=gdf, ax = ax[0], palette="Reds_d");
sns.barplot(x="weatherB",y = 'seriousInjuryCount', data=gdf, ax = ax[1], palette="Blues_d");
plt.tight_layout()
plt.show()
Geographic Data Exploration¶
gdf.plot(markersize = 0.01, edgecolor = 'r', figsize = (12,12))
plt.axis('off')
from folium.plugins import MarkerCluster
gdf_sample = gdf.sample(5000)
lons = gdf_sample.geometry.x
lats = gdf_sample.geometry.y
m = folium.Map(location = [np.mean(lats), np.mean(lons)],
zoom_start =5.7)
#FastMarkerCluster(data=list(zip(lats, lons))).add_to(m)
MarkerCluster(list(zip(lats, lons))).add_to(m)
folium.LayerControl().add_to(m)
m
gdf_sample = gdf.sample(5000)
lons = gdf_sample.geometry.x
lats = gdf_sample.geometry.y
heat_cols = list(zip(lats, lons))
from folium.plugins import HeatMap
m = folium.Map([np.mean(lats), np.mean(lons)],
tiles='CartoDB dark_matter',
zoom_start=7)
HeatMap(heat_cols).add_to(m)
m
Data Modelling¶
We can approach the machine learning part in different ways. We can consider it as a regression problem and predict the number of death or serious injuries. We can also take it as a classification problem and classify the severity of a certain car crash. In this post, we will first approach it as a regression problem. We will try to predict the the fatalCount based on all other variables. Random Forest will be used the the modelling algorithm. It needs only minimal data preprocessing and does well in many cases.
# import libraries
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
# first tranfer our data from geoDataFrame to pandas DataFrame, so that we can use sklearn to work on it.
df = pd.DataFrame(gdf.drop(['geometry', 'OBJECTID'], axis = 1))
df.head()
gdf.head()
Regression Modeling with Random forest¶
We first need to convert categorical variables into numerical variables, using LabelEncoder and OneHotEncoder.
from sklearn.preprocessing import LabelEncoder
LE = LabelEncoder()
for i in df:
if df[i].dtype =='object':
LE.fit(df[i])
df[i] = LE.transform(df[i])
df = pd.get_dummies(df)
df.head()
# split data into training and validation sets
X_train, X_test, y_train, y_test = train_test_split(df.drop('fatalCount', axis = 1), df.fatalCount,
test_size = 0.2, random_state = 42)
print((X_train.shape, X_test.shape, y_train.shape, y_test.shape))
def rmse(x,y):
return np.sqrt(((x-y)**2).mean())
def print_score(m):
res =[rmse(m.predict(X_train), y_train),
rmse(m.predict(X_test), y_test),
m.score(X_train,y_train),
m.score(X_test, y_test)]
if hasattr(m, 'oob_score_'):
res.append(m.oob_score_)
print(res)
# Initialize the regressor
m = RandomForestRegressor(n_estimators = 50)
m.fit(X_train, Y_train)
print_score(m)
Feature Importance
f_imp = pd.DataFrame(data={'importance':m.feature_importances_,'features':X_train.columns}).set_index('features')
f_imp = f_imp.sort_values('importance', ascending = False)
f_imp.head(10)
Clearly, crashSeverity is the most important in our model, because it is derived from the number of deaths.
f_imp[:20].plot(kind='bar', figsize=(12,10))
plt.show()
Select the variables with importance.
f_imp_sel = f_imp[f_imp['importance'] > 0.0001]
len(f_imp_sel.index), len(f_imp.index)
We have 73 out of 85 variables with importance value over 0.0001.
y = df.fatalCount
df_imp = df[f_imp_sel.index]
df_imp.shape
Retrain the RandomForestRegressor with only the variables with significance.
# Let us split our data into training and validation sets
X_train, X_test, y_train, y_test = train_test_split(df_imp, y, test_size=0.33, random_state=42)
m = RandomForestRegressor(n_estimators=50, oob_score=True)
m.fit(X_train, y_train)
print_score(m)
Adjusting the parameters of the RF regressor to see if we can get better results. Of course, you can also use a Grid Search Cross-validation to find more suitable parameters. Here, to make it easy, I will not go through this procedure. Because it may take a long time.
m = RandomForestRegressor(n_estimators=100, min_samples_leaf=10, oob_score=True)
m.fit(X_train, y_train)
print_score(m)
Comments
comments powered by Disqus