1 Contexte

Ce projet porte sur la base de données Contrats et Sinistres de la compagnie fictive ENSAssuRances. La base Contrats contient environ 320 000 lignes (situations contractuelles sur 5 exercices, 2019-2023) et la base Sinistres contient 72 130 opérations de gestion sur environ 35 000 sinistres uniques.

L’objectif est de structurer, nettoyer, analyser et visualiser ces données pour identifier les facteurs de risque liés aux sinistres automobiles et produire des insights exploitables pour la tarification et la prévention.

2 Chargement des donnees et bibliotheques

library(dplyr)
library(tidyr)
library(ggplot2)
library(readr)
library(readxl)
library(lubridate)
library(stringr)
library(corrplot)
library(scales)
library(forcats)
library(FactoMineR)
library(factoextra)
library(cluster)
library(class)

theme_set(theme_minimal(base_size = 13))
set.seed(42)
contrat <- read_csv("../data/Contrat.csv", show_col_types = FALSE)
sinistre <- read_excel("../data/Sinistre.xlsx")

cat("Contrat :", nrow(contrat), "lignes x", ncol(contrat), "colonnes\n")
## Contrat : 301437 lignes x 40 colonnes
cat("Sinistre :", nrow(sinistre), "lignes x", ncol(sinistre), "colonnes\n")
## Sinistre : 72130 lignes x 8 colonnes
glimpse(contrat)
## Rows: 301,437
## Columns: 40
## $ idxCt                <chr> "C002513884", "C002513884", "C002513884", "C00251…
## $ idxYear              <dbl> 2019, 2020, 2021, 2022, 2023, 2019, 2020, 2021, 2…
## $ vhImmat              <chr> "ME-6556-LY", "ME-6556-LY", "ME-6556-LY", "ME-655…
## $ sitStartDate         <date> 2019-01-01, 2020-01-01, 2021-01-01, 2022-01-01, …
## $ sitEndDate           <date> 2019-12-31, 2020-12-31, 2021-12-31, 2022-12-31, …
## $ sitExpo              <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ drv1Age              <dbl> 39.52, 40.52, 41.52, 42.52, 43.52, 59.09, 60.09, …
## $ drv1Sex              <chr> "H", "H", "H", "H", "H", "H", "H", "H", "H", "H",…
## $ drv1DriveLicenceType <chr> "Cond Accompagnée", "Cond Accompagnée", "Cond Acc…
## $ drv1DriveLicenceAge  <dbl> 18.68, 19.68, 20.68, 21.68, 22.68, 39.21, 40.21, …
## $ vhAge                <dbl> 2.20, 3.20, 4.20, 5.20, 6.20, 7.67, 8.67, 9.67, 1…
## $ ctFrm                <chr> "Med2", "Med2", "Med2", "Med2", "Med2", "Mini", "…
## $ ctAssBase            <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ ctAss0km             <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ ctAssVHR             <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ vhSegment            <chr> "Familiale", "Familiale", "Familiale", "Familiale…
## $ vhMarque             <chr> "Peugeot", "Peugeot", "Peugeot", "Peugeot", "Peug…
## $ vhEnergy             <chr> "Essence", "Essence", "Essence", "Essence", "Esse…
## $ vhWeight             <dbl> 1204, 1204, 1204, 1204, 1204, 1540, 1540, 1540, 1…
## $ vhDIN                <dbl> 141, 141, 141, 141, 141, 133, 133, 133, 133, 133,…
## $ vhValue              <dbl> 15047.99, 14596.55, 14158.65, 13733.89, 13321.87,…
## $ vhGroup              <dbl> 6, 6, 6, 6, 6, 20, 20, 20, 20, 20, 14, 14, 14, 14…
## $ vhClass              <dbl> 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2…
## $ ctUsage              <chr> "Pro", "Pro", "Pro", "Pro", "Pro", "Pri", "Pri", …
## $ ctKM                 <chr> "N", "N", "N", "N", "N", "N", "N", "N", "N", "N",…
## $ ctDeduc              <dbl> 400, 400, 400, 400, 400, 200, 200, 200, 200, 200,…
## $ claimsAnt            <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ ctINSEE              <chr> "8196", "8196", "8196", "8196", "8196", "53222", …
## $ id1_AssBase          <chr> NA, NA, NA, NA, "S23-0042530", NA, NA, NA, NA, "S…
## $ id1_Ass0km           <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ id1_AssVHR           <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ id2_AssBase          <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ id2_Ass0km           <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ id2_AssVHR           <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ id3_AssBase          <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ id3_Ass0km           <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ id3_AssVHR           <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
## $ COT_AssBase          <dbl> 21.01, 33.36, 43.62, 62.20, 75.00, 23.30, 35.60, …
## $ COT_Ass0km           <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0…
## $ COT_AssVHR           <dbl> 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0…
glimpse(sinistre)
## Rows: 72,130
## Columns: 8
## $ idx_sin  <chr> "S18-0043146", "S18-0043146", "S18-0043146", "S18-0057961", "…
## $ gar_sin  <chr> "AssVHR", "AssVHR", "AssVHR", "Ass0km", "Ass0km", "Ass0km", "…
## $ surv_sin <chr> "2018-12-08", "2018-12-08", "2018-12-08", "2018-10-30", "2018…
## $ decl_sin <chr> "2018-12-10", "2018-12-10", "2018-12-10", "2018-10-30", "2018…
## $ clo_sin  <chr> NA, NA, "2019-08-05", NA, NA, "2019-01-13", NA, NA, "2019-01-…
## $ gest_sin <chr> "2018-12-10", "2018-12-31", "2019-08-05", "2018-10-30", "2018…
## $ mt_eval  <chr> "170.0", "170.0", "262.86", "150.0", "150.0", "229.51", "170.…
## $ mt_regl  <chr> "0.0", "0.0", "262.86", "0.0", "0.0", "229.51", "0.0", "0.0",…

On a bien les 40 colonnes du dictionnaire Contrats et les 8 colonnes du dictionnaire Sinistres. Chaque contrat est identifie par idxCt et chaque exercice par idxYear (2019 a 2023). Les sinistres sont identifies par idx_sin et references dans les contrats via les colonnes id1_AssBase, id1_Ass0km, etc.


3 VOLET 1 — Ingenierie des donnees

3.1 Manipulation de donnees tabulaires

3.1.1 group_by + summarise

On calcule les statistiques cles par exercice pour avoir une vue d’ensemble.

resume_annee <- contrat %>%
  group_by(idxYear) %>%
  summarise(
    nb_contrats = n(),
    nb_contrats_uniques = n_distinct(idxCt),
    age_moyen_conducteur = mean(drv1Age, na.rm = TRUE),
    age_moyen_vehicule = mean(vhAge, na.rm = TRUE),
    cotisation_moy = mean(COT_AssBase, na.rm = TRUE),
    .groups = "drop"
  )
resume_annee
## # A tibble: 5 × 6
##   idxYear nb_contrats nb_contrats_uniques age_moyen_conducteur
##     <dbl>       <int>               <int>                <dbl>
## 1    2019       55227               55227                 42.8
## 2    2020       56610               56610                 43.8
## 3    2021       58227               58227                 44.7
## 4    2022       61699               61699                 45.6
## 5    2023       69674               69674                 46.2
## # ℹ 2 more variables: age_moyen_vehicule <dbl>, cotisation_moy <dbl>
# resume par segment de vehicule
resume_segment <- contrat %>%
  group_by(vhSegment) %>%
  summarise(
    nb = n(),
    pct = n() / nrow(contrat) * 100,
    age_vh_moy = mean(vhAge, na.rm = TRUE),
    valeur_moy = mean(vhValue, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(nb))
resume_segment
## # A tibble: 4 × 5
##   vhSegment     nb   pct age_vh_moy valeur_moy
##   <chr>      <int> <dbl>      <dbl>      <dbl>
## 1 Citadine  120614  40.0       11.5     11323.
## 2 Familiale  75566  25.1       11.5     11289.
## 3 Compacte   59953  19.9       11.5     11270.
## 4 SUV        45304  15.0       11.5     12407.

3.1.2 filter

# vehicules electriques/hybrides
electriques <- contrat %>% filter(vhEnergy == "Electrique/Hybride")
cat("Vehicules electriques/hybrides :", nrow(electriques),
    "(", round(nrow(electriques)/nrow(contrat)*100, 1), "%)\n")
## Vehicules electriques/hybrides : 45526 ( 15.1 %)
# jeunes conducteurs (moins de 25 ans)
jeunes <- contrat %>% filter(drv1Age < 25)
cat("Jeunes conducteurs (<25 ans) :", nrow(jeunes),
    "(", round(nrow(jeunes)/nrow(contrat)*100, 1), "%)\n")
## Jeunes conducteurs (<25 ans) : 20594 ( 6.8 %)
# contrats petit rouleur
petit_rouleur <- contrat %>% filter(ctKM == "O")
cat("Contrats petit rouleur :", nrow(petit_rouleur),
    "(", round(nrow(petit_rouleur)/nrow(contrat)*100, 1), "%)\n")
## Contrats petit rouleur : 40464 ( 13.4 %)

3.1.3 mutate — creation de variables

contrat <- contrat %>%
  mutate(
    # classes d'age du conducteur
    classe_age = case_when(
      drv1Age < 25 ~ "18-24",
      drv1Age < 35 ~ "25-34",
      drv1Age < 50 ~ "35-49",
      drv1Age < 65 ~ "50-64",
      TRUE ~ "65+"
    ),
    # classes d'age du vehicule
    classe_vh = case_when(
      vhAge < 3 ~ "Neuf (<3 ans)",
      vhAge < 8 ~ "Recent (3-7 ans)",
      vhAge < 15 ~ "Mature (8-14 ans)",
      TRUE ~ "Ancien (15+ ans)"
    ),
    # indicateur : a eu au moins un sinistre AssBase
    a_sinistre = !is.na(id1_AssBase),
    # nombre total de sinistres sur la situation
    nb_sin_situation = (!is.na(id1_AssBase)) + (!is.na(id1_Ass0km)) + (!is.na(id1_AssVHR)) +
                       (!is.na(id2_AssBase)) + (!is.na(id2_Ass0km)) + (!is.na(id2_AssVHR)) +
                       (!is.na(id3_AssBase)) + (!is.na(id3_Ass0km)) + (!is.na(id3_AssVHR)),
    # cotisation totale
    cotisation_totale = COT_AssBase + COT_Ass0km + COT_AssVHR,
    # dates
    dt_debut = as.Date(sitStartDate),
    dt_fin = as.Date(sitEndDate)
  )

cat("Nouvelles variables creees\n")
## Nouvelles variables creees
table(contrat$classe_age)
## 
##  18-24  25-34  35-49  50-64    65+ 
##  20594  62930 112816  78157  26940

3.1.4 arrange (order_by)

# top 10 des vehicules les plus chers
contrat %>%
  select(idxCt, vhMarque, vhSegment, vhValue, vhAge) %>%
  arrange(desc(vhValue)) %>%
  head(10)
## # A tibble: 10 × 5
##    idxCt      vhMarque vhSegment vhValue vhAge
##    <chr>      <chr>    <chr>       <dbl> <dbl>
##  1 C003531059 Seat     Citadine   43976.  0.58
##  2 C003432292 Autre    Citadine   42987.  0.68
##  3 C003405602 Citroen  Compacte   42826.  0.77
##  4 C003429016 Renault  SUV        42512.  0.78
##  5 C003563060 Peugeot  Familiale  42476.  1.27
##  6 C003422045 Peugeot  SUV        42306.  0.6 
##  7 C003426306 Audi     Citadine   42302.  0.75
##  8 C003430940 Peugeot  Familiale  42290.  1.35
##  9 C003064558 Autre    Compacte   42028.  1.92
## 10 C003450759 Renault  Compacte   41940.  0.92

3.1.5 Detection des incoherences et doublons

cat("=== Detection des incoherences ===\n\n")
## === Detection des incoherences ===
# doublons exacts
cat("1. Doublons exacts :", sum(duplicated(contrat)), "\n")
## 1. Doublons exacts : 0
# meme contrat + meme annee = doublon logique
doublons_logiques <- contrat %>% group_by(idxCt, idxYear) %>% filter(n() > 1)
cat("2. Doublons (idxCt + idxYear) :", nrow(doublons_logiques), "\n")
## 2. Doublons (idxCt + idxYear) : 0
# age conducteur aberrant
cat("3. Age conducteur < 18 :", sum(contrat$drv1Age < 18, na.rm = TRUE), "\n")
## 3. Age conducteur < 18 : 0
cat("   Age conducteur > 100 :", sum(contrat$drv1Age > 100, na.rm = TRUE), "\n")
##    Age conducteur > 100 : 0
# age vehicule negatif
cat("4. Age vehicule < 0 :", sum(contrat$vhAge < 0, na.rm = TRUE), "\n")
## 4. Age vehicule < 0 : 0
# valeur vehicule = 0
cat("5. Valeur vehicule = 0 :", sum(contrat$vhValue == 0, na.rm = TRUE), "\n")
## 5. Valeur vehicule = 0 : 0
# cotisation negative
cat("6. Cotisation negative :", sum(contrat$COT_AssBase < 0, na.rm = TRUE), "\n")
## 6. Cotisation negative : 0
# exposition hors [0,1]
cat("7. Exposition hors [0,1] :", sum(contrat$sitExpo < 0 | contrat$sitExpo > 1, na.rm = TRUE), "\n")
## 7. Exposition hors [0,1] : 0

3.2 Recodage et transformation

# recodage des modalites
contrat <- contrat %>%
  mutate(
    sexe_label = recode(drv1Sex, "H" = "Homme", "F" = "Femme"),
    permis_label = recode(drv1DriveLicenceType,
                          "Traditionnel" = "Traditionnel",
                          "Cond Accompagnée" = "Conduite accompagnee"),
    formule_label = recode(ctFrm,
                           "Mini" = "Tiers",
                           "Med1" = "Tiers+",
                           "Med2" = "Intermediaire",
                           "Maxi" = "Tous risques"),
    usage_label = recode(ctUsage, "Pri" = "Prive", "Pro" = "Professionnel"),
    km_label = recode(ctKM, "O" = "Petit rouleur", "N" = "Standard")
  )

cat("Recodage effectue\n")
## Recodage effectue
table(contrat$formule_label)
## 
## Intermediaire         Tiers        Tiers+  Tous risques 
##         23958        192558         25480         59441
# agregation par annee et segment
agg_annee_segment <- contrat %>%
  group_by(idxYear, vhSegment) %>%
  summarise(
    nb = n(),
    taux_sinistre = mean(a_sinistre) * 100,
    cotisation_moy = mean(COT_AssBase, na.rm = TRUE),
    .groups = "drop"
  )
agg_annee_segment %>% head(12)
## # A tibble: 12 × 5
##    idxYear vhSegment    nb taux_sinistre cotisation_moy
##      <dbl> <chr>     <int>         <dbl>          <dbl>
##  1    2019 Citadine  22109          8.38           21.3
##  2    2019 Compacte  10983          8.19           21.7
##  3    2019 Familiale 13856          8.02           22.4
##  4    2019 SUV        8279          7.83           22.7
##  5    2020 Citadine  22654          5.43           33.3
##  6    2020 Compacte  11249          5.60           34.2
##  7    2020 Familiale 14189          5.67           35.8
##  8    2020 SUV        8518          6.00           36.4
##  9    2021 Citadine  23305          9.36           43.3
## 10    2021 Compacte  11573          9.61           44.7
## 11    2021 Familiale 14580          9.16           46.9
## 12    2021 SUV        8769          9.18           47.9

3.3 Jointure de donnees

La jointure entre Contrats et Sinistres n’est pas directe : les identifiants de sinistres sont stockes en colonnes dans la table Contrats (id1_AssBase, id2_AssBase, etc.). Il faut d’abord les pivoter en format long avec pivot_longer, puis faire la jointure.

# etape 1 : pivoter les colonnes d'ID sinistre en format long
# on selectionne explicitement les colonnes de sinistre (pas idxCt ni idxYear)
sin_id_cols <- c("id1_AssBase", "id1_Ass0km", "id1_AssVHR",
                 "id2_AssBase", "id2_Ass0km", "id2_AssVHR",
                 "id3_AssBase", "id3_Ass0km", "id3_AssVHR")

contrat_sins <- contrat %>%
  select(idxCt, idxYear, all_of(sin_id_cols)) %>%
  mutate(across(all_of(sin_id_cols), as.character)) %>%
  pivot_longer(
    cols = all_of(sin_id_cols),
    names_to = "col_sinistre",
    values_to = "idx_sin",
    values_drop_na = TRUE
  )
cat("Apres pivot_longer :", nrow(contrat_sins), "lignes\n")
## Apres pivot_longer : 33711 lignes
head(contrat_sins)
## # A tibble: 6 × 4
##   idxCt      idxYear col_sinistre idx_sin    
##   <chr>        <dbl> <chr>        <chr>      
## 1 C002513884    2023 id1_AssBase  S23-0042530
## 2 C002513898    2023 id1_AssBase  S23-0042550
## 3 C002513949    2022 id1_AssBase  S22-0042603
## 4 C002513967    2019 id1_AssBase  S19-0042612
## 5 C002513967    2020 id1_AssBase  S20-0042615
## 6 C002513967    2021 id1_AssBase  S21-0042623
# etape 2 : prendre la derniere operation de gestion par sinistre
sinistre_dernier <- sinistre %>%
  group_by(idx_sin) %>%
  slice_max(order_by = gest_sin, n = 1, with_ties = FALSE) %>%
  ungroup()
cat("Sinistres uniques (derniere gestion) :", nrow(sinistre_dernier), "\n")
## Sinistres uniques (derniere gestion) : 35214
# etape 3 : jointures

# LEFT JOIN : tous les sinistres references dans contrat, meme sans detail
left_j <- contrat_sins %>% left_join(sinistre_dernier, by = "idx_sin")
cat("LEFT JOIN :", nrow(left_j), "lignes, NaN mt_eval :", sum(is.na(left_j$mt_eval)), "\n")
## LEFT JOIN : 33711 lignes, NaN mt_eval : 4797
# INNER JOIN : seulement les sinistres presents dans les deux tables
inner_j <- contrat_sins %>% inner_join(sinistre_dernier, by = "idx_sin")
cat("INNER JOIN :", nrow(inner_j), "lignes\n")
## INNER JOIN : 28914 lignes
# RIGHT JOIN : tous les sinistres, meme sans contrat associe
right_j <- contrat_sins %>% right_join(sinistre_dernier, by = "idx_sin")
cat("RIGHT JOIN :", nrow(right_j), "lignes, NaN idxCt :", sum(is.na(right_j$idxCt)), "\n")
## RIGHT JOIN : 35214 lignes, NaN idxCt : 6300
# FULL JOIN
full_j <- contrat_sins %>% full_join(sinistre_dernier, by = "idx_sin")
cat("FULL JOIN :", nrow(full_j), "lignes\n")
## FULL JOIN : 40011 lignes

Le LEFT JOIN est le plus adapte : on garde tous les sinistres references dans nos contrats et on enrichit avec les details (montants, dates). Les sinistres sans correspondance (NaN dans mt_eval) sont ceux dont l’identifiant n’apparait pas dans la table Sinistre.

3.4 Structures de controle et flux

# if/else avec sapply
classifier_risque <- function(nb_sin) {
  if (nb_sin == 0) "Sans sinistre"
  else if (nb_sin == 1) "Risque faible"
  else if (nb_sin == 2) "Risque modere"
  else "Risque eleve"
}
contrat$risque <- sapply(contrat$nb_sin_situation, classifier_risque)
table(contrat$risque)
## 
##  Risque eleve Risque faible Risque modere Sans sinistre 
##           131         29405          1953        269948
# switch
type_garantie <- function(formule) {
  switch(formule,
    "Mini" = "Responsabilite civile",
    "Med1" = "RC + vol + incendie",
    "Med2" = "RC + vol + incendie + bris",
    "Maxi" = "Tous risques",
    "Inconnu"
  )
}
# test
sapply(c("Mini", "Med1", "Med2", "Maxi"), type_garantie)
##                         Mini                         Med1 
##      "Responsabilite civile"        "RC + vol + incendie" 
##                         Med2                         Maxi 
## "RC + vol + incendie + bris"               "Tous risques"
# boucle for : evolution par annee
cat("Evolution annuelle :\n")
## Evolution annuelle :
for (an in sort(unique(contrat$idxYear))) {
  sub <- contrat %>% filter(idxYear == an)
  taux <- mean(sub$a_sinistre) * 100
  cat(sprintf("  %d : %d contrats, taux sinistre = %.1f%%\n", an, nrow(sub), taux))
}
##   2019 : 55227 contrats, taux sinistre = 8.2%
##   2020 : 56610 contrats, taux sinistre = 5.6%
##   2021 : 58227 contrats, taux sinistre = 9.3%
##   2022 : 61699 contrats, taux sinistre = 11.4%
##   2023 : 69674 contrats, taux sinistre = 12.1%
# boucle while : seuil de cotisation pour 80% du portefeuille
seuil <- 0
while (mean(contrat$COT_AssBase <= seuil) < 0.80) {
  seuil <- seuil + 1
}
cat("80% des contrats ont une cotisation AssBase <=", seuil, "EUR\n")
## 80% des contrats ont une cotisation AssBase <= 64 EUR
# apply / lapply / sapply
cols_num <- c("drv1Age", "drv1DriveLicenceAge", "vhAge", "vhWeight", "vhDIN", "vhValue")

# sapply : moyennes
cat("Moyennes (sapply) :\n")
## Moyennes (sapply) :
print(sapply(contrat[cols_num], mean, na.rm = TRUE))
##             drv1Age drv1DriveLicenceAge               vhAge            vhWeight 
##            44.69352            24.73730            11.48597          1387.54572 
##               vhDIN             vhValue 
##           132.55881         11466.76280
# lapply : ecarts-types (retourne une liste)
cat("\nEcarts-types (lapply) :\n")
## 
## Ecarts-types (lapply) :
print(lapply(contrat[cols_num], sd, na.rm = TRUE))
## $drv1Age
## [1] 13.68547
## 
## $drv1DriveLicenceAge
## [1] 13.65957
## 
## $vhAge
## [1] 4.879809
## 
## $vhWeight
## [1] 308.5832
## 
## $vhDIN
## [1] 47.5019
## 
## $vhValue
## [1] 6107.463
# apply sur lignes : somme des cotisations par ligne
contrat$cot_check <- apply(contrat[, c("COT_AssBase", "COT_Ass0km", "COT_AssVHR")], 1, sum)
cat("\nVerification cotisation totale (apply sur lignes) :\n")
## 
## Verification cotisation totale (apply sur lignes) :
cat("Max difference :", max(abs(contrat$cotisation_totale - contrat$cot_check)), "\n")
## Max difference : 2.842171e-14

3.5 Gestion des dates

# conversion des dates
contrat <- contrat %>%
  mutate(
    dt_debut = as.Date(sitStartDate),
    dt_fin = as.Date(sitEndDate),
    annee_debut = year(dt_debut),
    mois_debut = month(dt_debut),
    duree_jours = as.numeric(dt_fin - dt_debut)
  )

cat("Duree moyenne des situations :", round(mean(contrat$duree_jours, na.rm = TRUE)), "jours\n")
## Duree moyenne des situations : 352 jours
cat("Repartition par annee de debut :\n")
## Repartition par annee de debut :
table(contrat$annee_debut)
## 
##  2019  2020  2021  2022  2023 
## 55227 56610 58227 61699 69674
# dates des sinistres
sinistre <- sinistre %>%
  mutate(
    dt_surv = as.Date(surv_sin),
    dt_decl = as.Date(decl_sin),
    dt_clo = as.Date(clo_sin),
    dt_gest = as.Date(gest_sin),
    delai_declaration = as.numeric(dt_decl - dt_surv),
    delai_cloture = as.numeric(dt_clo - dt_surv),
    annee_surv = year(dt_surv),
    mois_surv = month(dt_surv)
  )

cat("Delai moyen de declaration :", round(mean(sinistre$delai_declaration, na.rm = TRUE), 1), "jours\n")
## Delai moyen de declaration : 2.4 jours
cat("Delai moyen de cloture :", round(mean(sinistre$delai_cloture, na.rm = TRUE), 1), "jours\n")
## Delai moyen de cloture : 67.8 jours

3.6 Valeurs manquantes

# bilan des NA
na_bilan <- data.frame(
  variable = names(contrat),
  nb_na = sapply(contrat, function(x) sum(is.na(x))),
  pct_na = sapply(contrat, function(x) round(sum(is.na(x)) / length(x) * 100, 2))
) %>%
  filter(nb_na > 0) %>%
  arrange(desc(pct_na))

na_bilan
##                variable  nb_na pct_na
## id2_AssVHR   id2_AssVHR 301437 100.00
## id3_Ass0km   id3_Ass0km 301437 100.00
## id3_AssVHR   id3_AssVHR 301437 100.00
## id3_AssBase id3_AssBase 301369  99.98
## id2_Ass0km   id2_Ass0km 301335  99.97
## id1_AssVHR   id1_AssVHR 300472  99.68
## id2_AssBase id2_AssBase 299885  99.49
## id1_Ass0km   id1_Ass0km 298994  99.19
## id1_AssBase id1_AssBase 272856  90.52
# visualisation
ggplot(na_bilan %>% head(15), aes(x = reorder(variable, pct_na), y = pct_na)) +
  geom_col(fill = ifelse(na_bilan$pct_na[1:min(15, nrow(na_bilan))] > 50, "#d32f2f", "#ff9800")) +
  coord_flip() +
  labs(title = "Taux de valeurs manquantes", x = "", y = "% NA") +
  theme_minimal()

Les colonnes id2_* et id3_* ont beaucoup de NaN, c’est normal : la plupart des contrats n’ont pas 2 ou 3 sinistres. Ce ne sont pas des “vrais” manquants mais des absences de sinistre.

# NA dans la table sinistres
cat("Sinistre - NA par colonne :\n")
## Sinistre - NA par colonne :
sapply(sinistre, function(x) sum(is.na(x)))
##           idx_sin           gar_sin          surv_sin          decl_sin 
##                 0                 0                 0                 0 
##           clo_sin          gest_sin           mt_eval           mt_regl 
##             39099                 0                 0                 0 
##           dt_surv           dt_decl            dt_clo           dt_gest 
##                 0                 0             39099                 0 
## delai_declaration     delai_cloture        annee_surv         mois_surv 
##                 0             39099                 0                 0

La colonne clo_sin (date de cloture) a beaucoup de NA : les sinistres pas encore clos.

3.7 Donnees spatiales

# les 20 codes INSEE les plus frequents
top_insee <- contrat %>%
  count(ctINSEE, sort = TRUE) %>%
  head(20)

ggplot(top_insee, aes(x = reorder(ctINSEE, n), y = n)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = "Top 20 communes (code INSEE)", x = "Code INSEE", y = "Nombre de contrats")

# taux de sinistre par zone (premiers 2 chiffres du code INSEE = departement)
contrat <- contrat %>%
  mutate(departement = substr(ctINSEE, 1, 2))

dept_risque <- contrat %>%
  group_by(departement) %>%
  summarise(
    nb_contrats = n(),
    taux_sinistre = mean(a_sinistre) * 100,
    .groups = "drop"
  ) %>%
  filter(nb_contrats >= 100) %>%
  arrange(desc(taux_sinistre))

cat("Top 10 departements les plus sinistres :\n")
## Top 10 departements les plus sinistres :
dept_risque %>% head(10)
## # A tibble: 10 × 3
##    departement nb_contrats taux_sinistre
##    <chr>             <int>         <dbl>
##  1 69                 2720          10.8
##  2 45                 2526          10.8
##  3 68                 3183          10.6
##  4 83                 2195          10.6
##  5 36                 2145          10.5
##  6 77                 5016          10.4
##  7 84                 2055          10.4
##  8 40                 3448          10.1
##  9 89                 2992          10.1
## 10 28                 3233          10.1

4 VOLET 2 — Analyse et visualisation

Les consignes demandent 12 visualisations specifiques. On les produit toutes avec ggplot2.

4.1 1. Total de sinistres par annee

sin_par_annee <- sinistre %>%
  filter(!is.na(annee_surv)) %>%
  group_by(annee_surv) %>%
  summarise(nb = n_distinct(idx_sin), .groups = "drop")

ggplot(sin_par_annee, aes(x = annee_surv, y = nb)) +
  geom_col(fill = "#2196F3") +
  geom_text(aes(label = nb), vjust = -0.5) +
  labs(title = "Nombre de sinistres uniques par annee", x = "Annee", y = "Nombre") +
  scale_x_continuous(breaks = sin_par_annee$annee_surv)

4.2 2. Distribution des contrats par exercice

ggplot(contrat, aes(x = factor(idxYear))) +
  geom_bar(fill = "#4CAF50") +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5) +
  labs(title = "Distribution des contrats par exercice", x = "Exercice", y = "Nombre")

4.3 3. Repartition des types de vehicules

ggplot(contrat, aes(x = fct_infreq(vhSegment), fill = vhSegment)) +
  geom_bar() +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5) +
  labs(title = "Repartition par segment de vehicule", x = "", y = "Nombre") +
  theme(legend.position = "none")

4.4 4. Repartition selon l’alimentation

ggplot(contrat, aes(x = fct_infreq(vhEnergy), fill = vhEnergy)) +
  geom_bar() +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.5) +
  labs(title = "Repartition par alimentation", x = "", y = "Nombre") +
  scale_fill_manual(values = c("Essence" = "#FF9800", "Diesel" = "#607D8B",
                                "Electrique/Hybride" = "#4CAF50")) +
  theme(legend.position = "none")

4.5 5. Distribution par groupe de vehicule

ggplot(contrat, aes(x = factor(vhGroup))) +
  geom_bar(fill = "#795548") +
  labs(title = "Distribution par groupe de vehicule", x = "Groupe", y = "Nombre") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

4.6 6. Option petit rouleur

km_counts <- contrat %>% count(km_label) %>% mutate(pct = round(n / sum(n) * 100, 1))

ggplot(km_counts, aes(x = km_label, y = n, fill = km_label)) +
  geom_col() +
  geom_text(aes(label = paste0(n, "\n(", pct, "%)")), vjust = -0.3) +
  labs(title = "Contrats avec/sans option Petit Rouleur", x = "", y = "Nombre") +
  scale_fill_manual(values = c("Petit rouleur" = "#4CAF50", "Standard" = "#9E9E9E")) +
  theme(legend.position = "none")

4.7 7. Sinistres en fonction de l’age du societaire

ggplot(contrat, aes(x = classe_age, y = nb_sin_situation, fill = classe_age)) +
  geom_boxplot() +
  labs(title = "Nombre de sinistres selon l'age du conducteur",
       x = "Classe d'age", y = "Nombre de sinistres") +
  theme(legend.position = "none")

# taux de sinistre par tranche d'age
taux_age <- contrat %>%
  group_by(classe_age) %>%
  summarise(
    nb = n(),
    taux = mean(a_sinistre) * 100,
    .groups = "drop"
  )

ggplot(taux_age, aes(x = classe_age, y = taux, fill = classe_age)) +
  geom_col() +
  geom_text(aes(label = paste0(round(taux, 1), "%")), vjust = -0.5) +
  labs(title = "Taux de sinistralite par tranche d'age", x = "", y = "Taux (%)") +
  theme(legend.position = "none")

4.8 8. Sinistres selon antecedents

taux_ant <- contrat %>%
  group_by(claimsAnt) %>%
  summarise(
    nb = n(),
    taux_sin = mean(a_sinistre) * 100,
    .groups = "drop"
  )

ggplot(taux_ant, aes(x = factor(claimsAnt), y = taux_sin, fill = factor(claimsAnt))) +
  geom_col() +
  geom_text(aes(label = paste0(round(taux_sin, 1), "%")), vjust = -0.5) +
  labs(title = "Taux de sinistralite selon le nombre de sinistres anterieurs",
       x = "Sinistres anterieurs", y = "Taux (%)") +
  theme(legend.position = "none")

4.9 9. Sinistres par segment commercial

taux_segment <- contrat %>%
  group_by(vhSegment) %>%
  summarise(
    nb = n(),
    taux_sin = mean(a_sinistre) * 100,
    nb_sin_moy = mean(nb_sin_situation),
    .groups = "drop"
  )

ggplot(taux_segment, aes(x = reorder(vhSegment, -taux_sin), y = taux_sin, fill = vhSegment)) +
  geom_col() +
  geom_text(aes(label = paste0(round(taux_sin, 1), "%")), vjust = -0.5) +
  labs(title = "Taux de sinistralite par segment", x = "", y = "Taux (%)") +
  theme(legend.position = "none")

4.10 10. Vehicules a risque eleve

# identification des vehicules a risque : croisement marque x segment
risque_marque <- contrat %>%
  group_by(vhMarque, vhSegment) %>%
  summarise(
    nb = n(),
    taux = mean(a_sinistre) * 100,
    .groups = "drop"
  ) %>%
  filter(nb >= 100) %>%
  arrange(desc(taux))

ggplot(risque_marque %>% head(15),
       aes(x = reorder(paste(vhMarque, vhSegment, sep = " - "), taux), y = taux)) +
  geom_col(fill = "#d32f2f") +
  coord_flip() +
  labs(title = "Top 15 combinaisons marque-segment les plus sinistrees",
       x = "", y = "Taux de sinistralite (%)")

4.11 11. Sinistres selon le sexe

taux_sexe <- contrat %>%
  group_by(sexe_label) %>%
  summarise(
    nb_contrats = n(),
    nb_sinistres = sum(a_sinistre),
    taux = mean(a_sinistre) * 100,
    .groups = "drop"
  )

ggplot(taux_sexe, aes(x = sexe_label, y = taux, fill = sexe_label)) +
  geom_col(width = 0.5) +
  geom_text(aes(label = paste0(round(taux, 1), "% (n=", nb_sinistres, ")")), vjust = -0.5) +
  labs(title = "Taux de sinistralite selon le sexe", x = "", y = "Taux (%)") +
  scale_fill_manual(values = c("Homme" = "#2196F3", "Femme" = "#E91E63")) +
  theme(legend.position = "none")

4.12 12. Zones geographiques a risque

# carte par departement
dept_map <- contrat %>%
  group_by(departement) %>%
  summarise(
    nb = n(),
    taux = mean(a_sinistre) * 100,
    .groups = "drop"
  ) %>%
  filter(nb >= 50)

ggplot(dept_map, aes(x = reorder(departement, -taux), y = taux)) +
  geom_col(aes(fill = taux)) +
  scale_fill_gradient(low = "#C8E6C9", high = "#B71C1C") +
  labs(title = "Taux de sinistralite par departement",
       x = "Departement", y = "Taux (%)") +
  theme(axis.text.x = element_text(angle = 90, size = 6), legend.position = "none")


5 Analyse multivariee

5.1 Correlations

vars_num <- contrat %>%
  select(drv1Age, drv1DriveLicenceAge, vhAge, vhWeight, vhDIN, vhValue,
         sitExpo, COT_AssBase, nb_sin_situation) %>%
  drop_na()

corrplot(cor(vars_num), method = "color", type = "upper",
         tl.cex = 0.8, addCoef.col = "black", number.cex = 0.6,
         title = "Matrice de correlations", mar = c(0,0,2,0))

5.2 ACP

L’ACP sur les variables numeriques du portefeuille permet d’identifier les dimensions principales de variabilite.

En ACP normee, la k-ieme composante principale est \(y_k = X_c u_k\) ou \(u_k\) est le vecteur propre associe a \(\lambda_k\). La variance expliquee par l’axe k est \(\lambda_k / \sum \lambda_j\).

acp_data <- contrat %>%
  select(drv1Age, drv1DriveLicenceAge, vhAge, vhWeight, vhDIN, vhValue, COT_AssBase) %>%
  drop_na()
set.seed(42)
acp_data <- acp_data[sample(nrow(acp_data), min(10000, nrow(acp_data))), ]

res_acp <- PCA(acp_data, graph = FALSE)

# eboulis
fviz_eig(res_acp, addlabels = TRUE) +
  labs(title = "Eboulis des valeurs propres")

# cercle des correlations
fviz_pca_var(res_acp, col.var = "contrib",
             gradient.cols = c("blue", "yellow", "red")) +
  labs(title = "Cercle des correlations — ACP")

# projection des individus colores par segment
acp_subset <- contrat %>%
  select(drv1Age, drv1DriveLicenceAge, vhAge, vhWeight, vhDIN, vhValue, COT_AssBase, vhSegment) %>%
  drop_na()
set.seed(42)
acp_idx <- sample(nrow(acp_subset), min(10000, nrow(acp_subset)))
acp_sample <- acp_subset[acp_idx, ]

res_acp <- PCA(acp_sample[, 1:7], graph = FALSE)
ind_coords <- as.data.frame(res_acp$ind$coord[, 1:2])
ind_coords$segment <- acp_sample$vhSegment

ggplot(ind_coords, aes(x = Dim.1, y = Dim.2, color = segment)) +
  geom_point(alpha = 0.3, size = 1) +
  labs(title = "Projection des individus (ACP)", x = "PC1", y = "PC2")

5.3 Clustering

On cherche des profils de contrats par K-means.

\(W = \sum_{k=1}^{K} \sum_{i \in C_k} \|x_i - \mu_k\|^2\)

clust_data <- contrat %>%
  select(drv1Age, vhAge, vhValue, COT_AssBase, nb_sin_situation) %>%
  drop_na()
set.seed(42)
clust_data <- scale(clust_data[sample(nrow(clust_data), min(10000, nrow(clust_data))), ])

# methode du coude
fviz_nbclust(clust_data, kmeans, method = "wss", k.max = 8) +
  labs(title = "Methode du coude")

# silhouette
fviz_nbclust(clust_data, kmeans, method = "silhouette", k.max = 8) +
  labs(title = "Score silhouette")

# k-means avec k=3
km <- kmeans(clust_data, centers = 3, nstart = 25)
fviz_cluster(km, data = clust_data, geom = "point", pointsize = 1, alpha = 0.3) +
  labs(title = "Clusters K-means (k=3)")

# CAH
hc_sample <- clust_data[sample(nrow(clust_data), min(2000, nrow(clust_data))), ]
hc <- hclust(dist(hc_sample), method = "ward.D2")
fviz_dend(hc, k = 4, cex = 0.4, rect = TRUE) +
  labs(title = "Dendrogramme CAH (Ward)")

5.4 Tests statistiques

La p-valeur est definie comme \(p = P_{H_0}(|T| \geq |t_{obs}|)\).

cat(strrep("=", 60), "\n")
## ============================================================
cat("TESTS STATISTIQUES\n")
## TESTS STATISTIQUES
cat(strrep("=", 60), "\n\n")
## ============================================================
# 1. Chi2 : sexe et sinistralite sont-ils lies ?
ct_chi <- table(contrat$drv1Sex, contrat$a_sinistre)
chi_test <- chisq.test(ct_chi)
cat("1. Chi2 — Sexe x Sinistre\n")
## 1. Chi2 — Sexe x Sinistre
cat("   chi2 =", round(chi_test$statistic, 1), ", p =", format(chi_test$p.value, scientific = TRUE), "\n\n")
##    chi2 = 1.6 , p = 2.125673e-01
# 2. ANOVA : la cotisation differe-t-elle selon le segment ?
anova_cot <- aov(COT_AssBase ~ vhSegment, data = contrat)
cat("2. ANOVA — Cotisation ~ Segment\n")
## 2. ANOVA — Cotisation ~ Segment
print(summary(anova_cot))
##                 Df    Sum Sq Mean Sq F value Pr(>F)    
## vhSegment        3   1054258  351419    1043 <2e-16 ***
## Residuals   301433 101589488     337                   
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
# 3. Kruskal-Wallis : taux sinistre selon formule
cat("\n3. Kruskal-Wallis — nb sinistres ~ Formule\n")
## 
## 3. Kruskal-Wallis — nb sinistres ~ Formule
kw <- kruskal.test(nb_sin_situation ~ ctFrm, data = contrat)
cat("   H =", round(kw$statistic, 1), ", p =", format(kw$p.value, scientific = TRUE), "\n\n")
##    H = 548.5 , p = 1.496863e-118
# 4. Shapiro-Wilk : normalite de la cotisation
cat("4. Shapiro-Wilk — Normalite de COT_AssBase (n=1000)\n")
## 4. Shapiro-Wilk — Normalite de COT_AssBase (n=1000)
sw <- shapiro.test(sample(contrat$COT_AssBase, 1000))
cat("   W =", round(sw$statistic, 4), ", p =", format(sw$p.value, scientific = TRUE), "\n")
##    W = 0.9833 , p = 2.669525e-09
# 5. Spearman : correlation age conducteur / nb sinistres
sp <- cor.test(contrat$drv1Age, contrat$nb_sin_situation, method = "spearman")
cat("\n5. Spearman — Age conducteur vs Nb sinistres\n")
## 
## 5. Spearman — Age conducteur vs Nb sinistres
cat("   rho =", round(sp$estimate, 4), ", p =", format(sp$p.value, scientific = TRUE), "\n")
##    rho = 0.01 , p = 4.168741e-08

6 Aide a la decision

cat(strrep("=", 60), "\n")
## ============================================================
cat("SYNTHESE — FACTEURS DE RISQUE\n")
## SYNTHESE — FACTEURS DE RISQUE
cat(strrep("=", 60), "\n\n")
## ============================================================
profil <- contrat %>%
  group_by(vhSegment, vhEnergy, classe_age) %>%
  summarise(
    nb = n(),
    taux_sin = mean(a_sinistre) * 100,
    cotisation_moy = mean(COT_AssBase, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  filter(nb >= 200) %>%
  arrange(desc(taux_sin))

cat("Top 10 profils les plus sinistres :\n")
## Top 10 profils les plus sinistres :
print(profil %>% head(10))
## # A tibble: 10 × 6
##    vhSegment vhEnergy           classe_age    nb taux_sin cotisation_moy
##    <chr>     <chr>              <chr>      <int>    <dbl>          <dbl>
##  1 Familiale Electrique/Hybride 65+          937     12.8           51.0
##  2 SUV       Electrique/Hybride 65+          551     12.5           53.1
##  3 Familiale Electrique/Hybride 50-64       2924     11.6           49.6
##  4 Compacte  Electrique/Hybride 65+          769     11.3           49.2
##  5 Citadine  Electrique/Hybride 50-64       4678     11.3           45.5
##  6 SUV       Electrique/Hybride 35-49       2513     11.1           49.0
##  7 Familiale Electrique/Hybride 35-49       4328     11.1           48.1
##  8 Compacte  Electrique/Hybride 50-64       2335     10.9           46.9
##  9 Citadine  Electrique/Hybride 35-49       6602     10.7           44.6
## 10 Citadine  Electrique/Hybride 18-24       1252     10.7           39.2

6.1 Recommandations

cat("
RECOMMANDATIONS POUR ENSAssuRances :

1. JEUNES CONDUCTEURS : taux de sinistralite plus eleve chez les 18-24 ans
   -> Adapter la tarification et proposer des formations

2. ANTECEDENTS : forte correlation entre sinistres anterieurs et risque futur
   -> Renforcer le malus pour claimsAnt >= 2

3. SEGMENTS A RISQUE : certaines combinaisons marque-segment sont plus sinistrees
   -> Cibler la prevention et ajuster les primes

4. OPTION PETIT ROULEUR : analyser si le taux est reellement plus faible
   -> Verifier la coherence kilometrique

5. ENERGIE : comparer la sinistralite electrique vs thermique
   -> Les vehicules electriques/hybrides ont un profil specifique
")
## 
## RECOMMANDATIONS POUR ENSAssuRances :
## 
## 1. JEUNES CONDUCTEURS : taux de sinistralite plus eleve chez les 18-24 ans
##    -> Adapter la tarification et proposer des formations
## 
## 2. ANTECEDENTS : forte correlation entre sinistres anterieurs et risque futur
##    -> Renforcer le malus pour claimsAnt >= 2
## 
## 3. SEGMENTS A RISQUE : certaines combinaisons marque-segment sont plus sinistrees
##    -> Cibler la prevention et ajuster les primes
## 
## 4. OPTION PETIT ROULEUR : analyser si le taux est reellement plus faible
##    -> Verifier la coherence kilometrique
## 
## 5. ENERGIE : comparer la sinistralite electrique vs thermique
##    -> Les vehicules electriques/hybrides ont un profil specifique