knitr::opts_chunk$set(
echo = TRUE,
warning = FALSE,
message = FALSE,
fig.width = 8.5,
fig.height = 5.2,
dpi = 120
)
options(scipen = 999)
paquetes <- c("dplyr", "tidyr", "ggplot2", "broom", "scales")
faltantes <- paquetes[
!vapply(paquetes, requireNamespace, logical(1), quietly = TRUE)
]
if (length(faltantes) > 0) {
stop(
paste0(
"Instale antes de tejer el documento: install.packages(c(",
paste0('"', faltantes, '"', collapse = ", "),
"))"
)
)
}
invisible(lapply(paquetes, library, character.only = TRUE))##
## Adjuntando el paquete: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
simular_caso_unidad_1 <- function(n_polizas = 6000, seed = 870) {
set.seed(seed)
id_poliza <- sprintf("POL-%05d", seq_len(n_polizas))
edad <- sample(18:82, n_polizas, replace = TRUE)
zona <- sample(
c("Andina", "Caribe", "Pacífica", "Orinoquía"),
n_polizas,
replace = TRUE,
prob = c(0.52, 0.22, 0.18, 0.08)
)
uso <- sample(
c("Particular", "Comercial"),
n_polizas,
replace = TRUE,
prob = c(0.82, 0.18)
)
tipo_vehiculo <- sample(
c("Automóvil", "Camioneta", "Motocicleta"),
n_polizas,
replace = TRUE,
prob = c(0.58, 0.25, 0.17)
)
canal <- sample(
c("Agencia", "Digital", "Corredor"),
n_polizas,
replace = TRUE,
prob = c(0.38, 0.32, 0.30)
)
antiguedad_vehiculo <- pmin(25, rpois(n_polizas, 7))
fecha_inicio <- as.Date("2025-01-01") +
sample(0:334, n_polizas, replace = TRUE)
exposicion <- round(runif(n_polizas, 0.25, 1), 3)
fecha_fin <- fecha_inicio + pmax(30, round(365 * exposicion) - 1)
suma_asegurada <- round(
exp(
rnorm(
n_polizas,
log(65000000) +
0.28 * (tipo_vehiculo == "Camioneta") -
0.38 * (tipo_vehiculo == "Motocicleta"),
0.30
)
),
-3
)
eta_frecuencia <- -2.40 +
0.38 * (edad < 25) +
0.20 * (edad >= 65) +
0.32 * (uso == "Comercial") +
0.28 * (tipo_vehiculo == "Motocicleta") +
0.17 * (zona == "Caribe") +
0.012 * antiguedad_vehiculo
frecuencia_anual <- exp(eta_frecuencia)
n_siniestros <- rpois(
n_polizas,
lambda = frecuencia_anual * exposicion
)
indice_poliza <- rep(seq_len(n_polizas), n_siniestros)
total_siniestros <- length(indice_poliza)
if (total_siniestros > 0) {
media_severidad <- exp(
log(2800000) +
0.30 * (tipo_vehiculo[indice_poliza] == "Camioneta") +
0.22 * (zona[indice_poliza] == "Caribe") +
0.015 * antiguedad_vehiculo[indice_poliza]
)
costo_siniestro <- rgamma(
total_siniestros,
shape = 2.3,
scale = media_severidad / 2.3
)
dias_hasta_siniestro <- floor(
runif(
total_siniestros,
0,
pmax(1, 365 * exposicion[indice_poliza])
)
)
siniestros <- data.frame(
id_siniestro = sprintf("SIN-%06d", seq_len(total_siniestros)),
id_poliza = id_poliza[indice_poliza],
fecha_siniestro =
fecha_inicio[indice_poliza] + dias_hasta_siniestro,
tipo_dano = sample(
c("Daño parcial", "Pérdida total", "Responsabilidad civil"),
total_siniestros,
replace = TRUE,
prob = c(0.72, 0.08, 0.20)
),
costo_siniestro = round(costo_siniestro, 0),
stringsAsFactors = FALSE
)
} else {
siniestros <- data.frame(
id_siniestro = character(),
id_poliza = character(),
fecha_siniestro = as.Date(character()),
tipo_dano = character(),
costo_siniestro = numeric()
)
}
costo_total <- numeric(n_polizas)
if (nrow(siniestros) > 0) {
costos_agregados <- tapply(
siniestros$costo_siniestro,
siniestros$id_poliza,
sum
)
costo_total[match(names(costos_agregados), id_poliza)] <-
as.numeric(costos_agregados)
}
severidad_teorica <- exp(
log(2800000) +
0.30 * (tipo_vehiculo == "Camioneta") +
0.22 * (zona == "Caribe") +
0.015 * antiguedad_vehiculo
)
prima_pura_teorica <- frecuencia_anual * severidad_teorica
prima_comercial_anual <- prima_pura_teorica / 0.70
polizas <- data.frame(
id_poliza,
fecha_inicio,
fecha_fin,
exposicion,
edad,
zona = factor(zona),
uso = factor(uso),
tipo_vehiculo = factor(tipo_vehiculo),
canal = factor(canal),
antiguedad_vehiculo,
suma_asegurada,
n_siniestros,
costo_total,
prima_devengada = round(
prima_comercial_anual * exposicion,
0
),
stringsAsFactors = FALSE
)
polizas_crudas <- polizas
names(polizas_crudas)[
names(polizas_crudas) == "id_poliza"
] <- "Id Póliza"
names(polizas_crudas)[
names(polizas_crudas) == "fecha_inicio"
] <- "Fecha Inicio"
names(polizas_crudas)[
names(polizas_crudas) == "suma_asegurada"
] <- "Suma Asegurada"
polizas_crudas[["Fecha Inicio"]] <- format(
polizas_crudas[["Fecha Inicio"]],
"%d/%m/%Y"
)
zona_cruda <- as.character(polizas_crudas$zona)
idx_zona <- sample(seq_len(n_polizas), 35)
zona_cruda[idx_zona] <- sample(
c("andina", "Caribe ", "PACIFICA", "orinoquia"),
length(idx_zona),
replace = TRUE
)
polizas_crudas$zona <- zona_cruda
idx_edad <- sample(seq_len(n_polizas), 7)
polizas_crudas$edad[idx_edad] <-
c(-4, 12, 104, 150, NA, 999, 7)
idx_exposicion <- sample(
setdiff(seq_len(n_polizas), idx_edad),
6
)
polizas_crudas$exposicion[idx_exposicion] <-
c(-0.2, 0, 1.35, NA, 2, -1)
idx_suma <- sample(seq_len(n_polizas), round(0.025 * n_polizas))
polizas_crudas[["Suma Asegurada"]][idx_suma] <- NA
duplicadas <- sample(seq_len(n_polizas), 12)
polizas_crudas <- rbind(
polizas_crudas,
polizas_crudas[duplicadas, , drop = FALSE]
)
if (nrow(siniestros) > 0) {
n_sin <- nrow(siniestros)
fecha_reporte <- siniestros$fecha_siniestro +
pmax(0, round(rexp(n_sin, rate = 1 / 2)))
fecha_revision <- fecha_reporte +
pmax(1, round(rexp(n_sin, rate = 1 / 3)))
fecha_documentos <- fecha_revision +
pmax(1, round(rexp(n_sin, rate = 1 / 5)))
fecha_decision <- fecha_documentos +
pmax(1, round(rexp(n_sin, rate = 1 / 4)))
fecha_cierre <- fecha_decision +
pmax(1, round(rexp(n_sin, rate = 1 / 6)))
registro_procesos <- rbind(
data.frame(
id_caso = siniestros$id_siniestro,
etapa = "Reporte",
fecha_evento = fecha_reporte
),
data.frame(
id_caso = siniestros$id_siniestro,
etapa = "Revisión inicial",
fecha_evento = fecha_revision
),
data.frame(
id_caso = siniestros$id_siniestro,
etapa = "Documentación",
fecha_evento = fecha_documentos
),
data.frame(
id_caso = siniestros$id_siniestro,
etapa = "Decisión",
fecha_evento = fecha_decision
),
data.frame(
id_caso = siniestros$id_siniestro,
etapa = "Cierre",
fecha_evento = fecha_cierre
)
)
} else {
registro_procesos <- data.frame(
id_caso = character(),
etapa = character(),
fecha_evento = as.Date(character())
)
}
list(
polizas = polizas,
polizas_crudas = polizas_crudas,
siniestros = siniestros,
registro_procesos = registro_procesos
)
}
caso <- simular_caso_unidad_1()Unidad I · Electiva I: Análisis de datos en R
¿Cómo transformar datos operativos en evidencia actuarial confiable, recorrer las etapas de la analítica, diagnosticar datos y procesos, y organizar el trabajo mediante una metodología reproducible?
Esta publicación funciona como un pequeño libro. Los capítulos se leen en orden porque comparten una cartera simulada de seguros de automóviles. El código está completo en los ejemplos; la tarea principal del estudiante es interpretar, verificar supuestos y justificar decisiones.
Instale una sola vez:
La publicación genera cuatro tablas:
| Tabla | Grano | Uso |
|---|---|---|
polizas |
Una fila por póliza | Exposición, prima y experiencia |
polizas_crudas |
Una fila operativa, con errores intencionales | Transformación y diagnóstico |
siniestros |
Una fila por siniestro | Severidad e integración |
registro_procesos |
Una fila por evento | Tiempos y cuellos de botella |
## $polizas
## [1] 6000 14
##
## $polizas_crudas
## [1] 6012 14
##
## $siniestros
## [1] 466 5
##
## $registro_procesos
## [1] 2330 3
Datos simulados. Los datos fueron creados con fines pedagógicos. No representan la experiencia de una compañía real ni deben usarse para tomar decisiones comerciales.
Al finalizar, el estudiante podrá:
explicar por qué transformar datos es una metodología y no una colección de comandos aislados;
declarar el grano, las llaves y los controles de una base;
diferenciar corrección, estandarización, derivación, integración e imputación;
construir una tabla analítica sin alterar exposición, siniestros ni costos.
Transformar datos consiste en convertir información capturada para operar un negocio en información adecuada para responder una pregunta analítica.
Un sistema de pólizas se diseña para emitir contratos. Un sistema de siniestros se diseña para gestionar reclamaciones. Un sistema de pagos se diseña para registrar movimientos. Ninguno fue creado específicamente para estimar frecuencia, severidad, reservas o primas. El análisis exige integrar esos registros sin destruir su significado.
Transformar no significa maquillar. Un dato no se corrige porque “se ve raro”. Toda modificación debe obedecer a una regla, conservar el valor original cuando sea posible y dejar una evidencia de lo realizado.
Antes del primer mutate() se debe responder:
| Pregunta | Ejemplo actuarial |
|---|---|
| ¿Cuál es el objetivo? | Estimar costo anual esperado por perfil |
| ¿Qué representa una fila? | Una póliza observada durante un periodo |
| ¿Cuál es la llave? | id_poliza |
| ¿Cuál es el periodo? | Año calendario 2025 |
| ¿Cuál es el denominador? | Años-póliza expuestos |
| ¿Qué universo se incluye? | Pólizas vigentes o expuestas durante el periodo |
| ¿Qué controles se conservarán? | Filas, exposición, siniestros, costo y prima |
Sin este contrato, una unión puede ejecutar correctamente y producir una cifra incorrecta. R no sabe si una fila representa una póliza, un asegurado, un siniestro o un pago. Ese significado debe declararlo el analista.
La misma fuente puede producir distintas bases:
Antes de modificar:
Una regla de validación compara el dato con una expectativa explícita:
Estandarizar unifica representación: andina,
Andina y ANDINA pasan a una misma categoría.
Derivar crea información nueva: días cubiertos, exposición, frecuencia o
prima pura.
La integración une fuentes con una cardinalidad esperada. La reconciliación verifica que los totales importantes no cambiaron accidentalmente.
El producto no es únicamente la tabla final. También incluye reglas, registros rechazados, controles y decisiones.
Una aseguradora de automóviles tiene:
Si se unen directamente pólizas, siniestros y pagos, la exposición de una póliza aparecerá tantas veces como pagos tenga. Dividir siniestros entre esa exposición multiplicada reduciría artificialmente la frecuencia.
La solución depende del objetivo:
Decisión metodológica. No existe una “base maestra” universal. Existe una base apropiada para una pregunta y un grano determinados.
cartera_cruda <- caso$polizas_crudas |>
as_tibble() |>
mutate(fila_origen = row_number())
dim(cartera_cruda)## [1] 6012 15
## [1] "Id Póliza" "Fecha Inicio" "fecha_fin"
## [4] "exposicion" "edad" "zona"
## [7] "uso" "tipo_vehiculo" "canal"
## [10] "antiguedad_vehiculo" "Suma Asegurada" "n_siniestros"
## [13] "costo_total" "prima_devengada" "fila_origen"
## Id Póliza Fecha Inicio fecha_fin exposicion
## Length:6012 Length:6012 Min. :2025-04-04 Min. :-1.0000
## Class :character Class :character 1st Qu.:2025-10-31 1st Qu.: 0.4390
## Mode :character Mode :character Median :2026-01-30 Median : 0.6250
## Mean :2026-01-28 Mean : 0.6245
## 3rd Qu.:2026-04-29 3rd Qu.: 0.8110
## Max. :2026-11-25 Max. : 2.0000
## NA's :1
## edad zona uso tipo_vehiculo
## Min. : -4.00 Length:6012 Comercial :1117 Automóvil :3486
## 1st Qu.: 33.00 Class :character Particular:4895 Camioneta :1542
## Median : 50.00 Mode :character Motocicleta: 984
## Mean : 50.04
## 3rd Qu.: 66.00
## Max. :999.00
## NA's :1
## canal antiguedad_vehiculo Suma Asegurada n_siniestros
## Agencia :2257 Min. : 0.000 Min. : 18417000 Min. :0.00000
## Corredor:1773 1st Qu.: 5.000 1st Qu.: 51683250 1st Qu.:0.00000
## Digital :1982 Median : 7.000 Median : 66415000 Median :0.00000
## Mean : 7.005 Mean : 70641235 Mean :0.07751
## 3rd Qu.: 9.000 3rd Qu.: 84978750 3rd Qu.:0.00000
## Max. :18.000 Max. :243535000 Max. :4.00000
## NA's :150
## costo_total prima_devengada fila_origen
## Min. : 0 Min. : 98372 Min. : 1
## 1st Qu.: 0 1st Qu.: 269275 1st Qu.:1504
## Median : 0 Median : 379030 Median :3006
## Mean : 281085 Mean : 413858 Mean :3006
## 3rd Qu.: 0 3rd Qu.: 517926 3rd Qu.:4509
## Max. :23080875 Max. :1542989 Max. :6012
##
evaluada <- cartera_cruda |>
rename(
id_poliza = `Id Póliza`,
fecha_inicio_texto = `Fecha Inicio`,
suma_asegurada = `Suma Asegurada`
) |>
mutate(
fecha_inicio = as.Date(
fecha_inicio_texto,
format = "%d/%m/%Y"
),
zona_original = as.character(zona),
zona_normalizada = trimws(tolower(zona_original)),
zona = case_when(
zona_normalizada == "andina" ~ "Andina",
zona_normalizada == "caribe" ~ "Caribe",
zona_normalizada %in% c("pacífica", "pacifica") ~ "Pacífica",
zona_normalizada %in% c("orinoquía", "orinoquia") ~ "Orinoquía",
TRUE ~ NA_character_
)
)
evaluada |>
count(zona_original, zona, sort = TRUE) |>
slice_head(n = 10)evaluada <- evaluada |>
mutate(
id_duplicado = duplicated(id_poliza) |
duplicated(id_poliza, fromLast = TRUE),
falla_id = is.na(id_poliza) | id_poliza == "",
falla_fecha = is.na(fecha_inicio),
falla_edad = is.na(edad) | edad < 18 | edad > 100,
falla_exposicion = is.na(exposicion) |
exposicion <= 0 | exposicion > 1,
falla_suma = is.na(suma_asegurada) |
suma_asegurada <= 0,
falla_zona = is.na(zona),
n_fallas = rowSums(
cbind(
falla_id,
falla_fecha,
falla_edad,
falla_exposicion,
falla_suma,
falla_zona,
id_duplicado
)
),
estado_registro = if_else(
n_fallas == 0,
"APROBADO",
"CUARENTENA"
)
)
evaluada |>
count(estado_registro)polizas_analiticas <- evaluada |>
filter(estado_registro == "APROBADO") |>
select(
id_poliza,
fecha_inicio,
fecha_fin,
exposicion,
edad,
zona,
uso,
tipo_vehiculo,
canal,
antiguedad_vehiculo,
suma_asegurada,
n_siniestros,
costo_total,
prima_devengada
)
cuarentena <- evaluada |>
filter(estado_registro == "CUARENTENA")
data.frame(
registros_fuente = nrow(cartera_cruda),
registros_aprobados = nrow(polizas_analiticas),
registros_cuarentena = nrow(cuarentena),
porcentaje_cuarentena =
nrow(cuarentena) / nrow(cartera_cruda)
)Interpretación. La cuarentena conserva los casos que requieren investigación. Eliminarlos sin reporte impediría medir el impacto de la calidad y podría sesgar el análisis si los errores se concentran en ciertos segmentos.
siniestros <- caso$siniestros |>
as_tibble()
duplicados_poliza <- polizas_analiticas |>
count(id_poliza) |>
filter(n > 1)
siniestros_huerfanos <- siniestros |>
anti_join(
polizas_analiticas,
by = "id_poliza"
)
duplicados_polizasiniestros_por_poliza <- siniestros |>
semi_join(
polizas_analiticas,
by = "id_poliza"
) |>
group_by(id_poliza) |>
summarise(
n_siniestros_calculado = n(),
costo_calculado = sum(costo_siniestro),
.groups = "drop"
)
base_maestra <- polizas_analiticas |>
select(
-n_siniestros,
-costo_total
) |>
left_join(
siniestros_por_poliza,
by = "id_poliza"
) |>
mutate(
n_siniestros = coalesce(
n_siniestros_calculado,
0L
),
costo_total = coalesce(
costo_calculado,
0
)
)controles_integracion <- tibble(
control = c(
"Filas antes",
"Filas después",
"Exposición antes",
"Exposición después",
"Costo en siniestros válidos",
"Costo en base maestra"
),
valor = c(
nrow(polizas_analiticas),
nrow(base_maestra),
sum(polizas_analiticas$exposicion),
sum(base_maestra$exposicion),
sum(
siniestros_por_poliza$costo_calculado
),
sum(base_maestra$costo_total)
)
)
controles_integracionConclusión. La unión es aceptable si mantiene una fila por póliza, conserva la exposición y reconcilia el costo del universo incluido.
Sobrescribir la variable original sin conservar evidencia.
Convertir identificadores en números y perder ceros iniciales.
Sustituir todos los faltantes por cero.
Eliminar valores extremos solo porque cambian el promedio.
Unir tablas sin declarar cardinalidad.
Reportar una base “limpia” sin informar exclusiones.
Un fondo desea construir una tabla con una fila por afiliado para estudiar continuidad de aportes. La tabla de movimientos puede contener varios meses por persona, valores negativos y afiliados que no aparecen en el maestro.
set.seed(110)
afiliados <- tibble(
id_afiliado = sprintf("A%04d", 1:800),
edad = sample(20:69, 800, replace = TRUE),
ingreso_base = round(
rlnorm(800, log(3200000), 0.45),
-3
)
)
aportes <- expand_grid(
id_afiliado = afiliados$id_afiliado,
mes = as.Date(c(
"2025-01-31",
"2025-02-28",
"2025-03-31"
))
) |>
sample_frac(0.82) |>
left_join(afiliados, by = "id_afiliado") |>
mutate(
aporte = round(ingreso_base * 0.16, -2)
) |>
select(-edad, -ingreso_base)
aportes$aporte[sample(seq_len(nrow(aportes)), 8)] <-
c(rep(-200000, 4), rep(NA, 4))
aportes <- bind_rows(
aportes,
tibble(
id_afiliado = c("A9998", "A9999"),
mes = as.Date(c("2025-02-28", "2025-03-31")),
aporte = c(480000, 520000)
)
)afiliados, aportes y
de la tabla final.Al finalizar, el estudiante podrá:
| Etapa | Pregunta | Producto actuarial | Riesgo de interpretación |
|---|---|---|---|
| Descriptiva | ¿Qué ocurrió? | Frecuencia, severidad, siniestralidad, mortalidad observada | Confundir asociación con causa |
| Predictiva | ¿Qué podría ocurrir condicionalmente? | Frecuencia esperada, probabilidad de cancelación, costo esperado | Suponer que el futuro repetirá el pasado |
| Prescriptiva | ¿Qué acción conviene bajo restricciones? | Revisión, tarifa, priorización, estrategia de retención | Automatizar una decisión sin gobernanza |
| Prospectiva | ¿Qué pasaría bajo escenarios? | Estrés de inflación, cambios regulatorios o climáticos | Presentar un escenario como pronóstico |
Las etapas no compiten. Se complementan. Una recomendación responsable necesita describir la evidencia, anticipar resultados, comparar acciones y estudiar escenarios.
La analítica descriptiva resume la experiencia observada:
\[ \text{Frecuencia} = \frac{\text{Número de siniestros}} {\text{Años-póliza expuestos}} \]
\[ \text{Severidad} = \frac{\text{Costo de siniestros}} {\text{Número de siniestros}} \]
\[ \text{Prima pura observada} = \frac{\text{Costo de siniestros}} {\text{Años-póliza expuestos}} \]
Estas medidas describen el periodo y el universo seleccionados. No garantizan que el siguiente año sea igual.
Un modelo predictivo estima una respuesta no observada utilizando relaciones aprendidas en datos históricos. En seguros de automóviles puede estimar:
número esperado de siniestros;
costo esperado de un siniestro;
probabilidad de renovación;
probabilidad de reporte tardío.
La predicción es condicional: depende del perfil, la exposición, los datos, el periodo y los supuestos del modelo.
La analítica prescriptiva compara acciones. Una probabilidad alta de cancelación no ordena, por sí sola, ofrecer un descuento. La acción depende de:
costo de la intervención;
capacidad operativa;
valor esperado de retener la póliza;
equidad y restricciones jurídicas;
incertidumbre del modelo;
revisión humana.
La prospectiva no intenta adivinar un único futuro. Construye futuros plausibles y pregunta si la decisión continúa siendo razonable.
Ejemplos:
inflación de reparación de 8%, 12% o 20%;
aumento de frecuencia por cambio en movilidad;
reducción de mortalidad;
incremento de cancelación por choque económico;
cambio en el régimen de aportes.
Una compañía observa que el 12% de sus pólizas de vida no se renueva.
El modelo no demuestra que llamar al asegurado evite su cancelación. Esa relación debe estudiarse mediante experimentación o diseños causales.
resumen_segmentos <- caso$polizas |>
as_tibble() |>
group_by(tipo_vehiculo, uso) |>
summarise(
polizas = n(),
exposicion = sum(exposicion),
siniestros = sum(n_siniestros),
costo = sum(costo_total),
prima = sum(prima_devengada),
frecuencia = siniestros / exposicion,
severidad = if_else(
siniestros > 0,
costo / siniestros,
NA_real_
),
prima_pura = costo / exposicion,
siniestralidad = costo / prima,
.groups = "drop"
)
resumen_segmentos |>
arrange(desc(prima_pura))ggplot(
resumen_segmentos,
aes(
x = frecuencia,
y = severidad,
size = exposicion,
color = tipo_vehiculo
)
) +
geom_point(alpha = 0.78) +
scale_y_continuous(
labels = label_number(scale = 1e-6, suffix = " M")
) +
labs(
title = "Frecuencia, severidad y volumen",
subtitle = "El tamaño representa años-póliza expuestos",
x = "Frecuencia anual",
y = "Severidad media (millones de COP)",
color = "Vehículo",
size = "Exposición"
) +
theme_minimal()Lectura. Un segmento puede tener prima pura alta por frecuencia, por severidad o por ambas. La estrategia de prevención, suscripción o reaseguro no sería necesariamente la misma.
Se ajusta un GLM Poisson con offset de exposición. En
esta unidad el objetivo es comprender el flujo; el diagnóstico formal
del modelo se profundizará más adelante.
datos_modelo <- caso$polizas |>
as_tibble()
modelo_frecuencia <- glm(
n_siniestros ~
uso +
tipo_vehiculo +
zona +
edad +
antiguedad_vehiculo,
family = poisson(link = "log"),
offset = log(exposicion),
data = datos_modelo
)
datos_modelo <- datos_modelo |>
mutate(
siniestros_esperados = predict(
modelo_frecuencia,
type = "response"
),
frecuencia_esperada =
siniestros_esperados / exposicion
)
tidy(modelo_frecuencia, exponentiate = TRUE) |>
select(term, estimate, std.error, p.value)Las exponenciales de los coeficientes se interpretan como relatividades condicionadas al resto de variables. No son efectos causales.
Se calcula una prima técnica ilustrativa y se propone revisión, no una decisión automática.
severidad_cartera <- with(
datos_modelo,
sum(costo_total) / sum(n_siniestros)
)
datos_decision <- datos_modelo |>
mutate(
prima_pura_esperada =
frecuencia_esperada * severidad_cartera,
prima_tecnica_anual =
(prima_pura_esperada + 70000) / (1 - 0.28),
prima_anual_actual =
prima_devengada / exposicion,
brecha_relativa =
prima_tecnica_anual / prima_anual_actual - 1,
accion = case_when(
brecha_relativa > 0.20 ~ "Revisión técnica prioritaria",
brecha_relativa < -0.20 ~ "Revisar competitividad",
TRUE ~ "Mantener y monitorear"
)
)
datos_decision |>
count(accion) |>
mutate(porcentaje = n / sum(n))Límite. La regla organiza el trabajo del equipo. No autoriza rechazo, cancelación ni cambio de tarifa sin validación, gobernanza y revisión de restricciones aplicables.
escenarios <- expand_grid(
inflacion_severidad = c(0, 0.10, 0.20),
cambio_frecuencia = c(-0.05, 0, 0.08)
) |>
mutate(
nombre = paste0(
"Severidad ",
percent(inflacion_severidad),
" | Frecuencia ",
percent(cambio_frecuencia)
),
costo_esperado = sum(
datos_modelo$siniestros_esperados
) *
severidad_cartera *
(1 + inflacion_severidad) *
(1 + cambio_frecuencia),
prima_devengada = sum(
datos_modelo$prima_devengada
),
siniestralidad_esperada =
costo_esperado / prima_devengada
)
escenarios |>
arrange(desc(siniestralidad_esperada))Un escenario de 20% de inflación no es una predicción. Es una prueba de resistencia que pregunta qué tan vulnerable es la suficiencia.
set.seed(220)
n_vida <- 5000
vida <- tibble(
id_poliza = sprintf("VID-%05d", seq_len(n_vida)),
antiguedad = runif(n_vida, 0.1, 12),
mora_dias = pmax(0, round(rnorm(n_vida, 12, 18))),
canal = sample(
c("Agencia", "Digital", "Corredor"),
n_vida,
replace = TRUE,
prob = c(0.40, 0.35, 0.25)
),
prima_mensual = round(
rlnorm(n_vida, log(180000), 0.45),
-2
)
) |>
mutate(
prob_cancelar = plogis(
-3.4 -
0.08 * antiguedad +
0.035 * mora_dias +
0.35 * (canal == "Digital")
),
cancela = rbinom(
n(),
size = 1,
prob = prob_cancelar
)
)vida |>
group_by(canal) |>
summarise(
polizas = n(),
tasa_cancelacion = mean(cancela),
mora_media = mean(mora_dias),
.groups = "drop"
)El equipo solo puede contactar al 10% de la cartera.
umbral_contacto <- quantile(
vida$prob_estimada,
0.90
)
grupo_contacto <- vida |>
filter(prob_estimada >= umbral_contacto)
resumen_contacto <- grupo_contacto |>
summarise(
polizas_contactadas = n(),
cancelaciones_esperadas = sum(prob_estimada),
prima_mensual_en_riesgo =
sum(prob_estimada * prima_mensual)
)
resumen_contactoefectividad <- c(0.05, 0.15, 0.30)
escenarios_retencion <- tibble(
efectividad = efectividad,
cancelaciones_evitar = as.numeric(
resumen_contacto$cancelaciones_esperadas
) * efectividad,
prima_mensual_preservada = as.numeric(
resumen_contacto$prima_mensual_en_riesgo
) * efectividad
)
escenarios_retencionLectura. La efectividad de la campaña es un supuesto, no una conclusión del modelo de cancelación. Para estimar el efecto causal del contacto se necesitaría un diseño experimental o cuasiexperimental.
set.seed(230)
n_salud <- 3500
salud <- tibble(
id = sprintf("SAL-%05d", seq_len(n_salud)),
edad = sample(18:85, n_salud, replace = TRUE),
cronico = rbinom(n_salud, 1, 0.24),
region = sample(
c("Centro", "Norte", "Occidente"),
n_salud,
replace = TRUE
),
exposicion = runif(n_salud, 0.5, 1)
) |>
mutate(
consultas = rpois(
n(),
exp(
-0.2 +
0.018 * edad +
0.70 * cronico
) * exposicion
),
costo = consultas * rgamma(
n(),
shape = 3,
scale = 90000
)
)Descriptiva: compare consultas por año-persona y costo por afiliado entre personas con y sin condición crónica.
Predictiva: ajuste un modelo Poisson para
consultas utilizando exposición, edad, condición crónica y
región.
Prescriptiva: proponga una regla de priorización para revisión clínica, sin negar cobertura ni sustituir criterio médico.
Prospectiva: evalúe una inflación médica de 8%, 15% y 25%.
Explique qué afirmaciones puede sostener y cuáles no.
Una tabla, un gráfico, una interpretación de máximo 200 palabras y una lista de supuestos.
Al finalizar, el estudiante podrá:
Se revisa si la información es apropiada para el propósito:
Se estudian:
Se estudia cómo ocurrió la operación:
El registro de procesos añade una dimensión que una tabla final suele ocultar: el tiempo.
Una suma asegurada faltante puede ser crítica para severidad, pero irrelevante para contar pólizas por canal. Una fecha de pago faltante puede impedir el análisis de flujo de caja, aunque el costo total del siniestro esté completo.
El ASOP No. 23 orienta al actuario a considerar selección, revisión, uso, limitaciones, dependencia de terceros y comunicación de deficiencias de los datos. La meta no es declarar que una base es “perfecta”, sino determinar si resulta apropiada y suficiente para la tarea.
La calidad no es un porcentaje universal. Una base puede ser apta para un informe descriptivo y no ser apta para estimar una tarifa o una reserva.
| Dimensión | Regla | Ejemplo de decisión |
|---|---|---|
| Completitud | Suma asegurada no faltante | Condiciona modelo de severidad |
| Validez | Exposición en (0,1] | Bloquea frecuencia |
| Unicidad | Una fila por póliza | Bloquea unión |
| Integridad | Todo siniestro tiene póliza | Aísla huérfanos |
| Temporalidad | Siniestro dentro de cobertura | Revisión del caso |
| Reconciliación | Costo agregado coincide | Bloquea publicación |
| Estabilidad | Distribución comparable con periodo previo | Genera alerta |
Una compañía observa un aumento de la reserva de siniestros.
Un diagnóstico estadístico pregunta:
Un diagnóstico de proceso pregunta:
La misma variación contable puede deberse a riesgo, operación, calendario, registro o una combinación de los cuatro.
Se utiliza la tabla evaluada construida en el capítulo
1.
tablero_calidad <- evaluada |>
summarise(
registros = n(),
id_faltante = sum(falla_id),
id_duplicado = sum(id_duplicado),
fecha_invalida = sum(falla_fecha),
edad_invalida = sum(falla_edad),
exposicion_invalida = sum(falla_exposicion),
suma_invalida = sum(falla_suma),
zona_invalida = sum(falla_zona),
registros_con_alguna_falla =
sum(n_fallas > 0)
) |>
pivot_longer(
everything(),
names_to = "indicador",
values_to = "valor"
)
tablero_calidadLos conteos por regla no deben sumarse como si fueran registros diferentes: una fila puede fallar varias reglas.
impacto_calidad <- evaluada |>
group_by(estado_registro) |>
summarise(
registros = n(),
exposicion_registrada = sum(
exposicion,
na.rm = TRUE
),
prima_registrada = sum(
prima_devengada,
na.rm = TRUE
),
.groups = "drop"
)
impacto_calidadInterpretación. No basta con decir cuántas filas fallan. Debe medirse cuánta exposición, prima y costo representan. Un 1% de registros puede ser material si concentra una parte grande del riesgo.
caso$polizas |>
as_tibble() |>
summarise(
polizas = n(),
exposicion = sum(exposicion),
media_edad = mean(edad),
frecuencia =
sum(n_siniestros) / sum(exposicion),
severidad =
sum(costo_total) / sum(n_siniestros),
prima_pura =
sum(costo_total) / sum(exposicion)
)diagnostico_severidad <- caso$siniestros |>
as_tibble() |>
summarise(
siniestros = n(),
media = mean(costo_siniestro),
mediana = median(costo_siniestro),
p90 = quantile(costo_siniestro, 0.90),
p99 = quantile(costo_siniestro, 0.99),
maximo = max(costo_siniestro)
)
diagnostico_severidadggplot(
caso$siniestros,
aes(x = costo_siniestro)
) +
geom_histogram(
bins = 45,
fill = "#235789",
color = "white"
) +
scale_x_continuous(
labels = label_number(scale = 1e-6, suffix = " M")
) +
labs(
title = "Distribución del costo de siniestros",
subtitle = "La asimetría es parte del fenómeno asegurador",
x = "Costo (millones de COP)",
y = "Número de siniestros"
) +
theme_minimal()Valor extremo no significa error. Un siniestro grande válido puede ser precisamente el riesgo que la tarifa, la reserva y el reaseguro deben capturar.
p99_costo <- quantile(
caso$siniestros$costo_siniestro,
0.99
)
tibble(
medida = c(
"Media observada",
"Media winsorizada al p99",
"Media excluyendo el 1% superior"
),
valor = c(
mean(caso$siniestros$costo_siniestro),
mean(
pmin(
caso$siniestros$costo_siniestro,
p99_costo
)
),
mean(
caso$siniestros$costo_siniestro[
caso$siniestros$costo_siniestro <= p99_costo
]
)
)
)Las tres cifras responden a universos o reglas diferentes. Deben presentarse como sensibilidad, no como permiso para escoger la que más convenga.
reconciliacion <- tibble(
fuente = c(
"Siniestros individuales",
"Costo agregado por póliza",
"Número individual",
"Número agregado por póliza"
),
valor = c(
sum(caso$siniestros$costo_siniestro),
sum(caso$polizas$costo_total),
nrow(caso$siniestros),
sum(caso$polizas$n_siniestros)
)
)
reconciliacionUna diferencia no siempre es un error: puede deberse a cortes temporales, recuperaciones, reaperturas o universos distintos. Pero debe explicarse.
Un registro de eventos debería contener, al menos:
identificador de caso;
nombre del evento;
marca de tiempo;
responsable o sistema, cuando esté disponible.
orden_etapas <- c(
"Reporte",
"Revisión inicial",
"Documentación",
"Decisión",
"Cierre"
)
eventos <- caso$registro_procesos |>
as_tibble() |>
mutate(
etapa = factor(
etapa,
levels = orden_etapas
)
) |>
arrange(id_caso, etapa) |>
group_by(id_caso) |>
mutate(
etapa_siguiente = lead(etapa),
fecha_siguiente = lead(fecha_evento),
dias_hasta_siguiente = as.numeric(
fecha_siguiente - fecha_evento
)
) |>
ungroup()
head(eventos, 10)tiempos_etapa <- eventos |>
filter(!is.na(dias_hasta_siguiente)) |>
group_by(etapa, etapa_siguiente) |>
summarise(
casos = n(),
mediana_dias = median(dias_hasta_siguiente),
p90_dias = quantile(
dias_hasta_siguiente,
0.90
),
maximo_dias = max(dias_hasta_siguiente),
.groups = "drop"
)
tiempos_etapaggplot(
tiempos_etapa,
aes(
x = etapa,
y = mediana_dias
)
) +
geom_col(fill = "#0F8B8D") +
geom_point(
aes(y = p90_dias),
color = "#C44536",
size = 3
) +
labs(
title = "Tiempo entre etapas del siniestro",
subtitle = "Barras: mediana | Puntos: percentil 90",
x = "Etapa de origen",
y = "Días hasta la siguiente etapa"
) +
theme_minimal()duracion_casos <- caso$registro_procesos |>
as_tibble() |>
group_by(id_caso) |>
summarise(
inicio = min(fecha_evento),
fin = max(fecha_evento),
duracion_total = as.numeric(fin - inicio),
eventos = n(),
.groups = "drop"
) |>
arrange(desc(duracion_total))
duracion_casos |>
slice_head(n = 10)Decisión posible. Los percentiles altos permiten priorizar una revisión del proceso. No demuestran por sí solos negligencia: un caso complejo puede requerir más tiempo de manera legítima.
set.seed(330)
reclamaciones_salud <- tibble(
id_reclamo = sprintf("R%04d", 1:1200),
id_afiliado = sprintf(
"A%04d",
sample(1:700, 1200, replace = TRUE)
),
fecha_servicio = as.Date("2025-01-01") +
sample(0:364, 1200, replace = TRUE),
fecha_reporte = fecha_servicio +
sample(0:40, 1200, replace = TRUE),
costo = round(
rgamma(1200, shape = 2, scale = 450000),
0
),
tipo = sample(
c("Consulta", "Urgencias", "Hospitalización"),
1200,
replace = TRUE,
prob = c(0.58, 0.27, 0.15)
)
)
reclamaciones_salud$costo[
sample(seq_len(nrow(reclamaciones_salud)), 5)
] <- c(-500000, NA, 0, 45000000, 80000000)
reclamaciones_salud <- bind_rows(
reclamaciones_salud,
reclamaciones_salud[
sample(seq_len(nrow(reclamaciones_salud)), 6),
]
)En los capítulos anteriores transformamos datos, distinguimos tipos de analítica y diagnosticamos una cartera. Falta una pregunta de gobierno: ¿cómo organizamos un proyecto para que el resultado sea útil, reproducible y defendible?
Al terminar este capítulo, el estudiante podrá:
Idea central. Una metodología no es una lista para marcar al final. Es un sistema de decisiones iterativo: un hallazgo en evaluación puede obligarnos a volver a la preparación de datos o incluso a redefinir el problema.
En algunos programas académicos se nombran CRISP y CRISP-DM por separado. Conviene hacer una precisión:
| Enfoque | Pregunta que enfatiza | Inicio típico | Resultado esperado |
|---|---|---|---|
| CRISP, como filosofía | ¿Cómo mantener el proyecto conectado con el problema? | Necesidad de decisión | Proceso iterativo y trazable |
| CRISP-DM | ¿Qué fases y entregables administran el proyecto? | Comprensión del negocio | Solución evaluada y desplegable |
| KDD | ¿Cómo convertir datos en conocimiento útil? | Selección de datos y objetivo de descubrimiento | Patrón evaluado e interpretado |
No son enemigos ni recetas incompatibles. Un equipo puede administrar el proyecto con CRISP-DM y, dentro de sus fases de datos y modelado, documentar un proceso KDD.
Se establece la decisión que será apoyada, la población, el horizonte, los costos de error, las restricciones y el criterio de éxito.
Ejemplo actuarial. “Estimar el número esperado de siniestros por póliza para revisar relatividades tarifarias” es mejor que “hacer un modelo de Poisson”. La primera frase declara la decisión; la segunda solo declara una técnica.
Se identifican fuentes, unidad de observación, cobertura temporal, diccionario, calidad, sesgos y relaciones preliminares. En seguros también se verifica si existe madurez suficiente de primas y siniestros.
Se seleccionan registros y variables, se corrigen formatos, se agregan tablas, se construyen exposiciones y se derivan predictores. Cada transformación debe ser reproducible y reconciliable.
Se eligen técnicas coherentes con la variable respuesta y con el uso.
Para conteos con distinta exposición, un GLM de Poisson con
offset(log(exposicion)) es un punto de partida
interpretable; no es automáticamente el modelo final.
Hay dos evaluaciones distintas:
El resultado puede ser una tarifa, un tablero, una regla de revisión o un lote de puntajes. Debe incluir versión, responsable, frecuencia, umbrales de alerta, protocolo de excepción y plan de retiro.
| Fase | Entregable actuarial mínimo | Pregunta de control |
|---|---|---|
| Negocio | ficha del problema y pérdidas por error | ¿Qué decisión cambia? |
| Datos | inventario, diccionario y diagnóstico | ¿La experiencia representa el riesgo? |
| Preparación | script y conciliación | ¿Se puede reproducir la base analítica? |
| Modelado | modelo base, candidato y supuestos | ¿La técnica corresponde a la respuesta? |
| Evaluación | validación técnica y juicio actuarial | ¿Funciona y además tiene sentido? |
| Despliegue | artefacto, monitoreo y responsables | ¿Quién actúa cuando el resultado cambia? |
Una aseguradora observa deterioro en la siniestralidad y solicita “usar inteligencia artificial para subir la tarifa”. CRISP-DM obliga a reformular:
Error frecuente. Un modelo que predice bien el costo observado no define por sí solo una tarifa. Gastos, margen, reaseguro, credibilidad, restricciones regulatorias y estrategia pertenecen a la decisión completa.
Decisión: identificar segmentos cuya frecuencia
esperada merece revisión técnica.
Unidad: póliza-periodo.
Respuesta: número de siniestros.
Exposición: fracción anual cubierta.
Criterio mínimo: calibración razonable en validación y
ordenamiento útil de riesgo, sin convertir el resultado automáticamente
en tarifa.
base_crisp <- caso$polizas |>
select(
id_poliza,
zona,
uso,
edad_conductor = edad,
antiguedad_vehiculo,
exposicion,
n_siniestros
) |>
mutate(
zona = factor(zona),
uso = factor(uso)
) |>
filter(
exposicion > 0,
!is.na(edad_conductor),
!is.na(antiguedad_vehiculo)
)
set.seed(410)
id_entrenamiento <- sample(
seq_len(nrow(base_crisp)),
size = floor(0.75 * nrow(base_crisp))
)
entrenamiento <- base_crisp[id_entrenamiento, ]
validacion <- base_crisp[-id_entrenamiento, ]
tibble(
muestra = c("Entrenamiento", "Validación"),
polizas = c(
nrow(entrenamiento),
nrow(validacion)
),
exposicion = c(
sum(entrenamiento$exposicion),
sum(validacion$exposicion)
),
siniestros = c(
sum(entrenamiento$n_siniestros),
sum(validacion$n_siniestros)
)
) |>
mutate(
frecuencia = siniestros / exposicion
)La partición ocurre antes del ajuste. La validación simula la llegada de observaciones no usadas para estimar parámetros.
Se ajustan dos modelos. El primero es una referencia sin segmentación; el segundo incorpora variables disponibles antes del periodo de cobertura.
modelo_base <- glm(
n_siniestros ~ 1 + offset(log(exposicion)),
family = poisson(link = "log"),
data = entrenamiento
)
modelo_candidato <- glm(
n_siniestros ~ zona + uso + edad_conductor +
antiguedad_vehiculo + offset(log(exposicion)),
family = poisson(link = "log"),
data = entrenamiento
)
coeficientes_crisp <- tidy(
modelo_candidato,
exponentiate = TRUE,
conf.int = TRUE
) |>
transmute(
termino = term,
relatividad = estimate,
limite_inferior = conf.low,
limite_superior = conf.high
)
coeficientes_crispLos coeficientes exponenciados son relatividades condicionadas al resto de las variables. Una relatividad de 1,15 se interpreta como una frecuencia esperada 15% mayor que la categoría de referencia, manteniendo constantes los demás predictores; no prueba causalidad.
validacion_evaluada <- validacion |>
mutate(
esperado_base = predict(
modelo_base,
newdata = validacion,
type = "response"
),
esperado_candidato = predict(
modelo_candidato,
newdata = validacion,
type = "response"
)
)
calibracion_global <- validacion_evaluada |>
summarise(
observado = sum(n_siniestros),
esperado_base = sum(esperado_base),
esperado_candidato = sum(esperado_candidato),
razon_oe_base = observado / esperado_base,
razon_oe_candidato = observado / esperado_candidato
)
calibracion_globalUna razón observado/esperado cercana a 1 indica calibración global, pero puede ocultar compensaciones entre segmentos. Por eso se inspecciona el ordenamiento por grupos de riesgo.
calibracion_deciles <- validacion_evaluada |>
mutate(
grupo_riesgo = ntile(
esperado_candidato / exposicion,
10
)
) |>
group_by(grupo_riesgo) |>
summarise(
polizas = n(),
exposicion = sum(exposicion),
observado = sum(n_siniestros),
esperado = sum(esperado_candidato),
frecuencia_observada = observado / exposicion,
frecuencia_esperada = esperado / exposicion,
.groups = "drop"
)
calibracion_deciles |>
ggplot(
aes(
x = factor(grupo_riesgo),
group = 1
)
) +
geom_line(
aes(y = frecuencia_observada, color = "Observada")
) +
geom_point(
aes(y = frecuencia_observada, color = "Observada")
) +
geom_line(
aes(y = frecuencia_esperada, color = "Esperada")
) +
geom_point(
aes(y = frecuencia_esperada, color = "Esperada")
) +
labs(
title = "Calibración por grupo de riesgo en validación",
x = "Grupo ordenado por frecuencia esperada",
y = "Siniestros por unidad de exposición",
color = NULL
) +
theme_minimal()En Poisson se supone igualdad entre media y varianza condicional. Una revisión elemental es la razón entre la desviación de Pearson y los grados de libertad.
dispersion_pearson <- sum(
residuals(modelo_candidato, type = "pearson")^2
) / df.residual(modelo_candidato)
tibble(dispersion_pearson = dispersion_pearson)Un valor claramente superior a 1 sugiere investigar heterogeneidad omitida, dependencia o una distribución alternativa. No existe un umbral universal que reemplace el diagnóstico.
El proyecto no termina al producir predict(). Una ficha
de despliegue podría contener:
El proceso KDD puede resumirse en las siguientes etapas, con retornos entre ellas:
“Los reclamos por canal digital tienen mayor costo medio” es un patrón. Se convierte en conocimiento solo después de revisar composición, cobertura, exposición, valores extremos y proceso. Tal vez el canal digital concentre hospitalizaciones; el canal no sería entonces la causa del costo.
Una compañía desea ordenar reclamos para revisión especializada. KDD ayuda a evitar el salto directo de dato a acusación:
La variable irregularidad se simula solo para practicar
el ciclo completo. En un proyecto real, su definición requeriría un
protocolo consistente y una auditoría del proceso que produce la
etiqueta.
set.seed(480)
n_reclamos <- 3500
reclamos_kdd <- tibble(
id_reclamo = sprintf("SK%05d", seq_len(n_reclamos)),
costo = round(rlnorm(n_reclamos, log(1800000), 0.95), 0),
dias_reporte = pmin(rpois(n_reclamos, 5), 45),
documentos_incompletos = rbinom(n_reclamos, 1, 0.13),
siniestros_previos = rpois(n_reclamos, 0.7),
canal = sample(
c("Agencia", "Digital", "Telefónico"),
n_reclamos,
replace = TRUE,
prob = c(0.48, 0.34, 0.18)
)
) |>
mutate(
logit_irregularidad = -4.4 +
0.35 * log1p(costo / 1000000) +
0.045 * dias_reporte +
1.10 * documentos_incompletos +
0.30 * siniestros_previos +
0.25 * (canal == "Digital"),
prob_irregularidad = plogis(logit_irregularidad),
irregularidad = rbinom(
n_reclamos,
1,
prob_irregularidad
)
) |>
select(-logit_irregularidad, -prob_irregularidad)
reclamos_kdd |>
summarise(
reclamos = n(),
casos_identificados = sum(irregularidad),
prevalencia = mean(irregularidad)
)El costo suele presentar asimetría. log1p() reduce su
escala y admite cero. La partición se realiza antes de estimar el
modelo.
modelo_alerta <- glm(
irregularidad ~ log_costo + dias_reporte +
documentos_incompletos + siniestros_previos + canal,
family = binomial(link = "logit"),
data = kdd_entrenamiento
)
tidy(
modelo_alerta,
exponentiate = TRUE,
conf.int = TRUE
) |>
select(term, estimate, conf.low, conf.high)Las razones de momios ayudan a describir asociaciones condicionadas, pero no prueban causalidad ni culpabilidad.
Suponga que el equipo solo puede revisar el 5% de los reclamos. Es más útil medir qué proporción de casos identificados queda dentro de ese cupo que fijar un umbral arbitrario de 0,5.
kdd_evaluada <- kdd_validacion |>
mutate(
puntaje = predict(
modelo_alerta,
newdata = kdd_validacion,
type = "response"
),
cupo_revision = ceiling(0.05 * n()),
prioridad = min_rank(desc(puntaje)) <= cupo_revision
)
metricas_kdd <- kdd_evaluada |>
summarise(
revisados = sum(prioridad),
identificados_en_revision = sum(
irregularidad[prioridad]
),
precision_revision = mean(irregularidad[prioridad]),
cobertura_casos = sum(irregularidad[prioridad]) /
sum(irregularidad),
prevalencia_total = mean(irregularidad),
lift = precision_revision / prevalencia_total
)
metricas_kddSi el lift es 3, el grupo priorizado contiene aproximadamente tres veces la proporción base de casos identificados. Aun así habrá falsos positivos. El puntaje organiza trabajo; la investigación determina el resultado.
kdd_evaluada |>
mutate(decil_puntaje = ntile(puntaje, 10)) |>
group_by(decil_puntaje) |>
summarise(
reclamos = n(),
puntaje_medio = mean(puntaje),
tasa_observada = mean(irregularidad),
.groups = "drop"
) |>
ggplot(
aes(
x = factor(decil_puntaje),
y = tasa_observada
)
) +
geom_col(fill = "#2C7FB8") +
scale_y_continuous(labels = percent) +
labs(
title = "Tasa observada por decil de puntaje",
subtitle = "El decil 10 contiene los puntajes más altos",
x = "Decil de puntaje",
y = "Tasa observada"
) +
theme_minimal()Una aseguradora de salud quiere anticipar afiliados con alta probabilidad de hospitalización y, al mismo tiempo, diseñar una intervención preventiva.
Una ficha de proyecto de máximo dos páginas y un diagrama de fases que indique en qué condiciones el equipo debe regresar a una etapa anterior.
La aseguradora Horizonte cerró su experiencia anual de automóviles. La gerencia observa mayor costo de siniestros y demora en la atención, y propone un aumento general de prima. El equipo actuarial debe determinar si la información permite sostener esa decisión, qué segmentos requieren estudio y qué problemas del proceso podrían explicar parte del resultado.
El proyecto integra los cuatro capítulos. No se espera “el modelo más sofisticado”; se espera una secuencia coherente, verificable y útil para la decisión.
proyecto_polizas_crudas <- caso$polizas_crudas
proyecto_siniestros <- caso$siniestros
proyecto_eventos <- caso$registro_procesos
list(
filas_polizas = nrow(proyecto_polizas_crudas),
filas_siniestros = nrow(proyecto_siniestros),
filas_eventos = nrow(proyecto_eventos)
)## $filas_polizas
## [1] 6012
##
## $filas_siniestros
## [1] 466
##
## $filas_eventos
## [1] 2330
Puede copiar y adaptar el código de los ejemplos, pero cada decisión debe estar explicada en lenguaje actuarial.
¿Qué transformaciones, diagnósticos y análisis necesita Horizonte antes de recomendar una acción sobre tarifa, suscripción o gestión de siniestros?
La publicación final debe incluir:
# Resumen ejecutivo
# Problema y contrato analítico
# Transformación de datos
# Etapas analíticas
# Diagnóstico estadístico y de procesos
# Metodología CRISP-DM y KDD
# Recomendación
# Limitaciones y monitoreo
| Criterio | Puntos | Evidencia principal |
|---|---|---|
| Formulación actuarial | 15 | decisión, población, horizonte y costos de error |
| Transformación y trazabilidad | 20 | código, cuarentena, diccionario y conciliación |
| Análisis e interpretación | 20 | métricas, gráficos y lectura actuarial |
| Diagnóstico integral | 15 | calidad, materialidad y proceso |
| Metodología | 15 | fases, iteraciones y criterios de aceptación |
| Recomendación profesional | 10 | evidencia, límites y acciones viables |
| Reproducibilidad y comunicación | 5 | HTML ejecutable, orden y fuentes |
| Total | 100 |
Una conclusión sólida:
Meta profesional. El producto no es solamente un HTML. Es un expediente analítico que permite a otra persona reproducir el dato, entender el razonamiento y cuestionar responsablemente la recomendación.
La unidad recorrió una cadena completa:
La competencia actuarial aparece al conectar la técnica con exposición, materialidad, incertidumbre, control y decisión. R hace reproducible el proceso; el juicio profesional establece su propósito y sus límites.
Todas las carteras, reclamos y resultados generados en esta publicación son simulados con fines pedagógicos. Los nombres de organizaciones son ficticios. Los resultados no constituyen una tarifa, recomendación comercial ni evaluación de personas reales.