Esta guía desarrolla la metodología SEMMA desde una perspectiva conceptual y práctica. El recorrido comienza con su origen y finalidad, continúa con la explicación de sus cinco fases y después muestra una aplicación completa en siniestralidad de automóviles. Finalmente, se comparan sus alcances con CRISP-DM y KDD y se propone un reto para aplicar lo aprendido.
La pregunta que conecta todo el documento es: ¿cómo puede una aseguradora transformar una base de pólizas en información útil para reconocer niveles de riesgo y orientar acciones preventivas?
El orden del contenido es el siguiente:
SEMMA organiza las tareas centrales de minería de datos en cinco fases: Sample, Explore, Modify, Model y Assess. Su nombre nace de las iniciales de esas palabras y se relaciona con el entorno de SAS Enterprise Miner.
En lenguaje sencillo responde cinco preguntas: ¿con qué datos trabajaré?, ¿qué encuentro?, ¿qué debo corregir o crear?, ¿qué modelo puedo construir? y ¿qué tan útil resulta?
Ayuda a ordenar un análisis cuando existen muchos datos y se quieren encontrar patrones o construir modelos. En actuaría puede apoyar estudios de frecuencia, severidad, fraude, cancelación y probabilidad de siniestro.
Las fases no deben verse como pasos totalmente aislados. Durante el análisis es normal regresar a una etapa anterior. Por ejemplo, un problema encontrado al modelar puede obligar a explorar nuevamente una variable o a modificar la forma en que fue preparada.
Esta fase define con qué parte de la información se trabajará. Cuando la base es muy grande, una muestra bien construida permite estudiar el problema sin procesar todos los registros. En un ejercicio predictivo también se separan datos para entrenamiento y prueba. El entrenamiento permite que el modelo aprenda patrones; la prueba permite comprobarlo con observaciones que no utilizó para aprender.
En R pueden utilizarse sample(), índices aleatorios,
particiones estratificadas y una semilla con set.seed(). La
semilla permite repetir la misma separación cuando se vuelve a ejecutar
el documento.
Aquí se busca conocer la base antes de modelar. Se revisa cuántas filas y variables existen, cuáles tienen datos faltantes, cómo se distribuyen las variables numéricas, qué categorías aparecen y si hay valores que parecen poco coherentes. También se realizan comparaciones iniciales con la variable objetivo.
En R se emplean funciones como dim(),
head(), summary(), table(),
is.na() y gráficos con ggplot2. La exploración
muestra señales, pero no demuestra por sí sola una relación causal.
En esta fase los datos se preparan para que puedan ser utilizados de forma consistente. Esto puede incluir corregir formatos, tratar valores faltantes, agrupar categorías, transformar variables y crear nuevas medidas. Cada modificación debe tener una explicación, porque una transformación incorrecta puede cambiar el resultado.
En R se utilizan mutate(), filter(),
case_when(), cut(), conversión a factores y
reglas de imputación. Para evitar introducir información del futuro, los
valores usados para completar datos de prueba deben calcularse
únicamente con el conjunto de entrenamiento.
La fase de modelado intenta representar la relación entre unas variables explicativas y un resultado. No existe un modelo que sea siempre mejor. La elección depende de la pregunta, del tipo de variable objetivo y de la necesidad de explicar el resultado.
En esta guía se utiliza una regresión logística con
glm(), apropiada para estimar una probabilidad entre 0 y 1,
y un árbol de decisión con rpart(), que divide la población
mediante reglas. Comparar ambos permite observar el equilibrio entre
capacidad predictiva e interpretación.
Evaluar significa comprobar si el modelo funciona de manera razonable en datos que no utilizó para aprender. No basta con revisar cuántos casos acertó: también importa entender qué tipo de errores comete. En seguros puede ser especialmente relevante identificar cuántos siniestros reales fueron omitidos.
En R se obtienen probabilidades, matriz de confusión, exactitud, sensibilidad, especificidad y AUC. El punto de corte no debe elegirse de forma automática; debería considerar el coste y la consecuencia de cada error.
En seguros de autos, SEMMA puede apoyar la estimación de frecuencia, severidad o probabilidad de siniestro. La evaluación debe considerar el uso: no detectar una póliza riesgosa puede ser más costoso que revisar una póliza que finalmente no presenta siniestro.
Conviene cuando la pregunta está clara, los datos existen y el centro del trabajo es preparar, modelar y evaluar. No conviene usarla sola cuando todavía no se entiende el problema de negocio o se necesita planificar regulación, despliegue y seguimiento.
Una aseguradora administra muchas pólizas de automóviles y no puede revisar cada caso con la misma profundidad. En la base existen características del conductor, del vehículo, del uso y de la exposición, además del resultado observado sobre siniestros.
El problema consiste en reconocer patrones que permitan ordenar las pólizas según su probabilidad estimada de presentar al menos un siniestro. El propósito no es afirmar que una persona tendrá un accidente ni tomar decisiones automáticas. Se busca construir una herramienta académica que ayude a priorizar revisiones preventivas, validar información y comprender qué variables aportan señales de riesgo.
La unidad de análisis es la póliza. La variable objetivo
tuvo_siniestro vale 1 si la póliza registró al menos un
siniestro y 0 si no lo registró. Los datos son sintéticos y se utilizan
únicamente con fines académicos.
El siguiente bloque busca el archivo en la carpeta del proyecto. Si no lo encuentra, abre una ventana para seleccionarlo. Después muestra el tamaño y las primeras filas. Esta comprobación confirma que la base fue cargada y permite observar sus nombres de variables.
archivo <- "datos_semma_autos.csv"
if(!file.exists(archivo)) archivo <- file.choose()
autos <- read.csv(archivo, stringsAsFactors=FALSE)
dim(autos)## [1] 1800 11
La unidad es la póliza y tuvo_siniestro vale 1 si hubo
al menos un evento.
En esta etapa se conserva aproximadamente el 70% de cada clase para entrenamiento y el resto para prueba. La separación se hace dentro de los grupos con y sin siniestro para evitar que una de las bases quede con una proporción demasiado diferente.
indices_entrenamiento <- unlist(lapply(split(seq_len(nrow(autos)), autos$tuvo_siniestro), function(i) sample(i, floor(.70*length(i)))))
entrenamiento <- autos[indices_entrenamiento, ]
prueba <- autos[-indices_entrenamiento, ]
data.frame(base=c("Entrenamiento","Prueba"), filas=c(nrow(entrenamiento),nrow(prueba)), tasa_siniestro=round(100*c(mean(entrenamiento$tuvo_siniestro),mean(prueba$tuvo_siniestro)),2)) |> kable()| base | filas | tasa_siniestro |
|---|---|---|
| Entrenamiento | 1259 | 4.92 |
| Prueba | 541 | 5.18 |
Las dos bases conservan una proporción parecida de siniestros. Una sirve para aprender y la otra para comprobar. La tabla también permite verificar que no se utilizó toda la información para entrenar, lo que sería una evaluación poco confiable.
Primero se calcula, para cada variable, la cantidad de valores faltantes y de valores diferentes. Luego se resumen las variables numéricas. Esta revisión permite detectar campos incompletos y reconocer rangos antes de modificar la información.
calidad <- data.frame(variable=names(entrenamiento), faltantes=sapply(entrenamiento, function(x) sum(is.na(x))), unicos=sapply(entrenamiento, function(x) length(unique(x))))
kable(calidad)| variable | faltantes | unicos | |
|---|---|---|---|
| id_poliza | id_poliza | 0 | 1259 |
| edad_conductor | edad_conductor | 0 | 63 |
| antiguedad_poliza | antiguedad_poliza | 0 | 209 |
| tipo_vehiculo | tipo_vehiculo | 0 | 3 |
| uso_vehiculo | uso_vehiculo | 0 | 2 |
| region | region | 0 | 5 |
| valor_vehiculo | valor_vehiculo | 4 | 739 |
| km_anuales | km_anuales | 11 | 1205 |
| exposicion | exposicion | 0 | 46 |
| tuvo_siniestro | tuvo_siniestro | 0 | 2 |
| costo_siniestro | costo_siniestro | 0 | 63 |
summary(entrenamiento[,c("edad_conductor","antiguedad_poliza","valor_vehiculo","km_anuales","exposicion")])## edad_conductor antiguedad_poliza valor_vehiculo km_anuales
## Min. :18.00 Min. : 0.200 Min. : 12900000 Min. : -100
## 1st Qu.:34.00 1st Qu.: 4.700 1st Qu.: 46900000 1st Qu.: 9556
## Median :50.00 Median : 7.500 Median : 64300000 Median :13952
## Mean :49.72 Mean : 8.279 Mean : 71180478 Mean :14216
## 3rd Qu.:66.00 3rd Qu.:10.850 3rd Qu.: 87300000 3rd Qu.:18603
## Max. :80.00 Max. :30.000 Max. :338600000 Max. :40633
## NAs :4 NAs :11
## exposicion
## Min. :0.5500
## 1st Qu.:0.6600
## Median :0.7700
## Mean :0.7712
## 3rd Qu.:0.8800
## Max. :1.0000
##
resumen_uso <- entrenamiento |> group_by(uso_vehiculo) |> summarise(polizas=n(), tasa_siniestro=mean(tuvo_siniestro), .groups="drop")
kable(resumen_uso, digits=3)| uso_vehiculo | polizas | tasa_siniestro |
|---|---|---|
| Comercial | 200 | 0.085 |
| Particular | 1059 | 0.042 |
ggplot(resumen_uso, aes(uso_vehiculo,tasa_siniestro,fill=uso_vehiculo))+geom_col(show.legend=FALSE)+scale_y_continuous(labels=percent)+labs(title="Tasa observada por uso",x="Uso",y="Tasa")+theme_minimal()ggplot(entrenamiento,aes(factor(tuvo_siniestro),km_anuales))+geom_boxplot(fill="#6BAED6")+labs(title="Kilometraje según resultado",x="Siniestro: 0=No, 1=Sí",y="Kilómetros anuales")+theme_minimal()La tabla de calidad indica dónde hay faltantes y qué variables funcionan como categorías o como valores continuos. Los gráficos permiten comparar la tasa observada por uso y la distribución del kilometraje. Estas diferencias son señales para el análisis, pero no demuestran que una variable sea la causa del siniestro.
La exploración mostró valores faltantes y algunos kilometrajes negativos. El bloque siguiente convierte los kilometrajes negativos en faltantes, completa kilometraje y valor del vehículo con medianas del entrenamiento y crea grupos comprensibles de edad y kilometraje. La misma regla se aplica a prueba sin calcular nuevas medianas con esos datos.
preparar <- function(datos, referencia) {
datos$km_anuales[datos$km_anuales < 0] <- NA
med_km <- median(referencia$km_anuales[referencia$km_anuales >= 0], na.rm=TRUE)
med_valor <- median(referencia$valor_vehiculo, na.rm=TRUE)
datos$km_anuales[is.na(datos$km_anuales)] <- med_km
datos$valor_vehiculo[is.na(datos$valor_vehiculo)] <- med_valor
datos$grupo_edad <- cut(datos$edad_conductor,c(17,25,40,60,Inf),labels=c("18-25","26-40","41-60","61+"))
datos$grupo_km <- cut(datos$km_anuales,c(-Inf,10000,20000,Inf),labels=c("Bajo","Medio","Alto"))
datos$tipo_vehiculo <- factor(datos$tipo_vehiculo)
datos$uso_vehiculo <- factor(datos$uso_vehiculo)
datos$region <- factor(datos$region)
datos
}
entrenamiento_m <- preparar(entrenamiento,entrenamiento)
prueba_m <- preparar(prueba,entrenamiento)
summary(entrenamiento_m[,c("km_anuales","valor_vehiculo","grupo_edad","grupo_km")])## km_anuales valor_vehiculo grupo_edad grupo_km
## Min. : 1000 Min. : 12900000 18-25:158 Bajo :345
## 1st Qu.: 9616 1st Qu.: 46900000 26-40:285 Medio:680
## Median :13966 Median : 64300000 41-60:394 Alto :234
## Mean :14258 Mean : 71158618 61+ :422
## 3rd Qu.:18509 3rd Qu.: 87200000
## Max. :40633 Max. :338600000
Con los datos preparados se construyen dos modelos. La regresión logística estima una probabilidad, mientras que el árbol intenta formar grupos mediante reglas. Usar dos métodos ayuda a comparar un enfoque numérico y otro más visual.
modelo_logistico <- glm(tuvo_siniestro ~ edad_conductor + antiguedad_poliza + tipo_vehiculo + uso_vehiculo + region + valor_vehiculo + km_anuales + exposicion, data=entrenamiento_m, family=binomial())
summary(modelo_logistico)##
## Call:
## glm(formula = tuvo_siniestro ~ edad_conductor + antiguedad_poliza +
## tipo_vehiculo + uso_vehiculo + region + valor_vehiculo +
## km_anuales + exposicion, family = binomial(), data = entrenamiento_m)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.723e+00 1.150e+00 -3.238 0.0012 **
## edad_conductor 4.251e-03 7.209e-03 0.590 0.5555
## antiguedad_poliza -3.411e-02 2.973e-02 -1.147 0.2512
## tipo_vehiculoPickup 5.928e-01 3.330e-01 1.780 0.0750 .
## tipo_vehiculoSUV -4.898e-02 3.218e-01 -0.152 0.8791
## uso_vehiculoParticular -5.797e-01 3.722e-01 -1.557 0.1194
## regionBogota 3.549e-01 3.646e-01 0.973 0.3304
## regionCaribe 2.551e-01 4.571e-01 0.558 0.5767
## regionCentro 1.214e-02 4.416e-01 0.027 0.9781
## regionPacifico -3.084e-01 5.128e-01 -0.601 0.5476
## valor_vehiculo 1.960e-09 3.801e-09 0.516 0.6061
## km_anuales 1.661e-05 2.426e-05 0.685 0.4936
## exposicion 8.551e-01 1.019e+00 0.839 0.4014
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 494.25 on 1258 degrees of freedom
## Residual deviance: 479.02 on 1246 degrees of freedom
## AIC: 505.02
##
## Number of Fisher Scoring iterations: 6
modelo_arbol <- rpart(
factor(tuvo_siniestro) ~ edad_conductor + antiguedad_poliza + tipo_vehiculo + uso_vehiculo + region + valor_vehiculo + km_anuales + exposicion,
data=entrenamiento_m,
method="class",
parms=list(loss=matrix(c(0,1,3,0),nrow=2,byrow=TRUE)),
control=rpart.control(cp=.001,minsplit=20,minbucket=7,maxdepth=5,xval=10)
)
modelo_arbol## n= 1259
##
## node), split, n, loss, yval, (yprob)
## * denotes terminal node
##
## 1) root 1259 186 0 (0.95075457 0.04924543)
## 2) uso_vehiculo=Particular 1059 135 0 (0.95750708 0.04249292)
## 4) antiguedad_poliza>=1.55 1038 123 0 (0.96050096 0.03949904)
## 8) tipo_vehiculo=Automovil,SUV 891 90 0 (0.96632997 0.03367003) *
## 9) tipo_vehiculo=Pickup 147 33 0 (0.92517007 0.07482993)
## 18) valor_vehiculo< 1.278e+08 140 27 0 (0.93571429 0.06428571) *
## 19) valor_vehiculo>=1.278e+08 7 5 1 (0.71428571 0.28571429) *
## 5) antiguedad_poliza< 1.55 21 12 0 (0.80952381 0.19047619)
## 10) region=Caribe,Pacifico 8 0 0 (1.00000000 0.00000000) *
## 11) region=Antioquia,Bogota,Centro 13 9 1 (0.69230769 0.30769231) *
## 3) uso_vehiculo=Comercial 200 51 0 (0.91500000 0.08500000)
## 6) antiguedad_poliza< 18.95 192 42 0 (0.92708333 0.07291667)
## 12) valor_vehiculo< 9.165e+07 153 21 0 (0.95424837 0.04575163)
## 24) exposicion< 0.985 146 15 0 (0.96575342 0.03424658) *
## 25) exposicion>=0.985 7 5 1 (0.71428571 0.28571429) *
## 13) valor_vehiculo>=9.165e+07 39 21 0 (0.82051282 0.17948718)
## 26) km_anuales>=23000.5 19 0 0 (1.00000000 0.00000000) *
## 27) km_anuales< 23000.5 20 13 1 (0.65000000 0.35000000)
## 54) km_anuales>=19351.5 11 6 0 (0.81818182 0.18181818) *
## 55) km_anuales< 19351.5 9 4 1 (0.44444444 0.55555556) *
## 7) antiguedad_poliza>=18.95 8 5 1 (0.62500000 0.37500000) *
if(nrow(modelo_arbol$frame)>1){
plot(modelo_arbol,uniform=TRUE,margin=.08)
text(modelo_arbol,use.n=TRUE,cex=.7)
} else {
plot.new()
text(.5,.5,"El árbol quedó solo con el nodo inicial.\nNo encontró una división suficientemente útil.",cex=1.1)
}La matriz de pérdidas da más importancia académica a no omitir
pólizas con siniestro. No es una regla universal. Además, la validación
evita que Knit falle si el árbol queda solo con la
raíz.
La evaluación utiliza exclusivamente la base de prueba. El primer bloque genera probabilidades, aplica un corte de 0,30 y resume los aciertos y errores. Ese corte es ilustrativo: en una aplicación real tendría que relacionarse con el coste de revisar una póliza y el coste de omitir un riesgo.
prob_log <- predict(modelo_logistico,newdata=prueba_m,type="response")
prob_arbol_matriz <- predict(modelo_arbol,newdata=prueba_m,type="prob")
if("1" %in% colnames(prob_arbol_matriz)) {
prob_arbol <- prob_arbol_matriz[,"1"]
} else {
prob_arbol <- rep(mean(entrenamiento_m$tuvo_siniestro),nrow(prueba_m))
}
auc_manual <- function(real,prob){
pos <- prob[real==1]; neg <- prob[real==0]
mean(outer(pos,neg,">")) + .5*mean(outer(pos,neg,"=="))
}
metricas <- function(real,prob,corte=.30){
pred <- ifelse(prob>=corte,1,0)
tp <- sum(pred==1 & real==1); tn <- sum(pred==0 & real==0)
fp <- sum(pred==1 & real==0); fn <- sum(pred==0 & real==1)
data.frame(exactitud=(tp+tn)/length(real), sensibilidad=ifelse(tp+fn>0,tp/(tp+fn),NA), especificidad=ifelse(tn+fp>0,tn/(tn+fp),NA), auc=auc_manual(real,prob), positivos_detectados=tp, siniestros_omitidos=fn)
}
resultado <- bind_rows(Logistico=metricas(prueba_m$tuvo_siniestro,prob_log),Arbol=metricas(prueba_m$tuvo_siniestro,prob_arbol),.id="modelo")
kable(resultado,digits=3)| modelo | exactitud | sensibilidad | especificidad | auc | positivos_detectados | siniestros_omitidos |
|---|---|---|---|---|---|---|
| Logistico | 0.948 | 0 | 1.000 | 0.640 | 0 | 28 |
| Arbol | 0.909 | 0 | 0.959 | 0.487 | 0 | 28 |
clasificacion <- ifelse(prob_log>=.30,1,0)
table(Real=prueba_m$tuvo_siniestro,Predicho=clasificacion)## Predicho
## Real 0
## 0 513
## 1 28
resultado_prueba <- prueba_m |> mutate(probabilidad=prob_log,decil=ntile(probabilidad,10)) |> group_by(decil) |> summarise(polizas=n(),probabilidad_promedio=mean(probabilidad),tasa_real=mean(tuvo_siniestro),.groups="drop")
ggplot(resultado_prueba,aes(decil,tasa_real))+geom_line(linewidth=1,color="#C0392B")+geom_point(size=2)+scale_y_continuous(labels=percent)+labs(title="Tasa real por decil",x="Decil de riesgo",y="Tasa observada")+theme_minimal()El resultado permite ordenar pólizas por probabilidad estimada. Una aplicación prudente sería usar los grupos superiores para revisión preventiva o campañas de conducción segura, no para decisiones automáticas.
Recomendaciones: revisar las pólizas de mayor probabilidad; comprobar si kilometraje y uso comercial mantienen importancia con datos reales; definir el corte con costes actuariales; validar en periodos posteriores; y vigilar cambios en la población.
Una aseguradora de motocicletas quiere reconocer qué pólizas
presentan mayor probabilidad de siniestro para orientar acciones
preventivas. El archivo reto_semma_motos.csv contiene datos
sintéticos sobre edad, cilindraje, uso, ciudad, kilometraje, uso de
casco, exposición y resultado del siniestro.
El reto consiste en aplicar las cinco fases SEMMA y entregar una recomendación. La guía siguiente explica la ruta, pero deja espacios e incógnitas que los compañeros deben completar.
reto <- read.csv("reto_semma_motos.csv")
# Complete: número de filas y columnas
__________(reto)
# Complete: primeras observaciones
__________(reto)
# Pista: revise la proporción de 0 y 1
table(reto$________________)Pregunta: ¿cuál es la unidad de análisis y cuál es la variable objetivo?
La tarea es crear una base de entrenamiento y otra de prueba. Se recomienda mantener proporciones parecidas de pólizas con y sin siniestro.
set.seed(_____)
indices <- sample(
seq_len(nrow(reto)),
size = floor(_____ * nrow(reto))
)
entrenamiento_reto <- reto[__________, ]
prueba_reto <- reto[__________, ]
# Compare la tasa de siniestro
mean(entrenamiento_reto$________________)
mean(prueba_reto$________________)Incógnita: ¿qué porcentaje utilizará para entrenamiento y por qué?
# Cantidad de faltantes por variable
sapply(entrenamiento_reto, function(x) sum(__________(x)))
# Tasa por uso
entrenamiento_reto |>
group_by(________________) |>
summarise(
polizas = n(),
tasa_siniestro = mean(________________)
)
# Complete un gráfico de kilometraje según siniestro
ggplot(
entrenamiento_reto,
aes(x = factor(________________),
y = ________________)
) +
geom_boxplot()Preguntas: ¿qué categorías muestran tasas distintas?, ¿hay valores faltantes o extremos?, ¿qué diferencias observa sin afirmar causalidad?
# Ejemplo: crear grupos de kilometraje
entrenamiento_reto <- entrenamiento_reto |>
mutate(
grupo_km = cut(
km_anuales,
breaks = c(-Inf, _____, _____, Inf),
labels = c("Bajo", "Medio", "Alto")
),
uso = factor(__________),
ciudad = factor(__________)
)
# Decida cómo tratar faltantes y escriba la justificación.Incógnita: ¿qué límites utilizará para los grupos y qué información usaría para justificarlos?
modelo_reto <- glm(
tuvo_siniestro ~ edad_conductor +
cilindraje + uso + ciudad +
km_anuales + usa_casco + exposicion,
data = __________________,
family = binomial()
)
summary(__________________)Como alternativa, puede construirse un árbol con
rpart(). El compañero debe decidir si prueba un modelo o
compara dos.
probabilidad_reto <- predict(
modelo_reto,
newdata = __________________,
type = "response"
)
corte <- _____
prediccion_reto <- ifelse(
probabilidad_reto >= corte,
1,
0
)
table(
Real = prueba_reto$________________,
Predicho = __________________
)Incógnitas: ¿qué corte utilizará?, ¿cuántos siniestros reales quedan sin detectar?, ¿qué error considera más importante y por qué?
SEMMA ordena el paso desde los datos hasta la evaluación. Su fortaleza es recordar que explorar y transformar es tan importante como modelar.
El resultado no es una certeza individual. Sirve para ordenar pólizas según patrones sintéticos. La decisión final necesita contexto actuarial, costes de error, validación con datos reales y seguimiento.
Idea de cierre: SEMMA no es solo entrenar un modelo; es preparar una muestra, comprenderla, mejorarla, modelarla y comprobar si sirve para decidir.