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

Pregunta orientadora

¿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.

Cómo usar esta publicación

Paquetes

Instale una sola vez:

install.packages(
  c("dplyr", "tidyr", "ggplot2", "broom", "scales")
)

Caso transversal

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
lapply(caso, dim)
## $polizas
## [1] 6000   14
## 
## $polizas_crudas
## [1] 6012   14
## 
## $siniestros
## [1] 466   5
## 
## $registro_procesos
## [1] 2330    3
head(caso$polizas, 4)
head(caso$siniestros, 4)

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.

Capítulo 1 · Metodología para la transformación de datos

Objetivos del capítulo

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.

1.1 ¿Qué significa transformar datos?

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.

1.2 Contrato analítico

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.

1.3 Etapas de una transformación defendible

1.3.1 Definir propósito y grano

La misma fuente puede producir distintas bases:

  • una fila por póliza para frecuencia;
  • una fila por siniestro para severidad;
  • una fila por pago para flujo de caja;
  • una fila por afiliado y mes para aportes pensionales;
  • una fila por persona y periodo para mortalidad o morbilidad.

1.3.2 Adquirir y perfilar

Antes de modificar:

  • verificar dimensiones;
  • revisar nombres y tipos;
  • contar faltantes y duplicados;
  • inspeccionar rangos y categorías;
  • documentar fecha, fuente y responsable.

1.3.3 Validar

Una regla de validación compara el dato con una expectativa explícita:

  • edad entre 18 y 100 años;
  • exposición mayor que 0 y menor o igual que 1;
  • costo no negativo;
  • fecha del siniestro dentro de la cobertura;
  • llave de siniestro presente en la tabla de pólizas.

1.3.4 Estandarizar y derivar

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.

1.3.5 Integrar y reconciliar

La integración une fuentes con una cardinalidad esperada. La reconciliación verifica que los totales importantes no cambiaron accidentalmente.

1.3.6 Documentar

El producto no es únicamente la tabla final. También incluye reglas, registros rechazados, controles y decisiones.

1.4 Ejemplo teórico actuarial · Pólizas, siniestros y pagos

Una aseguradora de automóviles tiene:

  • 10.000 pólizas;
  • 1.200 siniestros;
  • 1.850 pagos, porque un siniestro puede pagarse en varias cuotas.

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:

  1. Para frecuencia, agregar siniestros hasta obtener una fila por póliza.
  2. Para severidad, agregar pagos hasta obtener una fila por siniestro.
  3. Para flujo de caja, conservar una fila por pago.

Decisión metodológica. No existe una “base maestra” universal. Existe una base apropiada para una pregunta y un grano determinados.

1.5 Ejemplo aplicado en R · Preparar la cartera

Paso 1. Conservar la fuente y crear trazabilidad

cartera_cruda <- caso$polizas_crudas |>
  as_tibble() |>
  mutate(fila_origen = row_number())

dim(cartera_cruda)
## [1] 6012   15
names(cartera_cruda)
##  [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"
summary(cartera_cruda)
##   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  
## 

Paso 2. Renombrar, convertir y estandarizar

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)

Paso 3. Crear reglas de validación

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)

Paso 4. Separar, no borrar

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.

1.6 Ejemplo guiado · Integrar siniestros sin duplicar exposición

Paso 1. Controlar llaves

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_poliza
siniestros_huerfanos

Paso 2. Agregar antes de unir

siniestros_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
    )
  )

Paso 3. Reconciliar

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_integracion

Conclusión. La unión es aceptable si mantiene una fila por póliza, conserva la exposición y reconcilia el costo del universo incluido.

1.7 Errores frecuentes

  • 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.

1.8 Ejercicio por realizar · Aportes pensionales

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)
  )
)

Tareas

  1. Declare el grano de afiliados, aportes y de la tabla final.
  2. Identifique aportes huérfanos, negativos y faltantes.
  3. Agregue movimientos por afiliado antes de unir.
  4. Conserve afiliados sin aportes.
  5. Calcule meses aportados y aporte acumulado.
  6. Presente controles de filas, afiliados y valor total.
  7. Explique por qué “sin registro” no significa automáticamente “aporte cero”.

Criterios de verificación

  • una fila por afiliado;
  • 800 afiliados en la salida;
  • movimientos huérfanos reportados por separado;
  • valores inválidos conservados en cuarentena;
  • decisión documentada para cada tipo de falla.

Capítulo 2 · Etapas descriptiva, predictiva, prescriptiva y prospectiva

Objetivos del capítulo

Al finalizar, el estudiante podrá:

  • diferenciar las preguntas que responde cada etapa;
  • evitar presentar una descripción como predicción o una predicción como recomendación;
  • recorrer las cuatro etapas con una cartera actuarial;
  • reconocer los supuestos adicionales que exige una decisión.

2.1 Una cadena de preguntas

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.

2.2 Etapa descriptiva

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.

2.3 Etapa predictiva

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.

2.4 Etapa prescriptiva

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.

2.5 Etapa prospectiva

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.

2.6 Ejemplo teórico · Renovación de una cartera de vida

Una compañía observa que el 12% de sus pólizas de vida no se renueva.

  1. Descriptivo: mide cancelación por canal, antigüedad y forma de pago.
  2. Predictivo: estima la probabilidad individual de cancelación.
  3. Prescriptivo: prioriza contactos cuyo beneficio esperado supera el costo.
  4. Prospectivo: evalúa escenarios de desempleo, aumento de prima y efectividad de la campaña.

El modelo no demuestra que llamar al asegurado evite su cancelación. Esa relación debe estudiarse mediante experimentación o diseños causales.

2.7 Ejemplo aplicado en R · Cartera de automóviles

Etapa descriptiva

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.

Etapa predictiva

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.

Etapa prescriptiva

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.

Etapa prospectiva

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.

2.8 Ejemplo guiado · Cancelación en seguros de vida

Crear la base

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
    )
  )

Describir y predecir

vida |>
  group_by(canal) |>
  summarise(
    polizas = n(),
    tasa_cancelacion = mean(cancela),
    mora_media = mean(mora_dias),
    .groups = "drop"
  )
modelo_cancelacion <- glm(
  cancela ~
    antiguedad +
    mora_dias +
    canal +
    log(prima_mensual),
  family = binomial(),
  data = vida
)

vida <- vida |>
  mutate(
    prob_estimada = predict(
      modelo_cancelacion,
      type = "response"
    )
  )

Prescribir bajo una capacidad limitada

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_contacto

Construir escenarios

efectividad <- 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_retencion

Lectura. 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.

2.9 Ejercicio por realizar · Costos de salud

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
    )
  )

Tareas

  1. Descriptiva: compare consultas por año-persona y costo por afiliado entre personas con y sin condición crónica.

  2. Predictiva: ajuste un modelo Poisson para consultas utilizando exposición, edad, condición crónica y región.

  3. Prescriptiva: proponga una regla de priorización para revisión clínica, sin negar cobertura ni sustituir criterio médico.

  4. Prospectiva: evalúe una inflación médica de 8%, 15% y 25%.

  5. Explique qué afirmaciones puede sostener y cuáles no.

Producto

Una tabla, un gráfico, una interpretación de máximo 200 palabras y una lista de supuestos.

Capítulo 3 · Diagnóstico: análisis estadístico y registro de procesos

Objetivos del capítulo

Al finalizar, el estudiante podrá:

  • distinguir diagnóstico de datos, diagnóstico estadístico y diagnóstico de procesos;
  • construir reglas medibles de calidad;
  • reconciliar indicadores técnicos;
  • analizar tiempos, secuencias y cuellos de botella a partir de registros de eventos;
  • determinar si una falla bloquea, condiciona o solo advierte un análisis.

3.1 ¿Qué diagnosticamos?

3.1.1 Datos

Se revisa si la información es apropiada para el propósito:

  • completitud;
  • validez de dominio;
  • unicidad;
  • consistencia temporal;
  • integridad referencial;
  • exactitud frente a fuentes disponibles;
  • estabilidad entre periodos.

3.1.2 Comportamiento estadístico

Se estudian:

  • distribución y dispersión;
  • colas y valores extremos;
  • relaciones entre variables;
  • cambios de mezcla;
  • diferencias entre segmentos;
  • estabilidad de indicadores.

3.1.3 Procesos

Se estudia cómo ocurrió la operación:

  • secuencia de actividades;
  • tiempos entre eventos;
  • reprocesos;
  • casos incompletos;
  • variaciones del flujo;
  • cuellos de botella.

El registro de procesos añade una dimensión que una tabla final suele ocultar: el tiempo.

3.2 Calidad depende del propósito

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.

3.3 Matriz de reglas

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

3.4 Ejemplo teórico · Diagnóstico de reservas

Una compañía observa un aumento de la reserva de siniestros.

Un diagnóstico estadístico pregunta:

  • ¿aumentó el número de siniestros?
  • ¿cambió la severidad?
  • ¿hay grandes siniestros?
  • ¿cambió la mezcla de coberturas?

Un diagnóstico de proceso pregunta:

  • ¿se retrasó el reporte?
  • ¿aumentó el tiempo de revisión?
  • ¿faltan documentos?
  • ¿se acumularon casos en una etapa?
  • ¿cambió la política de cierre?

La misma variación contable puede deberse a riesgo, operación, calendario, registro o una combinación de los cuatro.

3.5 Ejemplo aplicado en R · Tablero de calidad

Se utiliza la tabla evaluada construida en el capítulo 1.

Conteo de fallas

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_calidad

Los conteos por regla no deben sumarse como si fueran registros diferentes: una fila puede fallar varias reglas.

Concentración de fallas

evaluada |>
  count(n_fallas) |>
  mutate(porcentaje = n / sum(n))

Impacto sobre exposición y prima

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_calidad

Interpretació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.

3.6 Diagnóstico estadístico

Exposición y experiencia

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)
  )

Cola de severidad

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_severidad
ggplot(
  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.

Sensibilidad a grandes siniestros

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.

3.7 Reconciliación técnica

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)
  )
)

reconciliacion

Una diferencia no siempre es un error: puede deberse a cortes temporales, recuperaciones, reaperturas o universos distintos. Pero debe explicarse.

3.8 Ejemplo guiado · Registro del proceso de siniestros

Un registro de eventos debería contener, al menos:

  • identificador de caso;

  • nombre del evento;

  • marca de tiempo;

  • responsable o sistema, cuando esté disponible.

Ordenar el flujo y calcular duración

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)

Localizar cuellos de botella

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_etapa
ggplot(
  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()

Casos con mayor duración total

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.

3.9 Ejercicio por realizar · Diagnóstico de reclamaciones de salud

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),
  ]
)

Tareas

  1. Construya reglas para identificador, fechas y costo.
  2. Detecte duplicados exactos y posibles reclamos repetidos.
  3. Compare media, mediana, p90 y p99 del costo.
  4. Calcule días de reporte y localice el 10% más tardío.
  5. Compare diagnóstico por tipo de servicio.
  6. Separe errores, valores extremos válidos y casos pendientes de investigación.
  7. Redacte una conclusión que diferencie calidad, riesgo y proceso.

3.10 Lista de control

Capítulo 4 · Metodologías CRISP, CRISP-DM y KDD

4.1 Propósito del capítulo

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á:

  • diferenciar el uso general de CRISP, el ciclo formal CRISP-DM y el proceso KDD;
  • convertir una necesidad actuarial en un objetivo analítico verificable;
  • asignar entregables, controles y criterios de aceptación a cada fase;
  • evaluar un modelo fuera de muestra y en términos del negocio;
  • explicar por qué desplegar no significa abandonar el monitoreo.

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.

4.2 Qué significan CRISP, CRISP-DM y KDD

En algunos programas académicos se nombran CRISP y CRISP-DM por separado. Conviene hacer una precisión:

  • CRISP se usa aquí como forma corta para la filosofía de un proceso intersectorial, iterativo y orientado al problema.
  • CRISP-DM (Cross-Industry Standard Process for Data Mining) es el ciclo documentado de seis fases: comprensión del negocio, comprensión de los datos, preparación, modelado, evaluación y despliegue.
  • KDD (Knowledge Discovery in Databases) describe el proceso más amplio mediante el cual se obtiene conocimiento válido, novedoso, útil y comprensible a partir de datos. La minería de datos es una etapa de ese proceso, no un sinónimo de todo KDD.

Una comparación práctica

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.

4.3 CRISP-DM: seis fases con lectura actuarial

Fase 1 · Comprensión del negocio

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.

Fase 2 · Comprensión de los datos

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.

Fase 3 · Preparación de los datos

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.

Fase 4 · Modelado

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.

Fase 5 · Evaluación

Hay dos evaluaciones distintas:

  1. Técnica: desempeño fuera de muestra, calibración, estabilidad y residuos.
  2. Actuarial y de negocio: coherencia de relatividades, materialidad, restricciones, impacto sobre asegurados y posibilidad real de implementación.

Fase 6 · Despliegue

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.

Entregables mínimos por fase

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?

4.4 Ejemplo teórico · Rediseño de una tarifa de automóviles

Una aseguradora observa deterioro en la siniestralidad y solicita “usar inteligencia artificial para subir la tarifa”. CRISP-DM obliga a reformular:

  1. Negocio: el objetivo es estimar costo puro esperado para el próximo año, no maximizar el precio ni reproducir la tarifa vigente.
  2. Datos: se requieren pólizas, exposición, siniestros, recuperaciones, gastos y madurez; una tabla de clientes aislada es insuficiente.
  3. Preparación: se define una fila por póliza-periodo y se agregan siniestros en la misma ventana.
  4. Modelado: se modelan frecuencia y severidad, o costo puro, con una base simple de comparación.
  5. Evaluación: se revisan calibración, estabilidad, segmentos pequeños, plausibilidad de relatividades e impacto comercial.
  6. Despliegue: cualquier cambio tarifario sigue aprobación, documentación, implementación y monitoreo.

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.

4.5 Ejemplo aplicado en R · CRISP-DM para frecuencia de siniestros

4.5.1 Comprensión del negocio

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.

4.5.2 Comprensión y preparación de los datos

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.

4.5.3 Modelado

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_crisp

Los 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.

4.5.4 Evaluación fuera de muestra

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_global

Una 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()

4.5.5 Evaluación de sobredispersión

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.

4.5.6 Propuesta de despliegue controlado

El proyecto no termina al producir predict(). Una ficha de despliegue podría contener:

  • versión del modelo y fecha de corte;
  • definición de variables y tratamiento de categorías nuevas;
  • prueba de reconciliación antes de puntuar;
  • monitoreo mensual de observado/esperado;
  • alerta por cambios en mezcla o exposición;
  • revisión humana para segmentos pequeños;
  • criterio documentado para recalibrar o retirar.

4.6 KDD: del dato al conocimiento

El proceso KDD puede resumirse en las siguientes etapas, con retornos entre ellas:

  1. comprender el dominio y seleccionar el objetivo;
  2. seleccionar el conjunto de datos;
  3. limpiar y preprocesar;
  4. transformar y representar;
  5. aplicar métodos de minería de datos;
  6. evaluar e interpretar patrones;
  7. incorporar el conocimiento a una decisión.

Diferencia entre patrón y conocimiento

“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.

4.7 Ejemplo teórico · Detección de reclamos que requieren revisión

Una compañía desea ordenar reclamos para revisión especializada. KDD ayuda a evitar el salto directo de dato a acusación:

  • selección: reclamos cerrados con resultado de investigación conocido;
  • preprocesamiento: duplicados, fechas imposibles, variables disponibles al momento de decidir;
  • transformación: costo logarítmico, demora de reporte y antecedentes;
  • minería: modelo que ordena probabilidad de irregularidad;
  • evaluación: precisión en la capacidad real de revisión y estabilidad;
  • interpretación: razones del puntaje, falsos positivos y sesgos;
  • uso: cola priorizada para análisis humano, nunca sentencia automática.

4.8 Ejemplo guiado en R · Priorización de alertas mediante KDD

4.8.1 Selección y creación del conjunto analítico

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)
  )

4.8.2 Preprocesamiento y transformación

El costo suele presentar asimetría. log1p() reduce su escala y admite cero. La partición se realiza antes de estimar el modelo.

base_kdd <- reclamos_kdd |>
  mutate(
    log_costo = log1p(costo),
    canal = factor(canal)
  )

set.seed(481)
id_kdd_entrenamiento <- sample(
  seq_len(nrow(base_kdd)),
  floor(0.70 * nrow(base_kdd))
)

kdd_entrenamiento <- base_kdd[id_kdd_entrenamiento, ]
kdd_validacion <- base_kdd[-id_kdd_entrenamiento, ]

4.8.3 Minería de datos

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.

4.8.4 Evaluación bajo capacidad operativa

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_kdd

Si 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.

4.8.5 Interpretación del conocimiento

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()

4.9 Ejercicio por realizar · Elegir y aplicar una metodología

Una aseguradora de salud quiere anticipar afiliados con alta probabilidad de hospitalización y, al mismo tiempo, diseñar una intervención preventiva.

Tareas

  1. Reformule la solicitud como una decisión y no como una técnica.
  2. Elija CRISP-DM, KDD o una combinación y justifique la elección.
  3. Defina población, unidad, horizonte, fecha índice y variable respuesta.
  4. Liste al menos cuatro fuentes de datos y un riesgo de calidad por fuente.
  5. Proponga una regla de separación temporal entre entrenamiento y validación.
  6. Defina una métrica técnica y dos criterios actuariales o de negocio.
  7. Explique cómo evitaría usar información posterior a la fecha de decisión.
  8. Diseñe un despliegue con cupo, responsable, monitoreo y mecanismo de salida.

Producto esperado

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.

4.10 Lista de control

Proyecto integrador · Diagnóstico y decisión sobre una cartera de automóviles

P.1 Situación profesional

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.

P.2 Datos de trabajo

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.

P.3 Pregunta central

¿Qué transformaciones, diagnósticos y análisis necesita Horizonte antes de recomendar una acción sobre tarifa, suscripción o gestión de siniestros?

P.4 Desarrollo solicitado

Parte A · Transformación y contrato analítico

  1. Defina la unidad de observación y el periodo.
  2. Construya una base de pólizas válida sin modificar el objeto crudo.
  3. Cree una tabla de cuarentena con motivo de exclusión.
  4. Agregue siniestros a póliza antes de unir.
  5. Reconcilie filas, exposición, primas y costo incurrido.
  6. Elabore un diccionario de al menos diez variables.

Parte B · Etapas analíticas

  1. Descriptiva: cuantifique exposición, frecuencia, severidad y prima pura por zona y uso.
  2. Predictiva: proponga y ajuste un modelo de frecuencia o costo puro con partición de entrenamiento y validación.
  3. Prescriptiva: formule una acción limitada por capacidad o materialidad; no convierta el puntaje en una decisión automática.
  4. Prospectiva: construya tres escenarios para el siguiente año cambiando exposición, frecuencia, severidad o mezcla.

Parte C · Diagnóstico

  1. Diseñe al menos seis reglas de calidad.
  2. Mida el impacto en registros, exposición y prima.
  3. Compare media, mediana, p90 y p99 del costo de siniestros.
  4. Analice duración por etapa en el registro de procesos.
  5. Identifique cuellos de botella y casos atípicos.
  6. Separe evidencia, hipótesis y recomendación.

Parte D · Metodología y decisión

  1. Organice el trabajo con las seis fases de CRISP-DM.
  2. Para cada fase, identifique un entregable y un criterio de aceptación.
  3. Explique qué parte del análisis corresponde a selección, preprocesamiento, transformación, minería e interpretación en KDD.
  4. Escriba una recomendación ejecutiva de máximo 300 palabras.
  5. Incluya limitaciones, monitoreo y siguiente decisión.

P.5 Productos mínimos

La publicación final debe incluir:

  • una tabla de contrato analítico;
  • un flujo reproducible de transformación;
  • una tabla de conciliación;
  • un tablero breve de calidad;
  • al menos tres gráficos con pregunta y conclusión;
  • una tabla de etapas descriptiva, predictiva, prescriptiva y prospectiva;
  • validación fuera de muestra si se ajusta un modelo;
  • diagnóstico del registro de procesos;
  • mapa CRISP-DM del proyecto;
  • recomendación ejecutiva y limitaciones.

P.6 Estructura sugerida del R Markdown

# 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

P.7 Rúbrica de evaluación

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

P.8 Criterios de una buena conclusión

Una conclusión sólida:

  • responde la pregunta central;
  • distingue lo observado de lo inferido;
  • cuantifica la materialidad;
  • no confunde asociación con causalidad;
  • propone una acción proporcional a la evidencia;
  • indica qué información falta;
  • define cómo se comprobará si la decisión funcionó.

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.

Cierre de la unidad

La unidad recorrió una cadena completa:

  1. Transformar sin perder trazabilidad.
  2. Describir qué ocurrió.
  3. Predecir qué podría ocurrir bajo información disponible.
  4. Prescribir acciones con restricciones explícitas.
  5. Prospectar escenarios, no adivinar futuros.
  6. Diagnosticar datos, estadística y proceso.
  7. Organizar el proyecto con CRISP-DM y convertir patrones en conocimiento mediante KDD.

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.

Referencias y lecturas recomendadas

Nota sobre los datos

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.