Introducción

Para el desarrollo de este taller vamos a aplicar técnicas de preprocesamiento, limpieza, transformación y análisis exploratorio de datos, sobre una base de riesgo crediticio, con el fin de construir una versión depurada y documentada del conjunto de datos que servirá posteriormente para la construcción de modelos de clasificación.

El conjunto de datos German Credit contiene información de 1.000 solicitantes de crédito.

Objetivo

El objetivo principal es comprender la relación entre las características de los clientes y su comportamiento crediticio, representado por la variable objetivo default, que indica si el cliente presenta un buen comportamiento crediticio (Good = 1) o un mal comportamiento (Bad = 2).

Este análisis permitirá construir una base de datos depurada y estructurada para futuros modelos de clasificación.

  1. Diagnostico calidad de los datos

Carga y estructura del dataset

El conjunto de datos original se encuentra codificado mediante etiquetas alfanuméricas (A11, A12, etc.), por lo que inicialmente se realiza la carga y asignación de nombres de variables.

#Cargar datos
german <- read.table("german.data", header = FALSE, sep = "")

#Actualizar nombres de las variables
names(german) <- c(
  "checking_status",
  "duration",
  "credit_history",
  "purpose",
  "credit_amount",
  "savings",
  "employment",
  "installment_rate",
  "personal_status_sex",
  "other_debtors",
  "residence_since",
  "property",
  "age",
  "other_installment_plans",
  "housing",
  "existing_credits",
  "job",
  "people_liable",
  "telephone",
  "foreign_worker",
  "default"
)
head(german,10)%>% kable(caption = "Tabla de datos( primeras 10 posiciones )") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
Tabla de datos( primeras 10 posiciones )
checking_status duration credit_history purpose credit_amount savings employment installment_rate personal_status_sex other_debtors residence_since property age other_installment_plans housing existing_credits job people_liable telephone foreign_worker default
A11 6 A34 A43 1169 A65 A75 4 A93 A101 4 A121 67 A143 A152 2 A173 1 A192 A201 1
A12 48 A32 A43 5951 A61 A73 2 A92 A101 2 A121 22 A143 A152 1 A173 1 A191 A201 2
A14 12 A34 A46 2096 A61 A74 2 A93 A101 3 A121 49 A143 A152 1 A172 2 A191 A201 1
A11 42 A32 A42 7882 A61 A74 2 A93 A103 4 A122 45 A143 A153 1 A173 2 A191 A201 1
A11 24 A33 A40 4870 A61 A73 3 A93 A101 4 A124 53 A143 A153 2 A173 2 A191 A201 2
A14 36 A32 A46 9055 A65 A73 2 A93 A101 4 A124 35 A143 A153 1 A172 2 A192 A201 1
A14 24 A32 A42 2835 A63 A75 3 A93 A101 4 A122 53 A143 A152 1 A173 1 A191 A201 1
A12 36 A32 A41 6948 A61 A73 2 A93 A101 2 A123 35 A143 A151 1 A174 1 A192 A201 1
A14 12 A32 A43 3059 A64 A74 2 A91 A101 4 A121 61 A143 A152 1 A172 1 A191 A201 1
A12 30 A34 A40 5234 A61 A71 4 A94 A101 2 A123 28 A143 A152 2 A174 1 A191 A201 2

Recodificación de variables

Para mejorar la interpretabilidad del conjunto de datos, se realiza la recodificación de las variables categóricas utilizando la documentación oficial del dataset.

library(dplyr)

german <- german %>%
  mutate(

    # 1. Estado cuenta corriente
    checking_status = recode(checking_status,
      "A11" = "< 0 DM",
      "A12" = "0 - 200 DM",
      "A13" = ">= 200 DM",
      "A14" = "Sin cuenta"
    ),

    # 2. Historial crediticio
    credit_history = recode(credit_history,
      "A30" = "Todos pagados",
      "A31" = "Pagados en este banco",
      "A32" = "Pagando normalmente",
      "A33" = "Retrasos previos",
      "A34" = "Cuenta crítica"
    ),

    # 3. Propósito del crédito
    purpose = recode(purpose,
      "A40" = "Carro nuevo",
      "A41" = "Carro usado",
      "A42" = "Muebles/equipos",
      "A43" = "Radio/TV",
      "A44" = "Electrodomésticos",
      "A45" = "Reparaciones",
      "A46" = "Educación",
      "A48" = "Reentrenamiento",
      "A49" = "Negocio",
      "A410" = "Otros"
    ),

    # 4. Ahorros
    savings = recode(savings,
      "A61" = "<100 DM",
      "A62" = "100-500 DM",
      "A63" = "500-1000 DM",
      "A64" = ">=1000 DM",
      "A65" = "Sin ahorros"
    ),

    # 5. Antigüedad laboral
    employment = recode(employment,
      "A71" = "Desempleado",
      "A72" = "<1 año",
      "A73" = "1-4 años",
      "A74" = "4-7 años",
      "A75" = ">=7 años"
    ),

    # 6. Estado civil y sexo
    personal_status_sex = recode(personal_status_sex,
      "A91" = "Hombre divorciado",
      "A92" = "Mujer casada/divorciada",
      "A93" = "Hombre soltero",
      "A94" = "Hombre casado",
      "A95" = "Mujer soltera"
    ),

    # 7. Otros deudores
    other_debtors = recode(other_debtors,
      "A101" = "Ninguno",
      "A102" = "Codeudor",
      "A103" = "Garante"
    ),

    # 8. Propiedad
    property = recode(property,
      "A121" = "Bien raíz",
      "A122" = "Seguro de vida",
      "A123" = "Vehículo u otro",
      "A124" = "Sin propiedad"
    ),

    # 9. Otros planes de pago
    other_installment_plans = recode(other_installment_plans,
      "A141" = "Banco",
      "A142" = "Almacén",
      "A143" = "Ninguno"
    ),

    # 10. Vivienda
    housing = recode(housing,
      "A151" = "Arriendo",
      "A152" = "Propia",
      "A153" = "Gratis"
    ),

    # 11. Ocupación
    job = recode(job,
      "A171" = "Desempleado",
      "A172" = "No calificado",
      "A173" = "Calificado",
      "A174" = "Profesional/Directivo"
    ),

    # 12. Teléfono
    telephone = recode(telephone,
      "A191" = "No",
      "A192" = "Sí"
    ),

    # 13. Trabajador extranjero
    foreign_worker = recode(foreign_worker,
      "A201" = "Sí",
      "A202" = "No"
    ),

    # 14. Variable objetivo
    default = recode(as.character(default),
      "1" = "Good",
      "2" = "Bad"
    )
  )

head(german,10)%>% kable(caption = "Tabla de datos con descripciones( primeras 10 posiciones )") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
Tabla de datos con descripciones( primeras 10 posiciones )
checking_status duration credit_history purpose credit_amount savings employment installment_rate personal_status_sex other_debtors residence_since property age other_installment_plans housing existing_credits job people_liable telephone foreign_worker default
< 0 DM 6 Cuenta crítica Radio/TV 1169 Sin ahorros >=7 años 4 Hombre soltero Ninguno 4 Bien raíz 67 Ninguno Propia 2 Calificado 1 Good
0 - 200 DM 48 Pagando normalmente Radio/TV 5951 <100 DM 1-4 años 2 Mujer casada/divorciada Ninguno 2 Bien raíz 22 Ninguno Propia 1 Calificado 1 No Bad
Sin cuenta 12 Cuenta crítica Educación 2096 <100 DM 4-7 años 2 Hombre soltero Ninguno 3 Bien raíz 49 Ninguno Propia 1 No calificado 2 No Good
< 0 DM 42 Pagando normalmente Muebles/equipos 7882 <100 DM 4-7 años 2 Hombre soltero Garante 4 Seguro de vida 45 Ninguno Gratis 1 Calificado 2 No Good
< 0 DM 24 Retrasos previos Carro nuevo 4870 <100 DM 1-4 años 3 Hombre soltero Ninguno 4 Sin propiedad 53 Ninguno Gratis 2 Calificado 2 No Bad
Sin cuenta 36 Pagando normalmente Educación 9055 Sin ahorros 1-4 años 2 Hombre soltero Ninguno 4 Sin propiedad 35 Ninguno Gratis 1 No calificado 2 Good
Sin cuenta 24 Pagando normalmente Muebles/equipos 2835 500-1000 DM >=7 años 3 Hombre soltero Ninguno 4 Seguro de vida 53 Ninguno Propia 1 Calificado 1 No Good
0 - 200 DM 36 Pagando normalmente Carro usado 6948 <100 DM 1-4 años 2 Hombre soltero Ninguno 2 Vehículo u otro 35 Ninguno Arriendo 1 Profesional/Directivo 1 Good
Sin cuenta 12 Pagando normalmente Radio/TV 3059 >=1000 DM 4-7 años 2 Hombre divorciado Ninguno 4 Bien raíz 61 Ninguno Propia 1 No calificado 1 No Good
0 - 200 DM 30 Cuenta crítica Carro nuevo 5234 <100 DM Desempleado 4 Hombre casado Ninguno 2 Vehículo u otro 28 Ninguno Propia 2 Profesional/Directivo 1 No Bad

Resultados de calidad de datos

Se realiza un diagnóstico general del dataset evaluando:

kable(summary(german))
checking_status duration credit_history purpose credit_amount savings employment installment_rate personal_status_sex other_debtors residence_since property age other_installment_plans housing existing_credits job people_liable telephone foreign_worker default
Length :1000 Min. : 4.0 Length :1000 Length :1000 Min. : 250 Length :1000 Length :1000 Min. :1.000 Length :1000 Length :1000 Min. :1.000 Length :1000 Min. :19.00 Length :1000 Length :1000 Min. :1.000 Length :1000 Min. :1.000 Length :1000 Length :1000 Length :1000
N.unique : 4 1st Qu.:12.0 N.unique : 5 N.unique : 10 1st Qu.: 1366 N.unique : 5 N.unique : 5 1st Qu.:2.000 N.unique : 4 N.unique : 3 1st Qu.:2.000 N.unique : 4 1st Qu.:27.00 N.unique : 3 N.unique : 3 1st Qu.:1.000 N.unique : 4 1st Qu.:1.000 N.unique : 2 N.unique : 2 N.unique : 2
N.blank : 0 Median :18.0 N.blank : 0 N.blank : 0 Median : 2320 N.blank : 0 N.blank : 0 Median :3.000 N.blank : 0 N.blank : 0 Median :3.000 N.blank : 0 Median :33.00 N.blank : 0 N.blank : 0 Median :1.000 N.blank : 0 Median :1.000 N.blank : 0 N.blank : 0 N.blank : 0
Min.nchar: 6 Mean :20.9 Min.nchar: 13 Min.nchar: 5 Mean : 3271 Min.nchar: 7 Min.nchar: 6 Mean :2.973 Min.nchar: 13 Min.nchar: 7 Mean :2.845 Min.nchar: 9 Mean :35.55 Min.nchar: 5 Min.nchar: 6 Mean :1.407 Min.nchar: 10 Mean :1.155 Min.nchar: 2 Min.nchar: 2 Min.nchar: 3
Max.nchar: 10 3rd Qu.:24.0 Max.nchar: 21 Max.nchar: 17 3rd Qu.: 3972 Max.nchar: 11 Max.nchar: 11 3rd Qu.:4.000 Max.nchar: 23 Max.nchar: 8 3rd Qu.:4.000 Max.nchar: 15 3rd Qu.:42.00 Max.nchar: 7 Max.nchar: 8 3rd Qu.:2.000 Max.nchar: 21 3rd Qu.:1.000 Max.nchar: 2 Max.nchar: 2 Max.nchar: 4
NA Max. :72.0 NA NA Max. :18424 NA NA Max. :4.000 NA NA Max. :4.000 NA Max. :75.00 NA NA Max. :4.000 NA Max. :2.000 NA NA NA

No se identificaron valores faltantes en ninguna variable, lo que indica una base de datos completa y estructuralmente consistente.

library(naniar)

naniar::vis_miss(german)

naniar::vis_miss(german) + 
  theme(axis.text.x = element_text(size = 10, angle = 60, hjust = 1))

Validación de reglas de negocio

Se verifican condiciones lógicas básicas como:

Edad > 0 Monto de crédito > 0 Duración positiva Variable objetivo válida

No se encontraron violaciones en las reglas definidas, lo que confirma la coherencia interna del dataset.

library(validate)

Rules <- validator(

 duration > 0,

 credit_amount > 0,

 age > 0,

 installment_rate > 0,

 residence_since >= 0,

 existing_credits > 0,

 people_liable > 0,

 default %in% c("Good","Bad")

)

kable(summary(confront(german, Rules)))
name items passes fails nNA error warning expression
V1 1000 1000 0 0 FALSE FALSE duration > 0
V2 1000 1000 0 0 FALSE FALSE credit_amount > 0
V3 1000 1000 0 0 FALSE FALSE age > 0
V4 1000 1000 0 0 FALSE FALSE installment_rate > 0
V5 1000 1000 0 0 FALSE FALSE residence_since - 0 >= -1e-08
V6 1000 1000 0 0 FALSE FALSE existing_credits > 0
V7 1000 1000 0 0 FALSE FALSE people_liable > 0
V8 1000 1000 0 0 FALSE FALSE default %vin% c(“Good”, “Bad”)

Detección de outliers

Se analizan variables numéricas mediante boxplot

library(dplyr)
par(mfrow = c(3, 3))
datos_num <- german %>%
  select(duration, credit_amount, installment_rate, age,
         residence_since, existing_credits, people_liable)

invisible(lapply(names(datos_num), function(var) {
  boxplot(datos_num[[var]],
          main = var,
          col = "lightblue")
}))

Se identifican valores atípicos principalmente en:

Sin embargo, estos valores corresponden a observaciones reales del comportamiento de los clientes, por lo que no se eliminan.

En análisis crediticio, los outliers pueden representar perfiles de alto riesgo y no necesariamente errores de captura.
id_out_uni <- function(x){
  out <- boxplot.stats(x)$out
  which(x %in% out)
}
outliers <- lapply(datos_num,id_out_uni)
kable(sapply(outliers,length))
x
duration 70
credit_amount 72
installment_rate 0
age 23
residence_since 0
existing_credits 6
people_liable 155

  1. Análisis exploratorio

Distribución de la variable objetivo (default)

La variable objetivo default presenta la siguiente distribución:

library(dplyr)

german %>%
  count(default) %>%
  mutate(pct = n / sum(n)) %>%
  knitr::kable() %>%
  kableExtra::kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE
  )
default n pct
Bad 300 0.3
Good 700 0.7
ggplot(german,
       aes(default,
           fill=default))+
 geom_bar()+ theme_minimal()

Existe un desbalance moderado en la variable objetivo, lo cual es relevante para la etapa de modelado, ya que podría afectar el desempeño de algoritmos de clasificación. Será necesario considerar técnicas de balanceo o métricas robustas.

Análisis descriptivo de variables cuantitativas

Se realiza un análisis descriptivo de variables numéricas.}

 psych::describe(datos_num) %>%
  data.frame() %>%
  kable() %>%
  kable_styling(full_width = FALSE)
vars n mean sd median trimmed mad min max range skew kurtosis se
duration 1 1000 20.903 12.0588145 18.0 19.47250 8.8956 4 72 68 1.0909038 0.9013804 0.3813332
credit_amount 2 1000 3271.258 2822.7368760 2319.5 2754.57500 1627.1535 250 18424 18174 1.9437827 4.2506403 89.2627776
installment_rate 3 1000 2.973 1.1187147 3.0 3.09125 1.4826 1 4 3 -0.5297551 -1.2140006 0.0353769
age 4 1000 35.546 11.3754686 33.0 34.16625 10.3782 19 75 56 1.0176791 0.5796408 0.3597239
residence_since 5 1000 2.845 1.1037179 3.0 2.93125 1.4826 1 4 3 -0.2717526 -1.3837822 0.0349026
existing_credits 6 1000 1.407 0.5776545 1.0 1.33375 0.0000 1 4 3 1.2687608 1.5812581 0.0182670
people_liable 7 1000 1.155 0.3620858 1.0 1.06875 0.0000 1 2 1 1.9037202 1.6257794 0.0114502
  datos_num %>%
 pivot_longer(everything()) %>%
 ggplot(aes(value))+
 geom_histogram(
   bins=25,
   fill="steelblue"
 )+
 facet_wrap(~name,
            scales="free")

Las variables muestran alta variabilidad, especialmente en el monto del crédito, lo que sugiere heterogeneidad en los perfiles de los solicitantes.

La matriz de correlación permite observar las relaciones lineales entre las variables numéricas del conjunto de datos. La inclusión de los coeficientes en cada celda facilita la interpretación de la intensidad y dirección de dichas relaciones.

library(corrplot)

cor_matrix <- cor(german %>% select(where(is.numeric)), use = "complete.obs")

corrplot(cor_matrix,
         method = "color",
         type = "upper",
         addCoef.col = "black",
         tl.cex = 0.8,
         number.cex = 0.7)

Análisis descriptivo de variables categóricas

Se analizan variables cualitativas mediante gráficos de barras.

library(ggplot2)

cat_vars <- names(german)[sapply(german, is.character)]

graficos <- lapply(cat_vars, function(var) {
  ggplot(german, aes(x = .data[[var]])) +
    geom_bar(fill = "steelblue") +
    labs(title = paste("Distribución de", var),
         x = var,
         y = "Frecuencia") +
    theme_minimal() +
    theme(axis.text.x = element_text(size = 8))
})

par(mfrow = c(3, 3))  # filas, columnas

library(patchwork)
wrap_plots(graficos, ncol = 3)

podemos destacar que:

Las variables categóricas presentan distribuciones desbalanceadas, lo cual puede aportar poder explicativo en la predicción del riesgo crediticio.

Comparación de variables según la categoría de default

Se comparan variables entre clientes Good y Bad.

Se observa que las variables numéricas presentan diferencias entre los grupos de default. Los clientes con incumplimiento tienden a tener montos de crédito más altos y mayor variabilidad en la duración del crédito.

En cuanto a variables categóricas, se evidencian diferencias en la distribución del propósito del crédito entre los grupos de default, lo que sugiere una posible relación entre el tipo de crédito y el riesgo de incumplimiento.

german %>%
  group_by(default) %>%
  summarise(
    media_credit = mean(credit_amount),
    media_duracion = mean(duration),
    media_edad = mean(age)) %>% 
    kable() %>% kable_styling(full_width = FALSE)
default media_credit media_duracion media_edad
Bad 3938.127 24.86000 33.96333
Good 2985.457 19.20714 36.22429

El primer gráfico muestra la distribución de los propósitos de crédito en la base de datos. El segundo gráfico permite analizar la proporción de incumplimiento (default) dentro de cada categoría, lo cual facilita identificar los tipos de crédito con mayor riesgo relativo.

ggplot(german,
aes(purpose))+
geom_bar()+
coord_flip()

ggplot(german, aes(x = purpose, fill = factor(default))) +
  geom_bar(position = "fill") +
  coord_flip()

Se observa que existen diferencias relevantes entre los clientes con buen y mal comportamiento crediticio. En particular, los clientes en situación de default tienden a presentar montos de crédito más altos y mayores duraciones de préstamo, lo que sugiere una posible relación entre el nivel de endeudamiento y el riesgo crediticio.

ggplot(german, aes(x = default, y = credit_amount)) +
  geom_boxplot(fill = "lightblue") +
  theme_minimal()      

ggplot(german, aes(x = default, y = duration)) +
  geom_boxplot(fill = "lightblue") +
  theme_minimal()

En las variables categóricas, se identifican diferencias en la distribución de características como el estado de la cuenta corriente, la vivienda y el propósito del crédito, lo que indica que estas variables pueden aportar información relevante para la clasificación del riesgo.

En general, los resultados sugieren que tanto variables financieras como socioeconómicas están asociadas al comportamiento de default.

ggplot(german, aes(x = checking_status, fill = default)) +
  geom_bar(position = "fill") +
  coord_flip()

ggplot(german, aes(x = housing, fill = default)) +
  geom_bar(position = "fill") +
  coord_flip()

Existen patrones diferenciados entre clientes con buen y mal comportamiento crediticio, lo cual sugiere que estas variables tienen capacidad predictiva.

Conclusiones

El desarrollo de este taller permitió aplicar de manera integral las técnicas de preprocesamiento, limpieza y análisis exploratorio sobre la base German Credit, dejando un conjunto de datos depurado, documentado y trazable para la posterior construcción de modelos de clasificación del riesgo crediticio.

Durante el diagnóstico de calidad se evidenció que el dataset, pese a estar originalmente codificado con etiquetas alfanuméricas (A11, A12, …), no presenta valores faltantes ni inconsistencias frente a las reglas de negocio definidas (edad, monto, duración, etc.), lo cual confirma su buena integridad estructural. La recodificación de las 13 variables categóricas resultó clave para transformar el dataset en una fuente interpretable y semánticamente útil para el análisis.

El análisis exploratorio reveló hallazgos relevantes para el modelado posterior:La variable objetivo presenta un desbalance moderado (70 % Good vs. 30 % Bad), lo que deberá considerarse en la fase de modelación (técnicas de balanceo como SMOTE, ponderación de clases o métricas robustas como AUC y F1).

Los clientes clasificados como Bad presentan en promedio mayores montos de crédito (3.938 DM vs. 2.985 DM) y mayor duración del préstamo (24,9 vs. 19,2 meses), lo que sugiere que los créditos más grandes y a más largo plazo están asociados a un mayor riesgo de incumplimiento.

Variables categóricas como checking_status, credit_history, purpose y housing muestran proporciones de default diferenciadas entre sus categorías, evidenciando su valor predictivo para los modelos.

Los outliers identificados en credit_amount, duration y age corresponden a observaciones reales (no errores de captura), por lo que se conservaron; sin embargo, será necesario evaluar su impacto en algoritmos sensibles a escala mediante transformaciones (log, Box-Cox) o algoritmos robustos (árboles, ensambles).

La matriz de correlación mostró baja multicolinealidad entre las variables numéricas, con la relación más destacada entre credit_amount y duration, lo cual es coherente con la lógica financiera.En síntesis, el dataset queda preparado para abordar la siguiente fase de modelación supervisada, contando con variables interpretables, sin datos faltantes y con un panorama claro de las relaciones más relevantes con la variable objetivo.