Objetivo : practicar visualización con ggplot2 y plotly en un contexto actuarial, leyendo e interpretando cada gráfico. No es solo cómo hacerlo, sino qué concluyes para tarificación, reservas y gestión del riesgo.
# Instala si hace falta:
# install.packages(c("tidyverse","ggrepel","plotly","ggridges","rpart","rpart.plot","scales"))
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.1     ✔ stringr   1.5.2
## ✔ ggplot2   4.0.0     ✔ tibble    3.3.0
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.1.0     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(ggplot2)
library(ggrepel)
library(plotly)
## 
## Attaching package: 'plotly'
## 
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## 
## The following object is masked from 'package:stats':
## 
##     filter
## 
## The following object is masked from 'package:graphics':
## 
##     layout
library(ggridges)
library(rpart)
library(rpart.plot)
library(scales)
## 
## Attaching package: 'scales'
## 
## The following object is masked from 'package:purrr':
## 
##     discard
## 
## The following object is masked from 'package:readr':
## 
##     col_factor
Simulamos frecuencia y severidad por asegurado/año con variables típicas para segmentación.
set.seed(2025)
n <- 2500
df <- tibble(
  anio = sample(2021:2024, n, replace = TRUE, prob = c(0.2, 0.3, 0.3, 0.2)),
  region = sample(c("Andina","Caribe","Pacífico","Orinoquía"), n, replace = TRUE, prob = c(0.5,0.2,0.25,0.05)),
  tipo_vehiculo = sample(c("Sedán","SUV","Moto","Pickup"), n, replace = TRUE, prob = c(0.4,0.3,0.2,0.1)),
  edad = pmin(pmax(round(rnorm(n, 42, 12)), 18), 80),
  genero = sample(c("F","M"), n, replace = TRUE, prob = c(0.45, 0.55)),
  # Exposición anual (años asegurado): entre 0.2 y 1
  expo = round(runif(n, 0.2, 1.0), 2)
) |>
  mutate(
    # Frecuencia (Poisson) condicionada por segmentos
    lambda_base = 0.25 +
      0.05*(tipo_vehiculo=="Moto") +
      0.07*(tipo_vehiculo=="Pickup") +
      0.03*(region=="Caribe") +
      0.04*(edad < 25) +
      0.02*(edad > 70),
    reclamos = rpois(n, lambda = pmax(lambda_base * expo, 0.01)),
    # Severidad lognormal dependiente del tipo de vehículo
    mu_sev = 8.0 + 0.25*(tipo_vehiculo %in% c("SUV","Pickup")),
    sigma_sev = 0.6 + 0.05*(region=="Pacífico"),
    costo_prom = rlnorm(n, meanlog = mu_sev, sdlog = sigma_sev),
    # Costo total (severidad por número de reclamos)
    costo_total = round(costo_prom * pmax(reclamos, 1) * rlnorm(n, 0, 0.2), 0),
    prima = round( # prima simple basada en frecuencia esperada y severidad típica
      (lambda_base * exp(mu_sev + (sigma_sev^2)/2)) * 1.15 * expo, 0
    )
  ) |>
  mutate(
    # Tasa de siniestralidad (loss ratio) simple
    loss_ratio = costo_total / pmax(prima, 1)
  )
glimpse(df)
## Rows: 2,500
## Columns: 14
## $ anio          <int> 2024, 2023, 2023, 2023, 2024, 2023, 2021, 2022, 2024, 20…
## $ region        <chr> "Pacífico", "Pacífico", "Andina", "Pacífico", "Andina", …
## $ tipo_vehiculo <chr> "SUV", "SUV", "Moto", "Moto", "Sedán", "Sedán", "Moto", …
## $ edad          <dbl> 55, 50, 45, 41, 53, 44, 63, 38, 36, 40, 61, 35, 45, 38, …
## $ genero        <chr> "F", "M", "M", "F", "M", "F", "M", "M", "M", "M", "M", "…
## $ expo          <dbl> 0.52, 0.22, 0.95, 0.46, 0.89, 0.87, 0.44, 0.53, 0.26, 0.…
## $ lambda_base   <dbl> 0.25, 0.25, 0.30, 0.30, 0.25, 0.25, 0.30, 0.25, 0.33, 0.…
## $ reclamos      <int> 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0,…
## $ mu_sev        <dbl> 8.25, 8.25, 8.00, 8.00, 8.00, 8.00, 8.00, 8.00, 8.00, 8.…
## $ sigma_sev     <dbl> 0.65, 0.65, 0.60, 0.65, 0.60, 0.65, 0.60, 0.60, 0.60, 0.…
## $ costo_prom    <dbl> 2033.355, 4577.208, 1606.826, 7117.637, 3959.740, 5172.4…
## $ costo_total   <dbl> 1865, 5315, 1579, 7732, 3837, 3745, 2690, 2614, 3993, 11…
## $ prima         <dbl> 707, 299, 1170, 584, 913, 921, 542, 544, 352, 1080, 1156…
## $ loss_ratio    <dbl> 2.6379066, 17.7759197, 1.3495726, 13.2397260, 4.2026287,…
Diccionario breve
-reclamos: frecuencia anual simulada.
-costo_total: severidad * frecuencia (aprox.).
-expo: exposición (años en riesgo).
-prima: prima técnica aproximada.
-loss_ratio: relación siniestro/prima (indicador de rentabilidad).
scale_y_log10() o scale_x_log10()
cuando la cola sea pesada.df |>
  ggplot(aes(costo_total)) +
  geom_histogram(bins = 40, color = "white") +
  scale_x_log10(labels = label_number(scale_cut = cut_si(" "))) +
  labs(title = "Distribución de costos (log10)",
       x = "Costo total (log10)",
       y = "Frecuencia")
df |>
  ggplot(aes(costo_total, fill = tipo_vehiculo)) +
  geom_density(alpha = 0.3) +
  scale_x_log10(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Densidad de costos por tipo de vehículo",
       x = "Costo total (log10)", y = "Densidad")
df |>
  ggplot(aes(tipo_vehiculo, costo_total, fill = tipo_vehiculo)) +
  geom_boxplot(outlier.alpha = 0.2) +
  scale_y_log10(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Severidad por tipo de vehículo",
       y = "Costo total (log10)", x = NULL)
set.seed(1)
df |>
  sample_n(800) |>
  ggplot(aes(tipo_vehiculo, costo_total, fill = tipo_vehiculo)) +
  geom_violin(trim = FALSE, alpha = 0.2) +
  geom_jitter(width = 0.1, alpha = 0.25) +
  scale_y_log10(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Distribución intra-segmento (violín + puntos)",
       y = "Costo total (log10)")
# INTERPRETACIÓN: TODO
df |>
  ggplot(aes(costo_total, region)) +
  geom_density_ridges(rel_min_height = 0.01, scale = 1.2) +
  scale_x_log10(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Perfiles de severidad por región (ridgelines)",
       x = "Costo total (log10)", y = NULL)
## Picking joint bandwidth of 0.0752
# INTERPRETACIÓN: TODO
df |>
  ggplot(aes(edad, costo_total, color = tipo_vehiculo)) +
  geom_point(alpha = 0.35) +
  geom_smooth(se = FALSE, method = "loess") +
  scale_y_log10(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Edad vs Costo total (con suavizado)",
       x = "Edad", y = "Costo total (log10)")
## `geom_smooth()` using formula = 'y ~ x'
# INTERPRETACIÓN: TODO
df |>
  ggplot(aes(edad, costo_total, size = expo, color = region)) +
  geom_point(alpha = 0.4) +
  scale_y_log10(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Burbuja: edad vs costo (tamaño = exposición)",
       x = "Edad", y = "Costo total (log10)", size = "Exposición")
# INTERPRETACIÓN: TODO
df |>
  group_by(tipo_vehiculo) |>
  summarise(mean_rec = mean(reclamos), se = sd(reclamos)/sqrt(n())) |>
  ggplot(aes(fct_reorder(tipo_vehiculo, mean_rec), mean_rec)) +
  geom_col() +
  geom_errorbar(aes(ymin = mean_rec - 1.96*se, ymax = mean_rec + 1.96*se), width = 0.2) +
  coord_flip() +
  labs(title = "Frecuencia media de reclamos por tipo",
       x = NULL, y = "Reclamos por año (promedio)")
# INTERPRETACIÓN: TODO
df |>
  group_by(anio) |>
  summarise(mediana = median(costo_total)) |>
  ggplot(aes(anio, mediana)) +
  geom_line() +
  geom_point() +
  scale_y_log10(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Evolución de severidad (mediana)",
       x = "Año", y = "Costo mediano (log10)")
# INTERPRETACIÓN: TODO
df_hm <- df |>
  mutate(edad_grupo = cut(edad, breaks = c(18,25,35,45,55,65,80),
                          labels = c("18-25","26-35","36-45","46-55","56-65","66-80"),
                          include.lowest = TRUE)) |>
  group_by(edad_grupo, tipo_vehiculo) |>
  summarise(mediana = median(costo_total), .groups = "drop")
df_hm |>
  ggplot(aes(tipo_vehiculo, edad_grupo, fill = mediana)) +
  geom_tile(color = "white") +
  scale_fill_viridis_c(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Heatmap de severidad mediana",
       x = "Tipo de vehículo", y = "Grupo de edad", fill = "Mediana")
# INTERPRETACIÓN: TODO
df |>
  group_by(region, tipo_vehiculo) |>
  summarise(lr = mean(loss_ratio), .groups = "drop") |>
  ggplot(aes(tipo_vehiculo, region, fill = lr)) +
  geom_tile(color = "white") +
  scale_fill_viridis_c() +
  labs(title = "Mapa de calor de loss ratio promedio",
       x = "Tipo de vehículo", y = "Región", fill = "LR")
# INTERPRETACIÓN: TODO
num_vars <- df |>
  transmute(edad, expo, reclamos, costo_total = log10(costo_total + 1), prima = log10(prima + 1), loss_ratio)
corr <- cor(num_vars, use = "pairwise.complete.obs")
tibble(
  var1 = rep(rownames(corr), times = ncol(corr)),
  var2 = rep(colnames(corr), each = nrow(corr)),
  r = as.vector(corr)
) |>
  ggplot(aes(var1, var2, fill = r)) +
  geom_tile(color = "white") +
  scale_fill_gradient2(limits = c(-1,1)) +
  coord_equal() +
  labs(title = "Mapa de calor de correlaciones", x = NULL, y = NULL, fill = "r")
# INTERPRETACIÓN: TODO
df |>
  arrange(desc(costo_total)) |>
  mutate(pct = costo_total / sum(costo_total),
         acum = cumsum(pct),
         idx = row_number()) |>
  ggplot(aes(idx, acum)) +
  geom_line() +
  geom_hline(yintercept = 0.8, linetype = "dashed") +
  labs(title = "Curva de Pareto: contribución acumulada del costo",
       x = "Pólizas ordenadas por costo", y = "Acumulado")
# INTERPRETACIÓN: TODO
p80 <- quantile(df$costo_total, 0.80)
df_tree <- df |>
  mutate(alto_costo = if_else(costo_total > p80, "SI","NO")) |>
  select(alto_costo, edad, region, tipo_vehiculo, expo, reclamos, prima)
set.seed(99)
fit <- rpart(alto_costo ~ ., data = df_tree, method = "class",
             control = rpart.control(cp = 0.01, minbucket = 100))
rpart.plot(fit, type = 2, extra = 104, box.palette = "RdYlGn", branch.lty = 2, shadow.col = "gray")
# INTERPRETACIÓN: ¿Qué splits aparecen? ¿Qué segmentos “empujan” a alto costo?
p1 <- df |>
  ggplot(aes(edad, costo_total, color = tipo_vehiculo)) +
  geom_point(alpha = 0.5) +
  scale_y_log10(labels = scales::label_number(scale_cut = scales::cut_si(" "))) +
  labs(title = "Interactivo: Edad vs Costo por tipo de vehículo",
       x = "Edad", y = "Costo total (log10)")
ggplotly(p1)
p2 <- df |>
  group_by(region, tipo_vehiculo) |>
  summarise(lr = mean(loss_ratio), .groups = "drop") |>
  ggplot(aes(tipo_vehiculo, region, fill = lr)) +
  geom_tile(color = "white") +
  labs(title = "Interactivo: Loss ratio promedio",
       x = "Tipo de vehículo", y = "Región", fill = "LR")
ggplotly(p2)
A partir del dataset df, cada uno de ustdes debe
elegir 3 gráficos diferentes entre los tipos listados
abajo. No vale repetir el mismo tipo cambiando
colores.
Para cada gráfico deben responder las preguntas interpretativas que aparecen debajo. Eso va en el PDF.
Gráfico: Severidad (costo_total) por
tipo_vehiculo facetado por anio.
df |>
  ggplot(aes(tipo_vehiculo, costo_total, fill = tipo_vehiculo)) +
  geom_boxplot(outlier.alpha = 0.2) +
  scale_y_log10(labels = label_number(scale_cut = cut_si(" "))) +
  facet_wrap(~ anio) +
  labs(title = "Severidad por tipo de vehículo y año", x = NULL, y = "Costo (log10)")
Preguntas a responder:
Gráfico: Severidad mediana por
tipo_vehiculo × grupo de edad.
df_hm <- df |>
  mutate(edad_grupo = cut(edad, breaks = c(18,25,35,45,55,65,80),
                          labels = c("18-25","26-35","36-45","46-55","56-65","66-80"),
                          include.lowest = TRUE)) |>
  group_by(edad_grupo, tipo_vehiculo) |>
  summarise(mediana = median(costo_total), .groups = "drop")
df_hm |>
  ggplot(aes(tipo_vehiculo, edad_grupo, fill = mediana)) +
  geom_tile(color = "white") +
  scale_fill_viridis_c(labels = label_number(scale_cut = cut_si(" "))) +
  labs(title = "Mapa de severidad mediana", x = "Tipo de vehículo", y = "Grupo de edad")
Preguntas a responder:
Gráfico: ¿Cuántas pólizas explican el 80% del costo total?
df |>
  arrange(desc(costo_total)) |>
  mutate(acum = cumsum(costo_total) / sum(costo_total),
         idx = row_number()) |>
  ggplot(aes(idx, acum)) +
  geom_line(color="steelblue") +
  geom_hline(yintercept = 0.8, linetype = "dashed") +
  labs(title = "Curva de Pareto del costo", x = "Pólizas ordenadas", y = "Acumulado")
Preguntas a responder:
Gráfico: prima vs
costo_total (log-log), coloreado por
loss_ratio > 1.
df |>
  ggplot(aes(prima, costo_total, color = loss_ratio > 1)) +
  geom_point(alpha = 0.5) +
  scale_x_log10(labels = label_number(scale_cut = cut_si(" "))) +
  scale_y_log10(labels = label_number(scale_cut = cut_si(" "))) +
  labs(color="Pérdida (>1)", title="Prima vs Costo: Pólizas rentables vs no rentables")
Preguntas a responder:
Gráfico: Árbol para identificar perfil de alto costo.
p80 <- quantile(df$costo_total, 0.80)
df_tree <- df |>
  mutate(alto_costo = if_else(costo_total > p80, "SI","NO"))
fit <- rpart(alto_costo ~ edad + region + tipo_vehiculo + expo + reclamos + prima,
             data=df_tree, method="class", control=rpart.control(cp=0.01))
rpart.plot(fit, type = 2, extra = 104)
Preguntas a responder:
Un documento PDF generado desde RMarkdown.
Contiene:
Nombre del archivo:
Visualizacion_Actuarial_ApellidoNombre.pdf