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.
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
## Sinistre : 72130 lignes x 8 colonnes
## 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…
## 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.
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.
# 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 %)
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
##
## 18-24 25-34 35-49 50-64 65+
## 20594 62930 112816 78157 26940
# 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
## === Detection des incoherences ===
## 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
## Age conducteur > 100 : 0
## 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
# 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
##
## 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
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
## # 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.
# 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"
## 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) :
## drv1Age drv1DriveLicenceAge vhAge vhWeight
## 44.69352 24.73730 11.48597 1387.54572
## vhDIN vhValue
## 132.55881 11466.76280
##
## Ecarts-types (lapply) :
## $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) :
## Max difference : 2.842171e-14
# 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
## Repartition par annee de 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
## Delai moyen de cloture : 67.8 jours
# 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.
## Sinistre - NA par colonne :
## 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.
# 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 :
## # 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
Les consignes demandent 12 visualisations specifiques. On les produit toutes avec ggplot2.
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)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")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")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")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))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")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")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")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")# 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 (%)")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")# 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")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))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")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)")La p-valeur est definie comme \(p = P_{H_0}(|T| \geq |t_{obs}|)\).
## ============================================================
## TESTS STATISTIQUES
## ============================================================
# 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
## 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
## rho = 0.01 , p = 4.168741e-08
## ============================================================
## SYNTHESE — FACTEURS DE RISQUE
## ============================================================
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 :
## # 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
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