Downtown Denver

Introduction

Denver est la ville la plus peuplée du Colorado, un état qui a été récemment classé comme le 21ème plus dangereux des Etats-Unis. C'est aussi l'un des premiers états à avoir légalisé la Marijuana à usage récréationnel en 2014.

D'après les statistiques du FBI, Denver présente un taux de criminalité au-dessus de la moyenne en matière de crimes contre la propriété par exemple les incendies volontaires, les cambriolages, vols, viols etc. Au contraire, lorsque l'on considère les meurtres et braquages le taux est en dessous de la moyenne. Nous allons donc nous intéresser à la problématique suivante :

Problématique

Quels sont les facteurs socio-économiques par quartier pouvant expliquer un taux supérieur des crimes contre la propriété par rapport à la moyenne nationale ?

Présentation des données

Données sur les types de délits en fonction de la localisation et de la date

Le gouvernement de Denver met à disposition les données concernant les différents crimes recensés dans la ville durant les 5 dernières années. Les données sont issues du système national de déclaration des incidents (NIBRS) et sont ainsi très complètes, incluant tout type de crimes tels que les vols, les viols, les meurtres ou encore le trafic de stupéfiants par exemple. On dispose également des dates précises ainsi que des coordonnées géographiques associées à chaque incident reporté. Ces informations sont précieuses pour étudier l'évolution du taux de criminalité au fil des années, ainsi que pour identifier les quartiers à risques.

Les données disponibles sur le site concernent les délits entre 2015 et 2020, mais on a également pu récupérer un fichier similaire pour les années 2012 à 2017. On va donc mettre en commun les deux fichiers de données.

Source : https://www.denvergov.org/opendata/dataset/city-and-county-of-denver-crime

In [8]:
from IPython.display import HTML

HTML('''<script>
code_show=true; 
function code_toggle() {
 if (code_show){
 $('div.input').hide();
 } else {
 $('div.input').show();
 }
 code_show = !code_show
} 
$( document ).ready(code_toggle);
</script>
<form action="javascript:code_toggle()"><input type="submit" value="Montrer/cacher le code."></form>''')
Out[8]:
In [9]:
import warnings
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import kpss

from folium import plugins
from folium.plugins import HeatMap
import folium
from IPython.display import IFrame

from sklearn.decomposition import PCA
from sklearn import preprocessing

from plotly.offline import init_notebook_mode; init_notebook_mode()

warnings.simplefilter('ignore')
In [10]:
data_dir = "data/"

df_old = pd.read_csv(data_dir + 'crime_2012_2017.csv') 
df_new = pd.read_csv(data_dir + 'crime_2015_2020.csv')
In [11]:
rows_before2015 = np.where(pd.to_datetime(df_old.FIRST_OCCURRENCE_DATE).dt.year <= 2014)[0]
df_old_before2015 = df_old.loc[rows_before2015, :]

df_old_before2015.index = range(df_new.shape[0], 
                                df_new.shape[0] + df_old_before2015.shape[0])

df = pd.concat([df_new, df_old_before2015])
In [12]:
print(df.shape)
df.head()
(675052, 19)
Out[12]:
INCIDENT_ID OFFENSE_ID OFFENSE_CODE OFFENSE_CODE_EXTENSION OFFENSE_TYPE_ID OFFENSE_CATEGORY_ID FIRST_OCCURRENCE_DATE LAST_OCCURRENCE_DATE REPORTED_DATE INCIDENT_ADDRESS GEO_X GEO_Y GEO_LON GEO_LAT DISTRICT_ID PRECINCT_ID NEIGHBORHOOD_ID IS_CRIME IS_TRAFFIC
0 2016376978 2016376978521300 5213 0 weapon-unlawful-discharge-of all-other-crimes 6/15/2016 11:31:00 PM NaN 6/15/2016 11:31:00 PM NaN 3193983.0 1707251.0 -104.809881 39.773188 5 521 montbello 1 0
1 20186000994 20186000994239900 2399 0 theft-other larceny 10/11/2017 12:30:00 PM 10/11/2017 4:55:00 PM 1/29/2018 5:53:00 PM NaN 3201943.0 1711852.0 -104.781434 39.785649 5 522 gateway-green-valley-ranch 1 0
2 20166003953 20166003953230500 2305 0 theft-items-from-vehicle theft-from-motor-vehicle 3/4/2016 8:00:00 PM 4/25/2016 8:00:00 AM 4/26/2016 9:02:00 PM 2932 S JOSEPHINE ST 3152762.0 1667011.0 -104.957381 39.663490 3 314 wellshire 1 0
3 201872333 201872333239900 2399 0 theft-other larceny 1/30/2018 7:20:00 PM NaN 1/30/2018 10:29:00 PM 705 S COLORADO BLVD 3157162.0 1681320.0 -104.941440 39.702698 3 312 belcaro 1 0
4 2017411405 2017411405230300 2303 0 theft-shoplift larceny 6/22/2017 8:53:00 PM NaN 6/23/2017 4:09:00 PM 2810 E 1ST AVE 3153211.0 1686545.0 -104.955370 39.717107 3 311 cherry-creek 1 0

On dispose de 19 variables dont :

  • 6 décrivant la nature du délit,
  • 8 liées à la localisation de l'incident,
  • 3 concernant la date de l'incident,
  • (et 2 variables de type ID),


et pas moins de 675052 incidents reportés pour les année 2012 à 2020.

On a effectué un prétraitement des données en rajoutant des colonnes de type temporel afin de nous aider dans la suite de notre étude.

In [13]:
df.FIRST_OCCURRENCE_DATE = pd.to_datetime(df.FIRST_OCCURRENCE_DATE)

df["YEAR"] = df.FIRST_OCCURRENCE_DATE.dt.year
df["DAY"] = df.FIRST_OCCURRENCE_DATE.dt.day
df["DAY_OF_WEEK"] = df.FIRST_OCCURRENCE_DATE.dt.dayofweek
df["MONTH"] = df.FIRST_OCCURRENCE_DATE.dt.month
df["HOUR"] = df.FIRST_OCCURRENCE_DATE.dt.hour

df.index = pd.DatetimeIndex(df["FIRST_OCCURRENCE_DATE"])

df[["YEAR", "DAY", "DAY_OF_WEEK", "MONTH", "HOUR"]].head()
Out[13]:
YEAR DAY DAY_OF_WEEK MONTH HOUR
FIRST_OCCURRENCE_DATE
2016-06-15 23:31:00 2016 15 2 6 23
2017-10-11 12:30:00 2017 11 2 10 12
2016-03-04 20:00:00 2016 4 4 3 20
2018-01-30 19:20:00 2018 30 1 1 19
2017-06-22 20:53:00 2017 22 3 6 20

Nous allons séparer les types de délits en 3 classes distinctes :

  • les crimes violents regroupant les meurtres, braquages et agressions aggravées,

  • les crimes contre la propriété regroupant les incendies volontaires, larcins, cambriolages, vols de voitures ainsi que les vols dans un véhicule à moteur,

  • les autres crimes constitués de tous les autres crimes, drogue et alcool, désordre publique, la criminalité en col blanc ainsi que les autres crimes contre les personnes.

In [14]:
df["Violent Crime"] = (df["OFFENSE_CATEGORY_ID"]
                       =="murder") | (df["OFFENSE_CATEGORY_ID"]
                                      =="robbery") | (df["OFFENSE_CATEGORY_ID"]
                                                      =="aggravated-assault") 
df["Violent Crime"] = df["Violent Crime"].astype("int32") 


df["Property Crime"] = (df["OFFENSE_CATEGORY_ID"]=="arson") | (df["OFFENSE_CATEGORY_ID"]
                     =="larceny") | (df["OFFENSE_CATEGORY_ID"]
                     =="burglary") | (df["OFFENSE_CATEGORY_ID"]
                     =="auto-theft") | (df["OFFENSE_CATEGORY_ID"]
                     =="theft-from-motor-vehicle")
df["Property Crime"] = df["Property Crime"].astype("int32") 


df["Other Crimes"] = (df["OFFENSE_CATEGORY_ID"]=="all-other-crimes") | (df["OFFENSE_CATEGORY_ID"]
                     =="drug-alcohol") | (df["OFFENSE_CATEGORY_ID"]
                     =="public-diorder") | (df["OFFENSE_CATEGORY_ID"]
                     =="white-collar-crime") | (df["OFFENSE_CATEGORY_ID"]
                     =="other-crimes-against-persons")
df["Other Crimes"] = df["Other Crimes"].astype("int32") 

Données sur les caractéristiques socio-économiques des habitants par quartier à Denver

Afin de répondre à la problématique, on va croiser ce jeu de données sur les délits avec un nouveau jeu de données qui décrit le type de population en fonction du quartier d'habitation à Denver. Ce jeu de données comme le précédent est fourni par la ville de Denver (source : https://www.denvergov.org/opendata/dataset/city-and-county-of-denver-american-community-survey-nbrhd-2013-2017).

On a notamment des variables concernant la race, les revenus ou encore le niveau d'études des habitants, les chiffres sont une estimation moyenne sur les années 2013 à 2017.

Les définitions des différentes variables peuvent être lues sur le site https://koordinates.com/layer/101867-denver-colorado-american-community-survey-neigborhood-2010-2014/metadata/?type=fgdc.

In [15]:
df_survey = pd.read_csv(data_dir + 'american_community_survey_nbrhd_2013_2017.csv') 
df_survey.head()
Out[15]:
NBHD_NAME TTL_POPULATION_ALL HISPANIC_OR_LATINO WHITE BLACK NATIVE_AMERICAN ASIAN HAWAIIAN_PI OTHER_RACE TWO_OR_MORE ... WESTERN_AFRICA_FB OCEANIA_FB AMERICAS_FB LATIN_AMERICA_FB CARRIBEAN_FB CENTRAL_AMERICA_FB SOUTH_AMERICA_FB NORTH_AMERICA_FB PCT_POVERTY PCT_FAM_POVERTY
0 Elyria Swansea 6687.0 5389.0 960.0 259.0 25.0 15.0 0.0 0.0 39.0 ... 0 0 2329 2329 39 2281 9 0 20.30 19.549419
1 Wellshire 3529.0 371.0 2932.0 26.0 0.0 81.0 0.0 0.0 119.0 ... 0 0 84 84 4 71 9 0 3.80 2.034884
2 University 9576.0 720.0 7672.0 261.0 5.0 677.0 0.0 0.0 241.0 ... 38 0 178 144 19 64 61 34 17.65 4.923088
3 Rosedale 2498.0 254.0 2101.0 35.0 10.0 92.0 0.0 0.0 6.0 ... 0 0 65 59 11 27 21 6 8.70 1.934236
4 Cheesman Park 8895.0 747.0 7269.0 342.0 45.0 225.0 28.0 1.0 238.0 ... 0 0 162 104 41 38 25 58 10.30 4.369919

5 rows × 147 columns

L'idée est d'étudier le lien éventuel entre le nombre de délits et les variables socio-économiques dont on dispose. Ainsi on va ajouter à cette table les colonnes représentant le nombre de crimes par quartier.

Puisque l'on va étudier la dangerosité des quartiers, le nombre de délits n'est pas suffisant, il faut aussi prendre en compte le nombre d'habitants dans chaque quartier. Ainsi on va définir une variable crimes_per_pop qui est le ratio $\frac{nb\_crimes\_sur\_la\_propriété}{nb\_habitants}$.

In [16]:
old_names = set(df_survey.NBHD_NAME)
right_names = set(df.NEIGHBORHOOD_ID)

replace_names = dict(zip(sorted(old_names), sorted(right_names)))

df_survey.NBHD_NAME = df_survey.NBHD_NAME.replace(replace_names)
In [17]:
df_survey.index = df_survey.NBHD_NAME

# tous les crimes
df_crimes = df[df.IS_CRIME == 1]

df_survey["nb_all"] = df_crimes.NEIGHBORHOOD_ID.value_counts()

# type de délits
for crime_type in set(df_crimes.OFFENSE_CATEGORY_ID):
    
    df_crime_type = df_crimes[df_crimes.OFFENSE_CATEGORY_ID == crime_type]
    
    df_survey["nb_" + str(crime_type)] = df_crime_type.NEIGHBORHOOD_ID.value_counts()
    
df_survey = df_survey.fillna(0)
In [18]:
df_survey["nb_violent_crimes"] = df_survey["nb_murder"] + df_survey["nb_robbery"] + df_survey["nb_aggravated-assault"]

df_survey["nb_property_crimes"] = df_survey["nb_larceny"] + df_survey["nb_burglary"] + df_survey["nb_auto-theft"] + df_survey["nb_theft-from-motor-vehicle"]

df_survey["nb_other_crimes"] = df_survey["nb_all-other-crimes"] + df_survey["nb_drug-alcohol"] + df_survey["nb_public-disorder"] + df_survey["nb_white-collar-crime"] + df_survey["nb_other-crimes-against-persons"]

df_survey["crimes_per_pop"] = df_survey["nb_all"]/df_survey["TTL_POPULATION_ALL"]
In [19]:
df_survey.iloc[:5, -18:]
Out[19]:
nb_sexual-assault nb_larceny nb_arson nb_theft-from-motor-vehicle nb_white-collar-crime nb_drug-alcohol nb_other-crimes-against-persons nb_public-disorder nb_aggravated-assault nb_burglary nb_auto-theft nb_robbery nb_all-other-crimes nb_murder nb_violent_crimes nb_property_crimes nb_other_crimes crimes_per_pop
NBHD_NAME
elyria-swansea 58 642 26.0 615 114 345 368 946 262 590 743 92 1553 9.0 363.0 2590 3326 0.951548
wellshire 3 77 0.0 161 21 20 17 102 7 110 47 3 37 1.0 11.0 395 197 0.171720
university 57 710 5.0 679 96 129 173 618 70 441 315 66 456 2.0 138.0 2145 1472 0.398601
rosedale 25 188 1.0 270 40 113 101 227 26 229 138 26 213 1.0 53.0 825 694 0.639712
cheesman-park 86 997 11.0 659 70 1706 412 1004 175 421 435 152 1405 2.0 329.0 2512 4597 0.847105

Analyse de la criminalité à Denver

Nous allons dans un premier temps analyser les délits en fonction des quartiers de Denver.

Analyse temporelle des délits

Nous allons commencer par regarder quels types de délits sont les plus courants à Denver et comment ils sont répartis dans le temps, en fonction des mois, des jours de la semaine et des heures.

Répartition et tendance des types de délits

In [20]:
cat_freq = df.OFFENSE_TYPE_ID.value_counts()

cat_freq = pd.DataFrame({"count": cat_freq.values, "category": cat_freq.index})

fig = px.bar(cat_freq.iloc[:20, :][::-1], y = "category", x = "count",
             orientation = "h", color = "count",
             title = "Les 20 types de délits les plus courants à Denver",
             template = "plotly_white" ,
             color_continuous_scale = px.colors.sequential.Teal)

fig.update(layout=dict(title=dict(x=0.5)))

fig.show()
In [21]:
fig = px.bar(cat_freq.iloc[-20:, :][::-1], y = "category", x = "count",
             orientation = "h", color = "count",
             title = "Les 20 types de délits les moins courants à Denver",
             template = "plotly_white" ,
             color_continuous_scale = px.colors.sequential.Teal)

fig.update(layout=dict(title=dict(x=0.5)))

fig.show()

Concernant la fréquence des types de délits à Denver, on peut remarquer que certains se comptent par dizaine de milliers en seulement 8 ans comme notamment les délits de la route, lorsque d'autres ne se produisent qu'une fois ou deux dans la même période. On a donc un écart d'échelle assez important entre les délits les plus courants et les moins courants.

In [22]:
df.index = df.index.rename("FIRST_OCC_DATE")

test = df.pivot_table(index='FIRST_OCCURRENCE_DATE', columns='OFFENSE_CATEGORY_ID',
               aggfunc='size', fill_value=0).resample('M').sum().rolling(window=12).mean().dropna()

fig = make_subplots(rows=5, cols=3,
                   subplot_titles = test.columns)

i = 0
for row in range(5):
    for col in range(3):
        
        fig_tmp = go.Scatter(x = test.iloc[:, i].index, y = test.iloc[:, i].values)
        fig.add_trace(fig_tmp, row = row+1, col = col+1)
        
        i += 1


fig.update_layout(showlegend = False)
fig.show()

On constate que la tendance varie beaucoup en fonction du type de délit considéré, ainsi par exemple les assauts aggravés ont une tendance croissante ces dernières années, alors que les cambriolages sont en forte baisse.

Répartition de la criminalité par mois selon les 3 classes de délits

In [23]:
months = ['Jan','Fev','Mar','Avr','Mai','Juin','Juil','Août','Sep','Oct','Nov','Dec']

violent_crimes_df = df[df["Violent Crime"] == 1]
property_crimes_df = df[df["Property Crime"] == 1]
other_crimes_df = df[df["Other Crimes"] == 1]


month_freq_violent_crimes = violent_crimes_df.MONTH.value_counts()
month_freq_violent_crimes = pd.DataFrame({"count": month_freq_violent_crimes.values, 
                                  "mois": month_freq_violent_crimes.index})


month_freq_property = property_crimes_df.MONTH.value_counts()
month_freq_property = pd.DataFrame(pd.DataFrame({"count": month_freq_property.values, 
                                  "mois": month_freq_property.index}))

month_freq_other = other_crimes_df.MONTH.value_counts()
month_freq_other = pd.DataFrame(pd.DataFrame({"count": month_freq_other.values, 
                                  "mois": month_freq_other.index}))


fig = make_subplots(rows=1, cols=3,
                   subplot_titles = ["Crimes violents", "Crimes sur la propriété", "Autres crimes"])

fig1 =  px.bar(month_freq_violent_crimes, x = "mois", y = "count")

fig2 =  px.bar(month_freq_property, x = "mois", y = "count")

fig3 = px.bar(month_freq_other, x = "mois", y = "count")

fig.add_trace(fig1["data"][0], row = 1, col = 1)
fig.add_trace(fig2["data"][0], row = 1, col = 2)
fig.add_trace(fig3["data"][0], row = 1, col = 3)

dic_xaxis = dict(
        tickmode = 'array',
        tickvals = list(range(1, 13)),
        ticktext = months,
        title = "Mois"
    )

fig.update_layout(
    xaxis1 = dic_xaxis,
    xaxis2 = dic_xaxis,
    xaxis3 = dic_xaxis,
    yaxis_title = "Nombre de délits"
)

fig.show()

En ce qui concerne la répartition des délits par mois, on peut constater un nombre moins important de délits au mois de février pour chaque classe, ainsi qu'un pic en janvier et l'été en juillet-août.

On note quand même que l'échelle est différente pour le graphe des crimes violents, on ne peut pas conclure sur cette observation car cette classe ne regroupe que 3 types de délits alors que les deux autres en regroupent 5 chacune. On retrouvera cette différence d'échelle sur l'ensemble des graphes du fait que le nombre de crimes violents dans le jeu de données soit plus petit que celui des autres classes.

Répartition de la criminalité par jour de la semaine selon les 3 classes de délits

In [24]:
weekdays = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]

day_freq_violent_crimes = violent_crimes_df["DAY_OF_WEEK"].value_counts()
day_freq_violent_crimes = pd.DataFrame({"count": day_freq_violent_crimes.values, 
                                  "jour": day_freq_violent_crimes.index})


day_freq_property = property_crimes_df["DAY_OF_WEEK"].value_counts()
day_freq_property = pd.DataFrame(pd.DataFrame({"count": day_freq_property.values, 
                                  "jour": day_freq_property.index}))

day_freq_other = other_crimes_df["DAY_OF_WEEK"].value_counts()
day_freq_other = pd.DataFrame(pd.DataFrame({"count": day_freq_other.values, 
                                  "jour": day_freq_other.index}))


fig = make_subplots(rows=1, cols=3,
                   subplot_titles = ["Crimes violents", "Crimes sur la propriété", "Autres crimes"])

fig1 =  px.bar(day_freq_violent_crimes, x = "jour", y = "count")

fig2 =  px.bar(day_freq_property, x = "jour", y = "count")

fig3 = px.bar(day_freq_other, x = "jour", y = "count")

fig.add_trace(fig1["data"][0], row = 1, col = 1)
fig.add_trace(fig2["data"][0], row = 1, col = 2)
fig.add_trace(fig3["data"][0], row = 1, col = 3)

dic_xaxis = dict(
        tickmode = 'array',
        tickvals = list(range(0, 8)),
        ticktext = weekdays,
        title = "Jour"
    )

fig.update_layout(
    xaxis1 = dic_xaxis,
    xaxis2 = dic_xaxis,
    xaxis3 = dic_xaxis,
    yaxis_title = "Nombre de délits"
)

fig.show()

On peut constater qu'il y a moins de délits des classes crime sur la propriété ou autres crimes le week-end qu'en semaine. A l'inverse, les crimes violents semblent être plus fréquents le week-end qu'en semaine.

Répartition de la criminalité par heure selon les 3 classes de délits

In [25]:
hour_freq_violent_crimes = violent_crimes_df["HOUR"].value_counts()
hour_freq_violent_crimes = pd.DataFrame({"count": hour_freq_violent_crimes.values, 
                                  "heure": hour_freq_violent_crimes.index})


hour_freq_property = property_crimes_df["HOUR"].value_counts()
hour_freq_property = pd.DataFrame(pd.DataFrame({"count": hour_freq_property.values, 
                                  "heure": hour_freq_property.index}))

hour_freq_other = other_crimes_df["HOUR"].value_counts()
hour_freq_other = pd.DataFrame(pd.DataFrame({"count": hour_freq_other.values, 
                                  "heure": hour_freq_other.index}))


fig = make_subplots(rows=1, cols=3,
                   subplot_titles = ["Crimes violents", "Crimes sur la propriété", "Autres crimes"])

fig1 =  px.bar(hour_freq_violent_crimes, x = "heure", y = "count")

fig2 =  px.bar(hour_freq_property, x = "heure", y = "count")

fig3 = px.bar(hour_freq_other, x = "heure", y = "count")

fig.add_trace(fig1["data"][0], row = 1, col = 1)
fig.add_trace(fig2["data"][0], row = 1, col = 2)
fig.add_trace(fig3["data"][0], row = 1, col = 3)

dic_xaxis = dict(
        tickmode = 'array',
        tickvals = list(range(24)),
        tickangle = 0,
        tickfont=dict(size=8),
        title = "Heure"
    )

fig.update_layout(
    xaxis1 = dic_xaxis,
    xaxis2 = dic_xaxis,
    xaxis3 = dic_xaxis,
    yaxis_title = "Nombre de délits"
)

fig.show()

En règle général, on peut dire qu'il y a une baisse significative des délits à l'aube. Les crimes violents se passent en majorité entre 21h et 2h. Pour les deux autres classes, il semble y avoir des pics à plusieurs heures par exemple à 8h, 12h et 17h pour les crimes sur la propriété.

Série chronologique

On représente ci-dessous le nombre de délits par jour, on va ensuite analyser cette série chronologique : tendance, décomposition et test de stationnarité.

Tendance globale des délits à Denver

In [26]:
# écart moyen et standard des délits par jour
crimes_per_day = pd.DataFrame(df.resample('D').size())
crimes_per_day["MEAN"] = df.resample('D').size().mean()
crimes_per_day["STD"] = df.resample('D').size().std()

# limite de contrôle supérieure et limite de contrôle inférieure
UCL = crimes_per_day['MEAN'] + 3 * crimes_per_day['STD']
LCL = crimes_per_day['MEAN'] - 3 * crimes_per_day['STD']
In [27]:
fig = go.Figure()

fig1 = go.Scatter(y = crimes_per_day[0],
                  x = crimes_per_day.index,
                  line = dict(color = "blue"), 
                  name = "nombre de délits")

fig2 = go.Scatter(x = crimes_per_day.index, y = UCL.values, 
              line = dict(dash = "dash",
              color = "red"), name = "limite de contrôle supérieure") 

fig3 = go.Scatter(x = crimes_per_day.index, y = LCL.values, 
              line = dict(dash = "dash",
              color = "red"), name = "limite de contrôle inférieure") 

fig4 = go.Scatter(x = crimes_per_day.index, y = crimes_per_day["MEAN"],
                 line = dict(color = "red"), name = "moyenne")


fig.add_trace(fig2)
fig.add_trace(fig1)
fig.add_trace(fig3)
fig.add_trace(fig4)

fig.update_layout(title = dict(x = 0.5, text = "Evolution du nombre de délits"),
                 xaxis_title = "Date", yaxis_title = "Nombre de délits")

fig.show()

Décomposition de la série

In [28]:
print(f"Décomposition de la série")
decomposition = seasonal_decompose(crimes_per_day[0], period = 365)

trend = decomposition.trend
seasonal = decomposition.seasonal
residual = decomposition.resid

plt.subplot(411)
plt.plot(crimes_per_day[0], label = 'Original')
plt.legend(loc = 'best')

plt.subplot(412)
plt.plot(trend, label='Trend')
plt.legend(loc = 'best')

plt.subplot(413)
plt.plot(seasonal,label='Seasonality')
plt.legend(loc = 'best')

plt.subplot(414)
plt.plot(residual, label = 'Residuals')
plt.legend(loc = 'best',)

plt.tight_layout()
plt.show()
Décomposition de la série

Il semblerait qu'il y ait une légère tendance haussière, ainsi qu'une saisonnalité annuelle. Nous allons vérifier cela à l'aide d'un test de stationnarité (KPSS).

Test de stationnarité (KPSS)

Les hypothèses du test KPSS sont les suivantes :

  • $\mathcal{H}_0$ : la série est stationnaire,
  • $\mathcal{H}_1$ : la série n'est pas stationnaire.
In [29]:
def kpss_test(timeseries):
    print ('Résultats du Test KPSS:')
    # test kpss
    kpsstest = kpss(timeseries, regression='c')

    # récupération puis affichage des résultats
    kpss_output = pd.Series(kpsstest[0:3], index = ['Test Statistic','p-value','Lags Used'])
    for key,value in kpsstest[3].items():
      kpss_output['Critical Value (%s)'%key] = value
    print (kpss_output)
    
kpss_test(crimes_per_day[0])
Résultats du Test KPSS:
Test Statistic            6.043731
p-value                   0.010000
Lags Used                28.000000
Critical Value (10%)      0.347000
Critical Value (5%)       0.463000
Critical Value (2.5%)     0.574000
Critical Value (1%)       0.739000
dtype: float64

La statistique de test étant supérieure à la valeur critique à 5%, nous rejetons donc l'hypothèse nulle et pouvons ainsi considérer que la série n'est pas stationnaire, ce qui confirme la tendance précédemment conjecturée.

ACP

On réalise une ACP où les individus sont les quartiers et les variables sont les nombres de délits pour chacune des 3 classes préalablement définies.

In [30]:
df_pca = df_survey.iloc[:, -4:-1]
df_pca
Out[30]:
nb_violent_crimes nb_property_crimes nb_other_crimes
NBHD_NAME
elyria-swansea 363.0 2590 3326
wellshire 11.0 395 197
university 138.0 2145 1472
rosedale 53.0 825 694
cheesman-park 329.0 2512 4597
... ... ... ...
city-park 133.0 1103 1266
cbd 1072.0 7281 12380
north-capitol-hill 564.0 3258 5834
civic-center 531.0 1728 10696
capitol-hill 1082.0 6148 10348

78 rows × 3 columns

In [31]:
# ACP
from sklearn.decomposition import PCA
from sklearn import preprocessing

X = df_pca.values

X = preprocessing.normalize(X)

# Définition de la commande
pca = PCA()

# Composantes principales
C = pca.fit(X).transform(X)
C.shape
Out[31]:
(78, 3)

On représente les individus, c'est-à-dire les quartiers, sur les 2 premières composantes principales obtenues avec l'ACP.

In [32]:
cols = sns.color_palette("RdBu_r", n_colors = 78)
sns.palplot(cols)
print("\t \t \t \t Moins dangereux -> Plus dangereux")

# Représentation des individus
plt.figure(figsize = (10,8))

for i, j, nb, name in zip(C[:, 0], C[:, 1], df_survey["crimes_per_pop"].values, df_survey.index):
    rank_danger = sorted(df_survey.crimes_per_pop).index(nb)
    plt.scatter(i,j, color = cols[rank_danger])
    plt.text(i, j, name, fontsize = 9, color = cols[rank_danger], rotation = 20)

#plt.ylim(-0.2, 0.2)
#plt.xlim(-0.2, 0.2)
plt.show()
	 	 	 	 Moins dangereux -> Plus dangereux

On constate une nette séparation des quartiers en fonction du ratio de dangerosité défini précédemment.

On représente maintenant le cercle des corrélations.

In [33]:
(fig, ax) = plt.subplots(figsize = (12, 12))

for i in range(0, len(pca.components_[0])):
    ax.arrow(0, 0,  # Start the arrow at the origin
             pca.components_[0, i], pca.components_[1, i],  # 0 and 1 correspond to dimension 1 and 2
             head_width = 0.1,head_length = 0.1, ec = "black")
    plt.text(pca.components_[0, i] + 0.05, pca.components_[1, i] + 0.05, df_pca.columns.values[i], fontsize = 9)
 
an = np.linspace(0, 2 * np.pi, 100)  # Add a unit circle for scale

plt.plot(np.cos(an), np.sin(an))

plt.axis('equal')
ax.set_title('Variable factor map')

plt.axhline(y = 0, color = "black", linestyle = '-')
plt.axvline(x = 0, color = "black")

plt.show()

Le cercle des corrélations montre que les variables représentant le nombre de crimes sur la propriété et le nombre d'autres crimes sont très bien représentées, alors que la variable sur les crimes violents est mal représentée. Les deux premières ont un coefficient de corrélation nul.

Outils de visualisation

Carte de chaleur

On représente ci-dessous une carte de chaleur des crimes sur la propriété pour les années 2012 à 2019. On a choisi de ne pas faire figurer l'année 2020 dans la carte puisqu'on ne dispose que d'un mois de données ce qui n'est clairement pas suffisant.

In [34]:
crimes_df = df[df["Property Crime"] == 1]

crimes_geo_df = crimes_df[["GEO_LAT", "GEO_LON"]].dropna(subset=['GEO_LAT', 'GEO_LON'])

years = sorted(set(crimes_geo_df.index.year))[:-1]
heat_data = []

for year in years :
    df_year = crimes_geo_df[crimes_geo_df.index.year == year]
    heat_data.append(df_year.values.tolist())
In [35]:
denver_map = folium.Map(location=[39.72378, -104.899157],
                       zoom_start=12,
                       tiles="CartoDB positron")

hm = plugins.HeatMapWithTime(heat_data, index = years, radius = 3)
hm.add_to(denver_map)

denver_map
Out[35]:

La zone de chaleur principale se trouve dans le centre de Denver à l'est de l'autoroute. On constate moins de délits dans la périphérie de la ville, notamment aux alentours de l'aéroport situé au nord-est de la ville et le long des grands axes routiers.

Application Shiny

Nous avons développé une application Shiny permettant de visualiser le nombre de crimes par quartier, avec l'option de sélectionner le type de crimes ainsi qu'une fenêtre de sélection temporelle.

In [36]:
IFrame(src='https://florian-goron.shinyapps.io/Denver2/', width=1000, height=500)
Out[36]:

En sélectionnant différentes variables, on peut remarquer que le changement du type de crimes implique un changement de quartier. Ainsi par exemple, les délits de type drogue/alcool sont plus fréquents dans le centre de Denver, avec une échelle allant de 0 à 4000. En revanche les délits de type assauts aggravés sont concentrés dans un quartier du nord de Denver avec une échelle allant de 0 à 1000 seulement.

Ce Shiny nous permet d'émettre une hypothèse sur un lien entre le type de délits et le quartier, c'est pourquoi nous allons par la suite nous intéresser aux caractéristiques socio-économiques des quartiers de Denver afin de confirmer cette hypothèse.

Caractéristiques socio-économiques à Denver

Quartiers les plus ou moins dangereux

Nous allons dans un premier temps représenter les 10 quartiers les plus dangereux, c'est-à-dire les quartiers ayant le plus grand nombre de délits par habitant, ainsi que les 10 quartiers les moins dangereux.

In [37]:
neigh_ratio = df_survey["crimes_per_pop"].sort_values()
neigh_ratio = pd.DataFrame({"crimes_per_pop": neigh_ratio.values,
                           "quartier": neigh_ratio.index})

safest = neigh_ratio.iloc[:10]

dangerous = neigh_ratio.iloc[-10:]
dangerous

fig = make_subplots(rows=1, cols=2,
                   subplot_titles = ["10 quartiers les plus dangereux",
                                     "10 quartiers les moins dangereux"])

fig1 = px.bar(neigh_ratio.iloc[-10:], x = "crimes_per_pop", y = "quartier", orientation = "h")
fig.add_trace(fig1["data"][0], row = 1, col = 1)

fig1 = px.bar(neigh_ratio.iloc[:10], x = "crimes_per_pop", y = "quartier", orientation = "h")

fig2 = px.bar(neigh_ratio.iloc[:10], x = "crimes_per_pop", y = "quartier", orientation = "h")
fig.add_trace(fig2["data"][0], row = 1, col = 2)

fig.show()

On constate qu'en moyenne dans les quartiers les plus dangereux on a environ 10 à 20% de délits en plus par habitant que dans les quartiers les moins dangereux.

Matrice de corrélation

On représente la matrice de corrélation en sélectionnant seulement les variables socio-économiques qui ont une corrélation supérieure à 0.5 avec la variable du nombre de crimes sur la propriété, toujours dans l'optique de répondre à la problématique.

In [38]:
cols = list(df_survey.iloc[:, :-19].columns) + ["nb_property_crimes"]
In [39]:
sns.set(style="white")

corr = df_survey[cols].corr()

# on sélectionne les vars corr >= 0.5 avec nb_property_crimes
strong_corr = corr[abs(corr["nb_property_crimes"]) > 0.5].index
corr = corr.loc[strong_corr, strong_corr]

mask = np.triu(np.ones_like(corr, dtype=np.bool))

f, ax = plt.subplots(figsize=(15, 15))


cmap = sns.diverging_palette(220.0, 10.0, as_cmap=True)

ax = sns.heatmap(corr, mask=mask, cmap=cmap, vmax=1, vmin=-1, center=0,
            square=True, linewidths=.5, cbar_kws={"shrink": .5},
           annot = True, annot_kws = {"size": 8})
In [40]:
len(strong_corr) - 1
Out[40]:
38

On a 38 variables qui sont fortement corrélées à la variable nb_property_crimes, les plus fortes corrélations ayant lieu avec les variables concernant l'année de construction du logement de l'habitant. On note également de très fortes corrélations entre les variables socio-économiques, comme par exemple NOT_ENROLLED (non inscrit dans un système éducatif) et ONLY_ENGLISH_LNG (anglais comme seule langage) qui ont une corrélation de 0.91.

ACP variables socio-économiques

On fait une ACP en utilisant les 38 variables socio-économiques les plus corrélées à la variable nb_property_crimes.

In [41]:
df_pca = df_survey.loc[:, strong_corr[:-1]]

for col_num in np.where(df_pca.dtypes == np.object)[0]:
    col = df_pca.columns[col_num]
    df_pca[col] = pd.to_numeric(df_pca[col], errors='coerce')
    
df_pca = df_pca.fillna(0)
df_pca
Out[41]:
TTL_POPULATION_ALL WHITE TWO_OR_MORE MALE FEMALE AGE_20_TO_29 AGE_30_TO_39 AGE_40_TO_49 TTL_AGE_3_PLUS_ENRSTATUS NURSERY_OR_PRESCHOOL ... HH_INC_10000_14999 HH_INC_60000_74999 HH_INC_75000_99999 HH_INC_100000_124999 HH_INC_125000_149999 HH_INC_150000_199999 BUILT_2014_OR_LATER BUILT_2010_2013 BUILT_2000_2009 NATIVE
NBHD_NAME
elyria-swansea 6687.0 960.0 39.0 3485.0 3202.0 1021.0 701.0 1100.0 6319 95 ... 148 108 204 128 45 11 0 0 61 4343
wellshire 3529.0 2932.0 119.0 1691.0 1838.0 176.0 449.0 465.0 3370 106 ... 8 91 125 134 92 194 0 6 32 3280
university 9576.0 7672.0 241.0 4953.0 4623.0 2977.0 1496.0 840.0 9306 140 ... 139 242 232 344 364 238 38 21 135 8561
rosedale 2498.0 2101.0 6.0 1280.0 1218.0 459.0 705.0 320.0 2430 24 ... 26 103 215 136 104 132 25 40 105 2346
cheesman-park 8895.0 7269.0 238.0 4625.0 4270.0 2233.0 2294.0 948.0 8715 30 ... 194 623 690 424 241 374 146 9 61 8276
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
city-park 3375.0 2591.0 40.0 1566.0 1809.0 792.0 1047.0 303.0 3282 45 ... 132 93 222 173 142 78 56 5 582 3169
cbd 15853.0 12368.0 538.0 8478.0 7375.0 5994.0 4495.0 1399.0 15738 23 ... 517 961 1469 530 372 368 58 35 471 14742
north-capitol-hill 6040.0 4668.0 162.0 3316.0 2724.0 2334.0 1618.0 567.0 5996 0 ... 160 380 391 412 257 313 35 314 1078 5547
civic-center 1962.0 1596.0 28.0 1106.0 856.0 458.0 407.0 284.0 1933 0 ... 98 93 166 177 109 83 87 49 757 1789
capitol-hill 4107.0 3105.0 174.0 2220.0 1887.0 1426.0 883.0 608.0 4100 7 ... 89 205 206 122 174 332 0 412 444 3764

78 rows × 38 columns

In [42]:
# ACP

X = df_pca.values

X = preprocessing.normalize(X)

# Définition de la commande
pca = PCA()

# Composantes principales
C = pca.fit(X).transform(X)
C.shape
Out[42]:
(78, 38)

On représente les individus, c'est-à-dire les quartiers, sur les 2 premières composantes principales obtenues avec l'ACP.

In [43]:
cols = sns.color_palette("RdBu_r", n_colors=78)
sns.palplot(cols)
print("\t \t \t \t Moins dangereux -> Plus dangereux")

# Représentation des individus
plt.figure(figsize=(10,8))

for i, j, nb, name in zip(C[:, 0], C[:, 1], df_survey["crimes_per_pop"].values, df_survey.index):
    rank_danger = sorted(df_survey.crimes_per_pop).index(nb)
    plt.scatter(i,j, color = cols[rank_danger])
    plt.text(i, j, name, fontsize = 9, color = cols[rank_danger], rotation = 20)

#plt.ylim(-0.2, 0.2)
#plt.xlim(-0.2, 0.2)
plt.show()
	 	 	 	 Moins dangereux -> Plus dangereux

On constate que les quartiers les moins dangereux se retrouvent en majorité en haut à gauche du graphe alors que les quartiers plus dangereux en rouge s'éparpillent vers la droite et vers le bas.

In [44]:
(fig, ax) = plt.subplots(figsize=(12, 12))

for i in range(0, len(pca.components_[0])):
    ax.arrow(0, 0,  # Start the arrow at the origin
             pca.components_[0, i], pca.components_[1, i],  # 0 and 1 correspond to dimension 1 and 2
             head_width = 0.1,head_length=0.1, ec = "black")
    plt.text(pca.components_[0, i] + 0.05, pca.components_[1, i] + 0.05, df_pca.columns.values[i], fontsize = 9)
 
an = np.linspace(0, 2 * np.pi, 100)  # Add a unit circle for scale

plt.plot(np.cos(an), np.sin(an))

plt.axis('equal')
ax.set_title('Variable factor map')

plt.axhline(y=0, color = "black", linestyle='-')
plt.axvline(x=0, color = "black")

plt.show()

On constate sur ce cercle des corrélations que la seule variable qui est bien représentée est la variable "WHITE" qui indique le nombre de personnes de couleur blanche. L'orientation de cette variable sur le cercle en comparaison avec le graphe des individus semble indiquer que le nombre de personnes de couleur blanche est lié à un faible taux de délits par habitant.

Régression linéaire

In [54]:
from IPython.core.display import display, HTML

IFrame(src='./reg_lin.html', width=1000, height=700)
Out[54]:

Conclusion

Pour rappel, le but de ce projet était de déterminer s'il était possible d'expliquer le taux important de crimes contre la propriété dans la ville de Denver au travers de caractéristiques socio-économiques des habitants par quartier.

Les outils de visualisation utilisés, c'est-à-dire la carte de chaleur ainsi que l'application Shiny, nous ont permis dans un premier temps d'avoir une première intuition sur un possible lien entre le type de délits commis et le quartier dans lequel il avait été commis.

On a ensuite cherché à confirmer cette intuition en ajoutant dans notre étude les variables socio-économiques. La matrice de corrélation nous a montré que 38 variables étaient corrélées positivement avec les crimes contre la propriété, mais finalement, en testant ces 38 variables dans l'ACP, nous ne pouvons conclure que sur un lien entre les personnes de race blanche et un faible nombre de délits par habitant.

Enfin, nous avons décidé de nous intéresser uniquement aux cambriolages, qui est un type de délits faisant partie de la classe des crimes contre la propriété. Nous avons ainsi sélectionné les variables explicatives grâce à la régression linéaire, et pouvons finalement conclure sur six variables liées aux cambriolages, variables concernant la race de l'habitant, le revenu ainsi que l'âge.

Pour conclure sur ce projet, les données dont nous disposions ne nous ont pas permis de pouvoir affirmer avec certitude un possible lien entre les crimes contre la propriété et les caractéristiques socio-économiques des habitants de Denver. Néanmoins, c'est un jeu de données très complet que nous n'avons pas pu exploiter dans son ensemble et qui, couplé avec d'autres jeux de données disponibles sur le même site que le jeu de données initial, pourrait permettre de nouvelles approches pour répondre à la problématique.