Demo de preparación de datos para metodologías de reserving¶
Archivo propuesto:
docs/examples/04-demo-preparacion-datos.md
Resumen¶
Este demo muestra cómo evaluar si un dataset operativo de seguros de salud está preparado para aplicar métodos de reserving. El flujo utiliza:
- nombres canónicos en español;
- alias opcionales en inglés;
- mapeo explícito de columnas;
- controles de fechas, importes e identificadores;
- deduplicación con reglas de negocio;
- evaluación de gates G0–G9;
- matriz de elegibilidad por método;
- plan de remediación de datos.
El objetivo no es producir una reserva. El primer resultado es una respuesta más básica y necesaria:
¿Qué métodos pueden utilizarse de forma defendible con la información disponible, cuáles deben bloquearse y qué datos permitirían ampliar el benchmark?
1. Objetivos del demo¶
Al finalizar, el lector podrá:
- mapear columnas operativas a nombres canónicos;
- distinguir presencia de una columna de validez semántica;
- detectar duplicados técnicos y movimientos económicos legítimos;
- construir variables temporales de origen, calendario y desarrollo;
- evaluar gates de elegibilidad;
- producir una matriz por método;
- priorizar datos faltantes;
- adaptar el proceso a un dataset real.
2. Archivos utilizados¶
El demo asume la siguiente estructura:
config/
└── diccionario_datos_canonico.yml
docs/
├── repository-governance/
│ ├── marco-preparacion-datos-metodologias-parte-1.md
│ └── marco-preparacion-datos-metodologias-parte-2.md
└── examples/
└── 04-demo-preparacion-datos.md
Dependencias opcionales para ejecutar el ejemplo en Python:
3. Escenario¶
Una aseguradora recibe dos archivos mensuales con movimientos de reclamaciones. Ambos contienen las mismas transacciones porque uno corresponde al extracto operativo y el otro a una copia enviada al equipo actuarial.
Las columnas originales son:
| Columna original | Significado declarado |
|---|---|
Periodo Servicio |
mes de prestación |
Periodo |
mes de pago |
COSTO |
importe del movimiento |
FRECUENCIA |
número de servicios |
factura |
identificador de factura |
Folio |
identificador de línea |
Componente |
tipo de movimiento |
archivo_origen |
archivo fuente |
segmento |
segmento actuarial |
Antes de usar el dataset se deben resolver cuatro riesgos:
Periodopodría ser contabilización, no pago;COSTOpodría mezclar pagos, reversos y glosas;- las filas de ambos archivos podrían estar duplicadas;
- la historia disponible podría ser insuficiente para estimar factores de desarrollo.
4. Flujo del demo¶
flowchart TD
A["Dataset crudo"] --> B["Mapeo canónico"]
B --> C["Normalización de tipos"]
C --> D["Validaciones semánticas"]
D --> E["Deduplicación"]
E --> F["Variables derivadas"]
F --> G["Perfil por dominio D1–D10"]
G --> H["Gates G0–G9"]
H --> I["Matriz por método"]
I --> J["Plan de remediación"]
5. Dataset de ejemplo¶
El ejemplo genera un archivo con movimientos duplicados deliberadamente.
from pathlib import Path
import pandas as pd
def crear_datos_demo(ruta: str = "datos_demo_preparacion.csv") -> Path:
movimientos = [
{
"Periodo Servicio": "2026-01-01",
"Periodo": "2026-01-01",
"COSTO": 120000.0,
"FRECUENCIA": 1,
"factura": "FAC-001",
"Folio": "01",
"Componente": "PAGO",
"archivo_origen": "extracto_operativo.csv",
"segmento": "AMBULATORIO",
},
{
"Periodo Servicio": "2026-01-01",
"Periodo": "2026-02-01",
"COSTO": 80000.0,
"FRECUENCIA": 1,
"factura": "FAC-001",
"Folio": "02",
"Componente": "PAGO",
"archivo_origen": "extracto_operativo.csv",
"segmento": "AMBULATORIO",
},
{
"Periodo Servicio": "2026-01-01",
"Periodo": "2026-03-01",
"COSTO": -10000.0,
"FRECUENCIA": 0,
"factura": "FAC-001",
"Folio": "03",
"Componente": "REVERSO",
"archivo_origen": "extracto_operativo.csv",
"segmento": "AMBULATORIO",
},
{
"Periodo Servicio": "2026-02-01",
"Periodo": "2026-02-01",
"COSTO": 250000.0,
"FRECUENCIA": 2,
"factura": "FAC-002",
"Folio": "01",
"Componente": "PAGO",
"archivo_origen": "extracto_operativo.csv",
"segmento": "HOSPITALARIO",
},
{
"Periodo Servicio": "2026-02-01",
"Periodo": "2026-03-01",
"COSTO": 90000.0,
"FRECUENCIA": 1,
"factura": "FAC-003",
"Folio": "01",
"Componente": "PAGO",
"archivo_origen": "extracto_operativo.csv",
"segmento": "AMBULATORIO",
},
{
"Periodo Servicio": "2026-03-01",
"Periodo": "2026-03-01",
"COSTO": 400000.0,
"FRECUENCIA": 1,
"factura": "FAC-004",
"Folio": "01",
"Componente": "PAGO",
"archivo_origen": "extracto_operativo.csv",
"segmento": "HOSPITALARIO",
},
]
copia = []
for fila in movimientos:
duplicada = fila.copy()
duplicada["archivo_origen"] = "copia_actuarial.csv"
copia.append(duplicada)
datos = pd.DataFrame(movimientos + copia)
ruta_salida = Path(ruta)
datos.to_csv(ruta_salida, index=False, encoding="utf-8")
return ruta_salida
El archivo contiene 12 filas físicas, pero solo seis movimientos económicos únicos.
6. Mapeo canónico¶
6.1 Mapeo explícito¶
MAPEO_COLUMNAS = {
"Periodo Servicio": "fecha_servicio",
"Periodo": "fecha_pago",
"COSTO": "costo_pagado",
"FRECUENCIA": "conteo_servicios",
"factura": "id_factura",
"Folio": "id_folio",
"Componente": "tipo_movimiento",
"archivo_origen": "archivo_fuente",
"segmento": "segmento",
}
6.2 Confirmaciones de negocio requeridas¶
El mapeo no debe aprobarse solo por similitud del nombre.
| Columna | Confirmación necesaria |
|---|---|
Periodo |
corresponde a desembolso, no contabilización |
COSTO |
representa movimientos pagados incrementales |
Componente |
distingue pagos, reversos y recuperaciones |
FRECUENCIA |
cuenta servicios, no reclamaciones |
Una alternativa conservadora consiste en mapear primero a nombres neutrales:
y promoverlos a fecha_pago y costo_pagado únicamente después de la validación semántica.
7. Normalización y construcción de identificadores¶
from __future__ import annotations
import hashlib
import pandas as pd
import yaml
def cargar_diccionario(
ruta: str = "../../config/diccionario_datos_canonico.yml",
) -> dict:
with open(ruta, "r", encoding="utf-8") as archivo:
return yaml.safe_load(archivo)
def normalizar_datos(
datos: pd.DataFrame,
mapeo: dict[str, str],
fecha_valoracion: str,
) -> pd.DataFrame:
faltantes = sorted(set(mapeo) - set(datos.columns))
if faltantes:
raise ValueError(
"No se encontraron las columnas de origen: "
+ ", ".join(faltantes)
)
salida = datos.rename(columns=mapeo).copy()
for columna in ["fecha_servicio", "fecha_pago"]:
salida[columna] = pd.to_datetime(
salida[columna],
errors="coerce",
)
salida["fecha_valoracion"] = pd.Timestamp(fecha_valoracion)
salida["costo_pagado"] = pd.to_numeric(
salida["costo_pagado"],
errors="coerce",
)
salida["conteo_servicios"] = pd.to_numeric(
salida["conteo_servicios"],
errors="coerce",
).fillna(0)
salida["tipo_movimiento"] = (
salida["tipo_movimiento"]
.astype("string")
.str.strip()
.str.upper()
)
salida["id_movimiento_economico"] = salida.apply(
construir_id_movimiento,
axis=1,
)
return salida
def construir_id_movimiento(fila: pd.Series) -> str:
componentes = [
str(fila.get("id_factura", "")),
str(fila.get("id_folio", "")),
str(fila.get("fecha_servicio", "")),
str(fila.get("fecha_pago", "")),
str(fila.get("costo_pagado", "")),
str(fila.get("tipo_movimiento", "")),
]
texto = "|".join(componentes)
return hashlib.sha256(texto.encode("utf-8")).hexdigest()
7.1 Por qué archivo_fuente no entra en la llave económica¶
Si se incluyera archivo_fuente, las dos copias serían consideradas movimientos diferentes. La llave económica debe representar el evento, no el contenedor técnico.
8. Validaciones iniciales¶
def validar_fechas(datos: pd.DataFrame) -> dict[str, int]:
return {
"fechas_servicio_nulas": int(
datos["fecha_servicio"].isna().sum()
),
"fechas_pago_nulas": int(
datos["fecha_pago"].isna().sum()
),
"rezagos_negativos": int(
(datos["fecha_pago"] < datos["fecha_servicio"]).sum()
),
"pagos_posteriores_valoracion": int(
(datos["fecha_pago"] > datos["fecha_valoracion"]).sum()
),
}
def validar_importes(datos: pd.DataFrame) -> dict[str, int]:
negativos_sin_clasificar = (
(datos["costo_pagado"] < 0)
& ~datos["tipo_movimiento"].isin(
["REVERSO", "RECUPERACION", "GLOSA"]
)
)
return {
"importes_nulos": int(datos["costo_pagado"].isna().sum()),
"importes_negativos": int((datos["costo_pagado"] < 0).sum()),
"negativos_sin_clasificar": int(
negativos_sin_clasificar.sum()
),
}
def validar_duplicados(datos: pd.DataFrame) -> dict[str, float]:
duplicados = datos.duplicated(
subset=["id_movimiento_economico"],
keep=False,
)
numero_duplicados = int(duplicados.sum())
tasa = numero_duplicados / len(datos) if len(datos) else 0.0
return {
"filas_fisicas": int(len(datos)),
"filas_en_grupos_duplicados": numero_duplicados,
"tasa_filas_en_grupos_duplicados": round(tasa, 4),
"movimientos_economicos_unicos": int(
datos["id_movimiento_economico"].nunique()
),
}
9. Deduplicación¶
def deduplicar_movimientos(datos: pd.DataFrame) -> pd.DataFrame:
prioridad_fuente = {
"extracto_operativo.csv": 1,
"copia_actuarial.csv": 2,
}
salida = datos.copy()
salida["prioridad_fuente"] = (
salida["archivo_fuente"]
.map(prioridad_fuente)
.fillna(999)
)
salida = (
salida
.sort_values(
["id_movimiento_economico", "prioridad_fuente"]
)
.drop_duplicates(
subset=["id_movimiento_economico"],
keep="first",
)
.drop(columns=["prioridad_fuente"])
.reset_index(drop=True)
)
return salida
Advertencia
Esta regla es válida solo porque se confirmó que ambas fuentes contienen copias exactas. En datos reales, una fila adicional podría ser un pago parcial, ajuste o reverso legítimo.
10. Variables derivadas¶
def agregar_variables_temporales(
datos: pd.DataFrame,
) -> pd.DataFrame:
salida = datos.copy()
salida["periodo_origen"] = (
salida["fecha_servicio"]
.dt.to_period("M")
.dt.to_timestamp()
)
salida["periodo_calendario"] = (
salida["fecha_pago"]
.dt.to_period("M")
.dt.to_timestamp()
)
salida["edad_desarrollo"] = (
12
* (
salida["periodo_calendario"].dt.year
- salida["periodo_origen"].dt.year
)
+ (
salida["periodo_calendario"].dt.month
- salida["periodo_origen"].dt.month
)
)
salida["importe_incremental"] = salida["costo_pagado"]
salida["indicador_rezago_valido"] = (
salida["edad_desarrollo"] >= 0
)
return salida
La fórmula mensual utilizada es:
11. Construcción del triángulo descriptivo¶
def construir_triangulo_incremental(
datos: pd.DataFrame,
) -> pd.DataFrame:
return (
datos
.pivot_table(
index="periodo_origen",
columns="edad_desarrollo",
values="importe_incremental",
aggfunc="sum",
fill_value=0.0,
)
.sort_index()
.sort_index(axis=1)
)
Este triángulo es descriptivo. Su construcción no implica que Chain Ladder sea elegible.
12. Perfil básico de datos¶
def construir_perfil(datos: pd.DataFrame) -> dict:
periodos_origen = datos["periodo_origen"].dropna().nunique()
horizonte = (
int(datos["edad_desarrollo"].max())
if datos["edad_desarrollo"].notna().any()
else 0
)
return {
"numero_filas": int(len(datos)),
"numero_periodos_origen": int(periodos_origen),
"horizonte_desarrollo": horizonte,
"fecha_minima_servicio": str(
datos["fecha_servicio"].min().date()
),
"fecha_maxima_servicio": str(
datos["fecha_servicio"].max().date()
),
"importe_total": float(datos["costo_pagado"].sum()),
"numero_facturas": int(datos["id_factura"].nunique()),
"numero_segmentos": int(datos["segmento"].nunique()),
"tiene_exposicion": "miembros_mes" in datos.columns,
"tiene_prior": "ultimate_esperado" in datos.columns,
"tiene_snapshots": "fecha_snapshot" in datos.columns,
"tiene_reserva_caso": "reserva_caso" in datos.columns,
}
13. Evaluación de gates¶
13.1 Configuración del demo¶
UMBRALES = {
"minimo_periodos_origen_chain_ladder": 36,
"minimo_horizonte_desarrollo": 12,
}
CONTEXTO = {
"obligacion_definida": True,
"fecha_pago_confirmada": True,
"costo_pagado_confirmado": True,
"historia_representativa": False,
"proceso_versionado": True,
}
13.2 Función¶
def evaluar_gates(
datos: pd.DataFrame,
perfil: dict,
contexto: dict,
umbrales: dict,
) -> dict[str, bool]:
fechas_validas = (
datos["fecha_servicio"].notna().all()
and datos["fecha_pago"].notna().all()
and (datos["fecha_pago"] >= datos["fecha_servicio"]).all()
and (datos["fecha_pago"] <= datos["fecha_valoracion"]).all()
)
importes_validos = (
datos["costo_pagado"].notna().all()
and contexto["costo_pagado_confirmado"]
and not (
(datos["costo_pagado"] < 0)
& ~datos["tipo_movimiento"].isin(
["REVERSO", "RECUPERACION", "GLOSA"]
)
).any()
)
integridad_valida = datos["id_movimiento_economico"].is_unique
historia_suficiente = (
perfil["numero_periodos_origen"]
>= umbrales["minimo_periodos_origen_chain_ladder"]
and perfil["horizonte_desarrollo"]
>= umbrales["minimo_horizonte_desarrollo"]
)
return {
"G0": bool(contexto["obligacion_definida"]),
"G1": bool(
fechas_validas
and contexto["fecha_pago_confirmada"]
),
"G2": bool(importes_validos),
"G3": bool(integridad_valida),
"G4": bool(historia_suficiente),
"G5": bool(perfil["tiene_exposicion"]),
"G6": bool(perfil["tiene_prior"]),
"G7": bool(contexto["historia_representativa"]),
"G8": bool(perfil["tiene_snapshots"]),
"G9": bool(contexto["proceso_versionado"]),
}
14. Requisitos por método¶
REQUISITOS_METODOS = {
"CHAIN_LADDER_PAGADO": {
"nombre": "Chain Ladder pagado",
"campos": {
"fecha_servicio",
"fecha_pago",
"costo_pagado",
"fecha_valoracion",
},
"gates_criticos": {
"G0", "G1", "G2", "G3", "G4", "G7", "G9"
},
},
"BORNHUETTER_FERGUSON": {
"nombre": "Bornhuetter-Ferguson",
"campos": {
"periodo_origen",
"importe_acumulado",
"proporcion_desarrollada",
"ultimate_esperado",
"fecha_prior",
"fecha_valoracion",
},
"gates_criticos": {
"G0", "G2", "G4", "G6", "G7", "G9"
},
},
"CAPE_COD": {
"nombre": "Cape Cod",
"campos": {
"periodo_origen",
"importe_acumulado",
"proporcion_desarrollada",
"miembros_mes",
"fecha_valoracion",
},
"gates_criticos": {
"G0", "G2", "G4", "G5", "G7", "G9"
},
},
"MACK": {
"nombre": "Mack Chain Ladder",
"campos": {
"periodo_origen",
"edad_desarrollo",
"importe_acumulado",
"fecha_valoracion",
},
"gates_criticos": {
"G0", "G1", "G2", "G3", "G4", "G7", "G9"
},
},
"BOOTSTRAP": {
"nombre": "Bootstrap Chain Ladder",
"campos": {
"periodo_origen",
"edad_desarrollo",
"importe_incremental",
"fecha_valoracion",
},
"gates_criticos": {
"G0", "G1", "G2", "G3", "G4", "G7", "G8", "G9"
},
},
"GLM": {
"nombre": "GLM agregado",
"campos": {
"periodo_origen",
"edad_desarrollo",
"periodo_calendario",
"importe_incremental",
},
"gates_criticos": {
"G0", "G1", "G2", "G3", "G4", "G7", "G8", "G9"
},
},
"MACHINE_LEARNING": {
"nombre": "Machine learning granular",
"campos": {
"id_reclamacion",
"fecha_snapshot",
"fecha_valoracion",
},
"gates_criticos": {
"G0", "G1", "G2", "G3", "G7", "G8", "G9"
},
},
}
15. Matriz de elegibilidad¶
def evaluar_metodos(
datos: pd.DataFrame,
gates: dict[str, bool],
) -> pd.DataFrame:
columnas = set(datos.columns)
resultados = []
for codigo, especificacion in REQUISITOS_METODOS.items():
faltantes = sorted(
especificacion["campos"] - columnas
)
gates_fallidos = sorted(
gate
for gate in especificacion["gates_criticos"]
if not gates.get(gate, False)
)
if faltantes or gates_fallidos:
estado = "BLOQUEADO"
elif not gates.get("G8", False):
estado = "LISTO_CON_LIMITACIONES"
else:
estado = "LISTO"
resultados.append(
{
"codigo_metodo": codigo,
"metodo": especificacion["nombre"],
"estado_preparacion": estado,
"campos_faltantes": ", ".join(faltantes),
"gates_fallidos": ", ".join(gates_fallidos),
}
)
return pd.DataFrame(resultados)
16. Script completo de ejecución¶
def ejecutar_demo() -> None:
ruta = crear_datos_demo()
datos_crudos = pd.read_csv(ruta)
datos = normalizar_datos(
datos=datos_crudos,
mapeo=MAPEO_COLUMNAS,
fecha_valoracion="2026-03-31",
)
print("Validación de fechas")
print(validar_fechas(datos))
print("\nValidación de importes")
print(validar_importes(datos))
print("\nValidación de duplicados")
print(validar_duplicados(datos))
datos_unicos = deduplicar_movimientos(datos)
datos_unicos = agregar_variables_temporales(datos_unicos)
triangulo = construir_triangulo_incremental(datos_unicos)
perfil = construir_perfil(datos_unicos)
gates = evaluar_gates(
datos=datos_unicos,
perfil=perfil,
contexto=CONTEXTO,
umbrales=UMBRALES,
)
matriz = evaluar_metodos(datos_unicos, gates)
print("\nTriángulo incremental")
print(triangulo)
print("\nPerfil")
print(perfil)
print("\nGates")
print(gates)
print("\nMatriz de elegibilidad")
print(matriz.to_string(index=False))
triangulo.to_csv(
"triangulo_incremental_demo.csv",
encoding="utf-8",
)
matriz.to_csv(
"matriz_elegibilidad_demo.csv",
index=False,
encoding="utf-8",
)
if __name__ == "__main__":
ejecutar_demo()
17. Resultado esperado¶
17.1 Perfil¶
numero_filas: 6
numero_periodos_origen: 3
horizonte_desarrollo: 2
importe_total: 930000
numero_facturas: 4
numero_segmentos: 2
tiene_exposicion: false
tiene_prior: false
tiene_snapshots: false
tiene_reserva_caso: false
17.2 Gates¶
| Gate | Resultado | Interpretación |
|---|---|---|
| G0 | Sí | obligación declarada |
| G1 | Sí | fechas coherentes y semántica confirmada |
| G2 | Sí | importes y negativos clasificados |
| G3 | Sí | movimientos únicos después de deduplicar |
| G4 | No | tres periodos de origen y horizonte 2 |
| G5 | No | no hay exposición |
| G6 | No | no hay prior |
| G7 | No | representatividad no demostrada |
| G8 | No | no hay snapshots |
| G9 | Sí | proceso reproducible |
17.3 Matriz esperada¶
| Método | Estado | Causa principal |
|---|---|---|
| Chain Ladder pagado | BLOQUEADO |
G4 y G7 |
| Bornhuetter-Ferguson | BLOQUEADO |
prior, historia y campos faltantes |
| Cape Cod | BLOQUEADO |
exposición e historia |
| Mack | BLOQUEADO |
historia insuficiente |
| Bootstrap | BLOQUEADO |
historia y snapshots |
| GLM agregado | BLOQUEADO |
historia y validación |
| Machine learning | BLOQUEADO |
claim ID y snapshots |
El dataset permite construir un triángulo descriptivo, pero no permite promover un método a benchmark actuarial.
18. Plan de remediación¶
| Prioridad | Dato o acción | Definición | Métodos habilitados |
|---|---|---|---|
| 1 | ampliar historia | al menos 36 meses de origen y desarrollo suficiente | CL, Mack, Bootstrap, GLM |
| 2 | demostrar representatividad | documentar cambios de red, TPA, beneficios y sistemas | todos |
| 3 | conservar snapshots | cortes mensuales inmutables | backtesting, Bootstrap, GLM, ML |
| 4 | integrar miembros_mes |
exposición por periodo y segmento | Cape Cod, PMPM, GLM con offset |
| 5 | integrar ultimate_esperado |
prior ex ante | BF, Benktander |
| 6 | integrar reserva_caso |
outstanding por claim y snapshot | CL incurrido |
| 7 | integrar id_reclamacion |
llave granular seudonimizada | supervivencia, multiestado, ML |
19. Segundo escenario: dataset remediado¶
Después de completar la información, el perfil objetivo podría ser:
numero_periodos_origen: 60
horizonte_desarrollo: 36
tiene_exposicion: true
tiene_prior: true
tiene_snapshots: true
tiene_reserva_caso: true
historia_representativa: true
La matriz objetivo sería:
| Método | Estado objetivo |
|---|---|
| Chain Ladder pagado | LISTO |
| Chain Ladder incurrido | LISTO_CON_LIMITACIONES |
| Bornhuetter-Ferguson | LISTO |
| Benktander | LISTO |
| Cape Cod | LISTO |
| Mack | LISTO |
| Bootstrap | LISTO |
| GLM | LISTO_CON_LIMITACIONES |
| Machine learning | EXPLORATORIO |
El estado final todavía depende de estabilidad, volumen, residual diagnostics, convergencia y gobierno.
20. Implementación equivalente en R¶
library(dplyr)
library(readr)
library(lubridate)
library(digest)
mapeo_columnas <- c(
"Periodo Servicio" = "fecha_servicio",
"Periodo" = "fecha_pago",
"COSTO" = "costo_pagado",
"FRECUENCIA" = "conteo_servicios",
"factura" = "id_factura",
"Folio" = "id_folio",
"Componente" = "tipo_movimiento",
"archivo_origen" = "archivo_fuente",
"segmento" = "segmento"
)
datos <- read_csv("datos_demo_preparacion.csv", show_col_types = FALSE)
datos <- datos %>%
rename(all_of(mapeo_columnas)) %>%
mutate(
fecha_servicio = as.Date(fecha_servicio),
fecha_pago = as.Date(fecha_pago),
fecha_valoracion = as.Date("2026-03-31"),
tipo_movimiento = toupper(trimws(tipo_movimiento)),
id_movimiento_economico = mapply(
function(id_factura, id_folio, fecha_servicio,
fecha_pago, costo_pagado, tipo_movimiento) {
digest(
paste(
id_factura,
id_folio,
fecha_servicio,
fecha_pago,
costo_pagado,
tipo_movimiento,
sep = "|"
),
algo = "sha256"
)
},
id_factura,
id_folio,
fecha_servicio,
fecha_pago,
costo_pagado,
tipo_movimiento
)
)
datos_unicos <- datos %>%
arrange(id_movimiento_economico, archivo_fuente) %>%
distinct(id_movimiento_economico, .keep_all = TRUE) %>%
mutate(
periodo_origen = floor_date(fecha_servicio, "month"),
periodo_calendario = floor_date(fecha_pago, "month"),
edad_desarrollo =
12 * (year(periodo_calendario) - year(periodo_origen)) +
month(periodo_calendario) - month(periodo_origen),
importe_incremental = costo_pagado
)
21. Controles equivalentes en SQL¶
21.1 Duplicados económicos¶
SELECT
id_factura,
id_folio,
fecha_servicio,
fecha_pago,
costo_pagado,
tipo_movimiento,
COUNT(*) AS numero_copias
FROM movimientos
GROUP BY
id_factura,
id_folio,
fecha_servicio,
fecha_pago,
costo_pagado,
tipo_movimiento
HAVING COUNT(*) > 1;
21.2 Rezagos negativos¶
21.3 Negativos no clasificados¶
SELECT COUNT(*) AS negativos_no_clasificados
FROM movimientos
WHERE costo_pagado < 0
AND tipo_movimiento NOT IN (
'REVERSO',
'RECUPERACION',
'GLOSA'
);
21.4 Historia disponible¶
SELECT
COUNT(DISTINCT periodo_origen) AS periodos_origen,
MAX(edad_desarrollo) AS horizonte_desarrollo
FROM movimientos_canonicos;
22. Adaptación a un dataset real¶
22.1 Paso 1 — No renombrar automáticamente¶
Crear una tabla de mapeo:
| Columna fuente | Campo canónico | Confianza | Validado por |
|---|---|---|---|
fec_ate |
fecha_servicio |
alta | operaciones |
f_cont |
fecha_contabilizacion |
alta | finanzas |
valor |
pendiente | baja | pendiente |
num_cta |
id_factura |
media | auditoría médica |
22.2 Paso 2 — Clasificar las ambigüedades¶
Estados sugeridos:
CONFIRMADO;PROVISIONAL;AMBIGUO;NO_MAPEADO;NO_APLICA.
22.3 Paso 3 — Definir la obligación¶
Ejemplo:
tipo_obligacion: FEE_FOR_SERVICE
medida_objetivo: PAGADO
base_bruta_neta: BRUTA
fecha_valoracion: 2026-06-30
segmento_objetivo: PLAN_INTEGRAL_NACIONAL
moneda: COP
22.4 Paso 4 — Ejecutar controles antes del triángulo¶
No construir factores hasta resolver:
- duplicados;
- fechas;
- movimientos negativos;
- cobertura;
- cambios de sistema;
- moneda;
- perímetro.
23. Consideraciones para Colombia¶
En fuentes colombianas, los nombres y procesos pueden variar entre:
- RIPS;
- FEV;
- cuentas médicas;
- ERP;
- tesorería;
- módulos de autorizaciones;
- recobros;
- información de afiliación;
- contratos con IPS.
Debe distinguirse cuidadosamente:
- fecha de atención;
- fecha de radicación;
- fecha de validación;
- fecha de contabilización;
- fecha de giro;
- fecha de pago efectivo.
Asimismo:
- una glosa no siempre equivale a una recuperación;
- una nota crédito no siempre equivale a un reverso económico;
- los identificadores pueden cambiar entre RIPS, factura y sistema contable;
- la exposición debe reconciliarse con afiliación y vigencia;
- los pagos prospectivos requieren una lógica contractual distinta.
24. Limitaciones del demo¶
Este ejemplo:
- usa pocos registros;
- no estima factores;
- no ejecuta métodos de reserva;
- no modela inflación médica;
- no incluye reaseguro;
- no incluye ajustes de riesgo;
- no sustituye validación actuarial;
- no define umbrales regulatorios;
- no prueba representatividad.
Su propósito es ilustrar el proceso de preparación y elegibilidad.
25. Errores frecuentes¶
Error 1 — Confundir una columna con su significado¶
Periodo no implica fecha_pago.
Error 2 — Eliminar negativos¶
Los negativos pueden ser reversos legítimos.
Error 3 — Deduplicar por toda la fila¶
Una diferencia en archivo_fuente impide detectar una copia económica.
Error 4 — Tratar nulos como cero¶
Un nulo puede representar desconocido, no ausencia de costo.
Error 5 — Ejecutar Chain Ladder con historia mínima¶
La posibilidad de crear un triángulo no demuestra credibilidad.
Error 6 — Usar priors retrospectivos¶
Produce una ventaja artificial en BF y Benktander.
Error 7 — Validar ML con datos finales¶
Genera leakage y resultados optimistas.
26. Extensiones recomendadas¶
El demo puede ampliarse para generar:
- reporte HTML;
- archivo
perfil_datos.csv; - archivo
matriz_elegibilidad.csv; - archivo
plan_remediacion.csv; - gráficos de completitud;
- heatmap de historia;
- matriz de aliases;
- score por dominio;
- comparación entre cortes;
- pruebas automatizadas.
27. Checklist práctico¶
Semántica¶
- La obligación fue definida.
- La fecha calendario fue confirmada.
- La medida económica fue confirmada.
- Los negativos fueron clasificados.
- La unidad de frecuencia fue definida.
Integridad¶
- La llave económica fue documentada.
- Los duplicados técnicos fueron separados de movimientos legítimos.
- Los identificadores sensibles fueron seudonimizados.
- Se conservó trazabilidad a la fuente.
Historia¶
- Se calcularon periodos de origen.
- Se calculó el horizonte de desarrollo.
- Se evaluó la madurez.
- Se documentaron cambios estructurales.
Métodos¶
- Se evaluaron gates.
- Se bloquearon métodos con brechas críticas.
- Se documentó el benchmark actual.
- Se documentó el benchmark objetivo.
- Se priorizaron datos faltantes.
Gobierno¶
- Se versionó el dataset.
- Se versionó el código.
- Se registró la fecha de valoración.
- Se guardaron logs.
- Se asignó responsable.
28. Bibliografía comentada¶
- Actuarial Standards Board. ASOP No. 23, Data Quality. Marco para selección, revisión y comunicación de datos.
- Actuarial Standards Board. ASOP No. 56, Modeling. Referencia para propósito, datos, estructura, validación, gobierno y riesgo de modelo.
- Actuarial Standards Board. ASOP No. 5, Incurred Health and Disability Claims. Orientación para estimación y revisión de obligaciones incurridas en salud.
- Health Insurance Reserving Handbook — Marco de preparación de datos, Partes 1 y 2. Especificación de dominios, gates, estados y requisitos por método.
- Health Insurance Reserving Handbook — Construcción de triángulos. Definiciones de origen, desarrollo, calendario e incrementales.
- England, P. D. y Verrall, R. J. Referencia para formulaciones estocásticas y bootstrap.
- Mack, T. Referencia para el error estándar bajo Chain Ladder libre de distribución.
29. Conclusión¶
El demo muestra que la secuencia correcta es:
No debe invertirse esa secuencia.
La primera salida de un dataset de reserving no es el IBNR: es la evidencia de que el dataset puede soportar el método que se desea utilizar.
Cierre editorial¶
- Nivel de confianza: Alto.
- Factores que podrían cambiar la conclusión: La semántica real de las columnas, el tipo de obligación, la disponibilidad de historia, exposición, priors, reservas de caso y snapshots.
- Acción recomendada: Ejecutar este demo con un dataset sintético en el repositorio y, posteriormente, incorporar un script separado que automatice la generación de la matriz y de los archivos de salida.