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.
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.
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)
| 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 |
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)
| 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 | Sí | Sí | 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 | Sí | 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 | Sí | 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 | Sí | 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 | Sí | 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 | Sí | Sí | 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 | Sí | 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 | Sí | Sí | 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 | Sí | 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 | Sí | Bad |
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))
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”) |
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.
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 |
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.
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)
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.
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.
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.