Estatus de la Salud Mental en adultos del municipio más acaudalado de América Látina.¶

Reporte técnico del análisis de la salud mental en adultos dentro de la población en edad de trabajar del municipio de San Pedro Garza García.

Hecho por: Estefania Nájera de la Rosa - estefania.najera@udem.edu a 26 de marzo del 2026.

In [174]:
# Importar las librerías.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
import statsmodels.formula.api as smf
import statsmodels.api as sm
from sklearn.preprocessing import LabelEncoder
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import ConfusionMatrixDisplay, classification_report
from sklearn.linear_model import LogisticRegression
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.tree import DecisionTreeClassifier as DTC
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, f1_score
from sklearn.model_selection import cross_val_score

1.1 Introducción.¶

Para este presente análisis, se seguirá utilizando la base de datos que ha sido explorada desde el proyecto del primer parcial y que dentro de este segundo parcial se estudió ahora con diversos enfoques clasificatorios. Esto bajo las condiciones establecidas donde los datos a utilizar sean aquellos de adultos en la edad económicamente activa (18-65 años) que residen en el municipio de San Pedro Garza García.

Dichos enfoques clasificatorios han sido trabajados anteriormente y se mostrará tanto su construcción como sus resultados para delimitar cuál algoritmo de clasificación es más compatible con el escenario a descubrir.

Vale la pena volver a mencionar que los datos con los que se estará trabajando, provienen del portal de datos abiertos del Gobierno del Estado de Nuevo León, estos recaen bajo la categoría de Salud - que nos da a entender que para el estado, la salud mental es un factor importante a considerar de la población de una región que constantemente se encuentra en crecimiento y mejora.

1.2 Objetivo.¶

El objetivo de seguir trabajando con la misma base de datos pero ahora bajo el término de clasificación, es para conocer en qué umbral recaen los adultos económicamente activos que residen en el municipio de San Pedro Garza García:

  • Trastornos mentales y del comportamiento.

  • Factores que influyen en el estado de salud y contacto con los servicios de salud.

De esta manera, respondiendo a la pregunta de: Cómo se encuentra el nivel de salud mental en los adultos que sostienen el desarrollo del municipio de San Pedro Garza García?

Esto debido a que aquellos que recaigan en el grupo de trastornos mentales, son individuos que se encuentran en condiciones graves de salud mental y que por ende, necesitan de apoyo y trato profesional más avanzado; a diferencia de aquellos que presentan factores que influyen en su estado de salud, los cuáles no se hacen de lado, sin embargo, no necesitan de un seguimiento tan estricto a comparación del primer grupo. Dicha delimitación da feedback de la calidad de vida de los Sanpetrinos.

Mencionando de nuevo el aspecto de la clasificación, es que se trabajarán con los siguientes modelos:

  • Regresión Logística.

  • Linear Discriminant Analysis (LDA).

  • Árbol de Decisión.

  • Métodos de ensamble (Random Forest Classifier).

  • Support Vector Machine (SVM).

  • Red Neuronal (Construida desde cero).

Cada uno de estos se explicará brevemente para justificar su uso y explicar sus resultados para al final delimitar el mejor.

2.1 Planteamiento.¶

Recordando la información del proyecto de primer parcial, según la OPS/OMS (2026), la salud mental se define como un estado del bienestar que permite al individuo afrontar momentos díficiles, desarrollar sus habilidades, aprender, enseñar y trabajar de forma adecuada para contribuir a la mejora de su comunidad.

Este bienestar es fundamental, ya que sustenta las capacidades individuales como colectivas que permiten dar forma al mundo.

En el estado de Nuevo León, actualmente se cuenta con un hospital de especialidades en salud mental, lo que evidencia que las autoridades reconocen la importancia de este bienestar en su población.

Por otro lado, el municipio de San Pedro Garza García se encuentra en México, exactamente en el estado de Nuevo León y es parte del área metropolitana de Monterrey, este es conocido como el mejor de América Latina por diversas razones. Según Pardo (2025), su PIB per cápita es de $107,000 dólares anuales, cinco veces el promedio nacional; además, el 70% de los adultos poseé de un título universitario, el tripe que el promedio del resto de México.

Es justo aquí donde unimos A con B, es decir, tenemos el aspecto de la salud mental justificado y planteado como el problema principal, directamente relacionado con el municipio de San Pedro Garza García, donde esa riqueza y altos niveles educativos generan expectativas y responsabilidades en los individuos que podría influir en su bienestar psicológico, dado que estos sostienen la imagen y el progreso del municipio mediante su esfuerzo constante.

Por ello, este análisis se centrará en los adultos de 18 a 65 años (la población económicamente activa) del municipio, quienes son los pilares que impulsan, organizan y mejoran la economía, la plusvalía y la calidad de vida deSan Pedro Garza García. La investigación parte de la premisa de que a pesar de vivir en una "burbuja de privilegios", estas personas no están exentas de presentar problemas, inquietudes o incertidumbres que afecten su salud mental, sea por razones académicas, laborales o personales.

2.2 Contexto de los datos.¶

Conocemos la procedencia de los datos, ahora se cargarán:

In [175]:
# Cargar los datos.
url = 'https://raw.githubusercontent.com/estefaniadelarosa/IA-I/refs/heads/main/P1.%20Regresi%C3%B3n/P1.%20Regresi%C3%B3n/2024_2025_salud_mental.csv'
df = pd.read_csv(url)
print(df.shape)
df.head()
(48224, 14)
Out[175]:
fecha id_consulta edad edad_meses edad_dias sexo peso altura municipio_unidad_medica institucion_unidad_medica clave_grupo_ enfermedad descripcion_grupo_enfermedad clave_enfermedad descripcion_enfermedad
0 02/10/2024 SM_2024_38869 21 0 0 Masculino 82 174 LINARES HOSPITAL GENERAL DE LINARES V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION
1 08/10/2024 SM_2024_38870 21 0 0 Masculino 82 174 LINARES HOSPITAL GENERAL DE LINARES V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION
2 08/10/2024 SM_2024_38871 5 0 0 Masculino 21 111 LINARES HOSPITAL GENERAL DE LINARES V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F919 TRASTORNO DE LA CONDUCTA NO ESPECIFICADO
3 09/10/2024 SM_2024_38872 69 0 0 Masculino sin valor sin valor LINARES HOSPITAL GENERAL DE LINARES V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F321 EPISODIO DEPRESIVO MODERADO
4 09/10/2024 SM_2024_38873 78 0 0 Masculino sin valor sin valor LINARES HOSPITAL GENERAL DE LINARES V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F321 EPISODIO DEPRESIVO MODERADO
In [176]:
df['municipio_unidad_medica'].unique()
Out[176]:
array(['LINARES', 'MONTEMORELOS', 'MONTERREY', 'SABINAS HIDALGO',
       'SAN NICOLAS DE LOS GARZA', 'SANTA CATARINA', 'GENERAL ESCOBEDO',
       'PESQUERIA', 'CADEREYTA JIMENEZ', 'SAN PEDRO GARZA GARCIA',
       'GARCIA', 'GALEANA', 'CERRALVO', 'CHINA'], dtype=object)

Los datos originales provienen de personas que residen en distintos municipios del estado de Nuevo León, por mencionar algunos que pertenecen al área metropólitana como:

  • Monterrey.

  • San Nicolás de los Garza.

  • Santa Catarina.

  • General Escobedo.

entre otros.

Sin dejar de lado que también hay datos de municipios aledaños a la metrópoli como lo son:

  • Montemorelos.

  • Linares.

  • Cadereyta Jiménez

entre otros.

Por cada municipio, es que se tiene registrado por individuo lo siguiente:

  • Fecha de la consulta.

  • Número de identificación de la consulta.

  • Básicos de la persona como edad, sexo, peso y altura.

  • Municipio.

  • Institución médica dentro del municipio.

  • Número de identificación y descripción de su grupo de enfermedad.

  • Número de identificación y descripción de la enfermedad como tal.

3.1 Exploración y comprensión del conjunto de datos.¶

Como se tiene el interés de trabajar con los datos que provengan de San Pedro Garza García, el dataset se filtrará para que mediante el municipio, recopilemos aquellos específicos que se necesitan.

In [177]:
df1 = df[df['municipio_unidad_medica'] == 'SAN PEDRO GARZA GARCIA']
print(df1.shape)
df1.head()
(1060, 14)
Out[177]:
fecha id_consulta edad edad_meses edad_dias sexo peso altura municipio_unidad_medica institucion_unidad_medica clave_grupo_ enfermedad descripcion_grupo_enfermedad clave_enfermedad descripcion_enfermedad
2625 02/10/2024 SM_2024_41494 33 0 0 Masculino 60 160 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F122 TRASTORNOS MENTALES Y DEL COMPORTAMIENTO DEBID...
2626 02/10/2024 SM_2024_41495 53 0 0 Femenino 60 160 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION
2627 02/10/2024 SM_2024_41496 60 0 0 Femenino 60 160 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F630 JUEGO PATOLOGICO
2628 04/10/2024 SM_2024_41497 46 0 0 Femenino 70 165 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION
2629 04/10/2024 SM_2024_41498 47 0 0 Femenino 60 160 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION

Seguido de aplicar otro filtro que ahora nos recopile todos los datos donde la edad se encuentre dentro del rango de los 18 a los 65 años como se había establecido anteriormente.

In [178]:
# Aplicar el filtro de personas dentro del rango de edad de 18 a 65 años.
df2 = df1[(df1['edad'] >= 18) & (df1['edad'] <= 65)]
print(df2.shape)
df2.head()
(720, 14)
Out[178]:
fecha id_consulta edad edad_meses edad_dias sexo peso altura municipio_unidad_medica institucion_unidad_medica clave_grupo_ enfermedad descripcion_grupo_enfermedad clave_enfermedad descripcion_enfermedad
2625 02/10/2024 SM_2024_41494 33 0 0 Masculino 60 160 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F122 TRASTORNOS MENTALES Y DEL COMPORTAMIENTO DEBID...
2626 02/10/2024 SM_2024_41495 53 0 0 Femenino 60 160 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION
2627 02/10/2024 SM_2024_41496 60 0 0 Femenino 60 160 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F630 JUEGO PATOLOGICO
2628 04/10/2024 SM_2024_41497 46 0 0 Femenino 70 165 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION
2629 04/10/2024 SM_2024_41498 47 0 0 Femenino 60 160 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION

Aspectos como los outliers y los huecos ya se trataron en el proyecto del primer parcial, sin embargo, se vuelve a recalcar en este punto que no existen datos vacíos.

In [179]:
# Verificar si hay datos vacíos.
df2.isna().sum().sort_values(ascending = False)
Out[179]:
fecha                           0
id_consulta                     0
edad                            0
edad_meses                      0
edad_dias                       0
sexo                            0
peso                            0
altura                          0
municipio_unidad_medica         0
institucion_unidad_medica       0
clave_grupo_ enfermedad         0
descripcion_grupo_enfermedad    0
clave_enfermedad                0
descripcion_enfermedad          0
dtype: int64

Dentro del objetivo se definió que se va a delimitar bajo este tipo de regresión la clasificación de un individuo bajo dos clases, siendo estas pertenecientes a la descripción del grupo de la enfermedad. De estas dos se mencionó su nombre y el propósito de clasificar a las personas bajo alguno de estos bajo las predicciones que se harán más adelante para ver la calidad del modelo según su predictor.

Una vez volviendo a clarificar lo anterior, cuando indagamos en el dataset a aspectos más técnicos de cuántos datos existen ya bajo alguna de las clases, podemos darnos cuenta que entre ambos existe un desbalance, mejor conocido como desbalance de clases dentro del mundo de Machine Learning, donde esto consiste en que las clases cuentan con una diferencia significativa de registros bajo los mismos, por ejemplo, en este caso obtenemos los siguientes números bajo cada clase:

  • Trastornos mentales y del comportamiento con 547 registros.

  • Factores que influyen en el estado de salud y contacto con los servicios de salud con 173 registros.

Por ende, obtenemos que hay más registros de personas bajo la descripción de trastornos mentales y del comportamiento - visto por porcentaje, de un 100%, estos encabezan el 75% de los datos, sin embargo, este desbalance no es un limitante para desarrollar el modelo ya que podemos aplicar técnicas para balancear esta diferencia, teniendo el desbalance original para hacer una comparación de desempeños entre los modelos.

In [180]:
df2['descripcion_grupo_enfermedad'].value_counts()
Out[180]:
descripcion_grupo_enfermedad
TRASTORNOS MENTALES Y DEL COMPORTAMIENTO                                             547
FACTORES QUE INFLUYEN EN EL ESTADO DE SALUD Y CONTACTO CON LOS SERVICIOS DE SALUD    173
Name: count, dtype: int64
In [181]:
# Para el porcentaje.
df2['descripcion_grupo_enfermedad'].value_counts()/(df2['descripcion_grupo_enfermedad'].value_counts().sum())*100
Out[181]:
descripcion_grupo_enfermedad
TRASTORNOS MENTALES Y DEL COMPORTAMIENTO                                             75.972222
FACTORES QUE INFLUYEN EN EL ESTADO DE SALUD Y CONTACTO CON LOS SERVICIOS DE SALUD    24.027778
Name: count, dtype: float64
In [182]:
# Gráfica del balance de clases.
df2['descripcion_grupo_enfermedad'].str[:43].value_counts().plot(kind = 'bar')
plt.show()
No description has been provided for this image

Transformaremos las variables categóricas a numéricas para poderlas manipular.

In [183]:
df2.dtypes
Out[183]:
fecha                           object
id_consulta                     object
edad                             int64
edad_meses                       int64
edad_dias                        int64
sexo                            object
peso                            object
altura                          object
municipio_unidad_medica         object
institucion_unidad_medica       object
clave_grupo_ enfermedad         object
descripcion_grupo_enfermedad    object
clave_enfermedad                object
descripcion_enfermedad          object
dtype: object

Los datos y su tipo que disponemos son los siguientes:

  • id_consulta → object

  • edad → int64

  • sexo → object

  • peso → object

  • altura → object

  • municipio_unidad_medica → object

  • institucion_unidad_medica → object

  • clave_grupo_ enfermedad → object

  • descripcion_grupo_enfermedad → object

  • clave_enfermedad → object

  • descripcion_enfermedad → object

Por ende, las que se transformarán mediante LabelEncoder son las siguientes:

  • sexo

  • institucion_unidad_medica

  • descripcion_grupo_enfermedad_num

  • clave_enfermedad_num

In [184]:
# Convertimos las variables categóricas a categóricas numéricas.
from sklearn.preprocessing import LabelEncoder 

# Aplicamos la transformación.
df2['sexo_num'] = LabelEncoder().fit_transform(df2['sexo'])
df2['institucion_unidad_medica_num'] = LabelEncoder().fit_transform(df2['institucion_unidad_medica'])
df2['descripcion_grupo_enfermedad_num'] = LabelEncoder().fit_transform(df2['descripcion_grupo_enfermedad'])
df2['clave_enfermedad_num'] = LabelEncoder().fit_transform(df2['clave_enfermedad'])
df2['descripcion_enfermedad_num'] = LabelEncoder().fit_transform(df2['descripcion_enfermedad'])

df2.sample(5)
/var/folders/rw/krrrqrzn68j3jq_d4yl8mzk80000gn/T/ipykernel_57416/763801538.py:5: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2['sexo_num'] = LabelEncoder().fit_transform(df2['sexo'])
/var/folders/rw/krrrqrzn68j3jq_d4yl8mzk80000gn/T/ipykernel_57416/763801538.py:6: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2['institucion_unidad_medica_num'] = LabelEncoder().fit_transform(df2['institucion_unidad_medica'])
/var/folders/rw/krrrqrzn68j3jq_d4yl8mzk80000gn/T/ipykernel_57416/763801538.py:7: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2['descripcion_grupo_enfermedad_num'] = LabelEncoder().fit_transform(df2['descripcion_grupo_enfermedad'])
/var/folders/rw/krrrqrzn68j3jq_d4yl8mzk80000gn/T/ipykernel_57416/763801538.py:8: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2['clave_enfermedad_num'] = LabelEncoder().fit_transform(df2['clave_enfermedad'])
/var/folders/rw/krrrqrzn68j3jq_d4yl8mzk80000gn/T/ipykernel_57416/763801538.py:9: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df2['descripcion_enfermedad_num'] = LabelEncoder().fit_transform(df2['descripcion_enfermedad'])
Out[184]:
fecha id_consulta edad edad_meses edad_dias sexo peso altura municipio_unidad_medica institucion_unidad_medica clave_grupo_ enfermedad descripcion_grupo_enfermedad clave_enfermedad descripcion_enfermedad sexo_num institucion_unidad_medica_num descripcion_grupo_enfermedad_num clave_enfermedad_num descripcion_enfermedad_num
11244 11/12/2024 SM_2024_50113 40 0 0 Femenino 86.5 156 SAN PEDRO GARZA GARCIA CENTRO DE SALUD CON SERVICIOS AMPLIADOS SAN PE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F411 TRASTORNO DE ANSIEDAD GENERALIZADA 0 1 1 31 29
32284 16/05/2025 SM_2025_19420 33 0 0 Femenino 57 153 SAN PEDRO GARZA GARCIA CENTRO DE SALUD CON SERVICIOS AMPLIADOS SAN PE... XXI FACTORES QUE INFLUYEN EN EL ESTADO DE SALUD Y ... Z630 PROBLEMAS EN LA RELACION ENTRE ESPOSOS O PAREJA 0 1 0 46 16
37258 17/06/2025 SM_2025_24394 42 0 0 Femenino 46.5 146 SAN PEDRO GARZA GARCIA CENTRO DE SALUD CON SERVICIOS AMPLIADOS SAN PE... XXI FACTORES QUE INFLUYEN EN EL ESTADO DE SALUD Y ... Z630 PROBLEMAS EN LA RELACION ENTRE ESPOSOS O PAREJA 0 1 0 46 16
41673 22/07/2025 SM_2025_28809 43 0 0 Masculino 75 175 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F100 TRASTORNOS MENTALES Y DEL COMPORTAMIENTO DEBID... 1 0 1 0 40
36851 11/06/2025 SM_2025_23987 24 0 0 Masculino 83 175 SAN PEDRO GARZA GARCIA CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... V TRASTORNOS MENTALES Y DEL COMPORTAMIENTO F412 TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION 1 0 1 32 38

Se eliminarán del dataset las variables siguientes:

  • edad_meses

  • edad_dias

Ya que estas solo contienen 0 como registro, no nos sirve para nada y solo genera ruido.

In [185]:
df2 = df2.drop(columns=['edad_meses', 'edad_dias'])

De igual forma, al tener variables de peso y altura respectivas, se puede unir ambas como una sola variable nueva, siendo esta la de IMC donde se crea dicha variable bajo la operación para encontrar este dato.

In [186]:
# Convertir peso y altura a enteros.
df2['peso'] = df2['peso'].astype(float)
df2['altura'] = df2['altura'].astype(float)

# Verificar.
print(df2.dtypes[['peso','altura']])
peso      float64
altura    float64
dtype: object
In [187]:
# Convertir altura a metros.
df2['altura_m'] = df2['altura'] / 100
# IMC.
df2['IMC'] = df2['peso'] / (df2['altura_m']**2)

4.1 Selección de características.¶

Para delimitar cuáles son las variables de entrada para el modelo, se usarán varios métodos:

  • Mapa de calor.

  • ANOVA.

  • VIF.

In [188]:
plt.figure(figsize = (10, 6))
sns.heatmap(df2.corr(numeric_only = True), annot = True)
plt.show()
No description has been provided for this image
In [189]:
# Definimos el modelo de Feature Selection - ANOVA.
modelFS = smf.ols(formula='descripcion_grupo_enfermedad_num ~ edad + sexo_num + IMC + institucion_unidad_medica_num + clave_enfermedad_num + descripcion_enfermedad_num', data = df2).fit()
modelFS.summary()
Out[189]:
OLS Regression Results
Dep. Variable: descripcion_grupo_enfermedad_num R-squared: 0.557
Model: OLS Adj. R-squared: 0.553
Method: Least Squares F-statistic: 149.4
Date: Tue, 24 Mar 2026 Prob (F-statistic): 1.74e-122
Time: 23:52:19 Log-Likelihood: -116.25
No. Observations: 720 AIC: 246.5
Df Residuals: 713 BIC: 278.6
Df Model: 6
Covariance Type: nonrobust
coef std err t P>|t| [0.025 0.975]
Intercept 1.2893 0.078 16.470 0.000 1.136 1.443
edad -0.0005 0.001 -0.575 0.565 -0.002 0.001
sexo_num -0.1472 0.026 -5.559 0.000 -0.199 -0.095
IMC -0.0018 0.002 -0.876 0.382 -0.006 0.002
institucion_unidad_medica_num -0.0496 0.026 -1.882 0.060 -0.101 0.002
clave_enfermedad_num -0.0189 0.001 -17.611 0.000 -0.021 -0.017
descripcion_enfermedad_num 0.0056 0.001 5.564 0.000 0.004 0.008
Omnibus: 758.401 Durbin-Watson: 1.802
Prob(Omnibus): 0.000 Jarque-Bera (JB): 46.874
Skew: 0.010 Prob(JB): 6.63e-11
Kurtosis: 1.750 Cond. No. 473.


Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
In [190]:
# Imprimimos los P value para ver cuál es más importante.
pvalue_percent = (modelFS.pvalues * 100).sort_values(ascending = True)
pvalue_percent
Out[190]:
clave_enfermedad_num             6.584618e-56
Intercept                        6.892180e-50
descripcion_enfermedad_num       3.738917e-06
sexo_num                         3.829040e-06
institucion_unidad_medica_num    6.021948e+00
IMC                              3.815116e+01
edad                             5.652051e+01
dtype: float64
In [191]:
X = df2[['edad', 'sexo_num', 'IMC', 'institucion_unidad_medica_num', 'clave_enfermedad_num', 'descripcion_enfermedad_num']]

vif_data = pd.DataFrame()
vif_data["feature"] = X.columns

vif_data["VIF"] = [variance_inflation_factor(X.values, i)
                          for i in range(len(X.columns))]
print(vif_data)
                         feature        VIF
0                           edad   9.406207
1                       sexo_num   2.008371
2                            IMC  20.720012
3  institucion_unidad_medica_num   2.930642
4           clave_enfermedad_num   8.236488
5     descripcion_enfermedad_num   6.445571

Según GeeksforGeeks (2025), para mejorar la fiabilidad del modelo bajo el criterio de la colinealidad, se utiliza este método de inflación de varianza, donde este muestra como la varianza aumenta debido a los valores tan parecidos entre las variables independientes.

Interpretación del FIV:

  • FIV ≈ 1: Sin correlación con otros predictores.

  • 1 < FIV ≤ 5: Correlación leve a moderada (generalmente fina).

  • FIV > 10: Fuerte multicolinealidad -> tomar medidas correctivas.

In [192]:
X = df2[['edad', 'sexo_num', 'institucion_unidad_medica_num', 'descripcion_enfermedad_num']]

vif_data = pd.DataFrame()
vif_data["feature"] = X.columns

vif_data["VIF"] = [variance_inflation_factor(X.values, i)
                          for i in range(len(X.columns))]
print(vif_data)
                         feature       VIF
0                           edad  4.187639
1                       sexo_num  1.918244
2  institucion_unidad_medica_num  2.000134
3     descripcion_enfermedad_num  4.060178

Según la interpretación del VIF, se eliminarán las variables siguientes:

  • IMC por su VIF de 20.

  • clave_enfermedad_num que es redundante con descripcion_enfermedad_num

Una vez haciendo la eliminación de estas variables, los VIF mejorán mucho más, estando todos de respectivas variables dentro del umbral de una correlación fina para la elaboración del modelo.

5.1 Definición de variables.¶

Variable dependiente.

Esta variable se tomará como descripcion_grupo_enfermedad_num, ya que desde un principio se delimitó que queremos evaluar dentro de que umbral se encontrará un individuo, siendo dentro de uno que necesite seguimiento profesional con un especialista o chequeos de vez en cuando sin la necesidad de dicho seguimiento tan riguroso.

Variables independientes.

Para las entradas del modelo, se usarán las siguientes después de evaluar la colinealidad de las variables:

  • edad

  • sexo_num

  • institucion_unidad_medica_num

Vale la pena mencionar que descripcion_enfermedad_num fue excluida intencionalmente. Esta variable es un subconjunto directo de la variable dependiente descripcion_grupo_enfermedad_num, lo que genera un caso de Data Leakage — específicamente de contaminación de datos externos — donde la fusión de datos externos con datos de entrenamiento puede generar predicciones sesgadas o inexactas ya que estos datos contienen información directa sobre la variable objetivo. Incluirla haría que el modelo aprenda la respuesta en lugar de las relaciones reales entre predictores y variable dependiente.

In [193]:
# Definimos entrada y salida.
X = df2[['edad', 'sexo_num', 'institucion_unidad_medica_num']]
y = df2['descripcion_grupo_enfermedad_num']
print(X.shape)
print(y.shape)
(720, 3)
(720,)

6.1 Construcción.¶

Antes de construir para este proyecto, anteriormente cuando se estuvo realizando la Regresión Logística, se tuvo como resultado que el mejor modelo balanceado es aquel bajo la técnica de SMOTE, por lo que se evaluará tanto el modelo desbalanceado como el de SMOTE, una vez habiendo clarificado esto para que todas las construcciones sean consistentes en cuánto que modelos vamos a estar probando.

Regresión Logística.¶

Logistic by Default.

Separamos los datos de entrenamiento y prueba:

In [194]:
# Dividir los datos en train y test.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0, stratify = y)
X_train.shape, X_test.shape, y_train.shape, y_test.shape
# No dividir de manera que se desbalanceen los datos -> cuidado con el random_state).
Out[194]:
((576, 3), (144, 3), (576,), (144,))

Construimos el modelo:

In [195]:
# Definimos el modelo.
model1 = LogisticRegression(random_state = 0)
# Entrenamos el modelo.
model1.fit(X_train, y_train)
Out[195]:
LogisticRegression(random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
penalty penalty: {'l1', 'l2', 'elasticnet', None}, default='l2'

Specify the norm of the penalty:

- `None`: no penalty is added;
- `'l2'`: add a L2 penalty term and it is the default choice;
- `'l1'`: add a L1 penalty term;
- `'elasticnet'`: both L1 and L2 penalty terms are added.

.. warning::
Some penalties may not work with some solvers. See the parameter
`solver` below, to know the compatibility between the penalty and
solver.

.. versionadded:: 0.19
l1 penalty with SAGA solver (allowing 'multinomial' + L1)

.. deprecated:: 1.8
`penalty` was deprecated in version 1.8 and will be removed in 1.10.
Use `l1_ratio` instead. `l1_ratio=0` for `penalty='l2'`, `l1_ratio=1` for
`penalty='l1'` and `l1_ratio` set to any float between 0 and 1 for
`'penalty='elasticnet'`.
'deprecated'
C C: float, default=1.0

Inverse of regularization strength; must be a positive float.
Like in support vector machines, smaller values specify stronger
regularization. `C=np.inf` results in unpenalized logistic regression.
For a visual example on the effect of tuning the `C` parameter
with an L1 penalty, see:
:ref:`sphx_glr_auto_examples_linear_model_plot_logistic_path.py`.
1.0
l1_ratio l1_ratio: float, default=0.0

The Elastic-Net mixing parameter, with `0 <= l1_ratio <= 1`. Setting
`l1_ratio=1` gives a pure L1-penalty, setting `l1_ratio=0` a pure L2-penalty.
Any value between 0 and 1 gives an Elastic-Net penalty of the form
`l1_ratio * L1 + (1 - l1_ratio) * L2`.

.. warning::
Certain values of `l1_ratio`, i.e. some penalties, may not work with some
solvers. See the parameter `solver` below, to know the compatibility between
the penalty and solver.

.. versionchanged:: 1.8
Default value changed from None to 0.0.

.. deprecated:: 1.8
`None` is deprecated and will be removed in version 1.10. Always use
`l1_ratio` to specify the penalty type.
0.0
dual dual: bool, default=False

Dual (constrained) or primal (regularized, see also
:ref:`this equation `) formulation. Dual formulation
is only implemented for l2 penalty with liblinear solver. Prefer `dual=False`
when n_samples > n_features.
False
tol tol: float, default=1e-4

Tolerance for stopping criteria.
0.0001
fit_intercept fit_intercept: bool, default=True

Specifies if a constant (a.k.a. bias or intercept) should be
added to the decision function.
True
intercept_scaling intercept_scaling: float, default=1

Useful only when the solver `liblinear` is used
and `self.fit_intercept` is set to `True`. In this case, `x` becomes
`[x, self.intercept_scaling]`,
i.e. a "synthetic" feature with constant value equal to
`intercept_scaling` is appended to the instance vector.
The intercept becomes
``intercept_scaling * synthetic_feature_weight``.

.. note::
The synthetic feature weight is subject to L1 or L2
regularization as all other features.
To lessen the effect of regularization on synthetic feature weight
(and therefore on the intercept) `intercept_scaling` has to be increased.
1
class_weight class_weight: dict or 'balanced', default=None

Weights associated with classes in the form ``{class_label: weight}``.
If not given, all classes are supposed to have weight one.

The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``.

Note that these weights will be multiplied with sample_weight (passed
through the fit method) if sample_weight is specified.

.. versionadded:: 0.17
*class_weight='balanced'*
None
random_state random_state: int, RandomState instance, default=None

Used when ``solver`` == 'sag', 'saga' or 'liblinear' to shuffle the
data. See :term:`Glossary ` for details.
0
solver solver: {'lbfgs', 'liblinear', 'newton-cg', 'newton-cholesky', 'sag', 'saga'}, default='lbfgs'

Algorithm to use in the optimization problem. Default is 'lbfgs'.
To choose a solver, you might want to consider the following aspects:

- 'lbfgs' is a good default solver because it works reasonably well for a wide
class of problems.
- For :term:`multiclass` problems (`n_classes >= 3`), all solvers except
'liblinear' minimize the full multinomial loss, 'liblinear' will raise an
error.
- 'newton-cholesky' is a good choice for
`n_samples` >> `n_features * n_classes`, especially with one-hot encoded
categorical features with rare categories. Be aware that the memory usage
of this solver has a quadratic dependency on `n_features * n_classes`
because it explicitly computes the full Hessian matrix.
- For small datasets, 'liblinear' is a good choice, whereas 'sag'
and 'saga' are faster for large ones;
- 'liblinear' can only handle binary classification by default. To apply a
one-versus-rest scheme for the multiclass setting one can wrap it with the
:class:`~sklearn.multiclass.OneVsRestClassifier`.

.. warning::
The choice of the algorithm depends on the penalty chosen (`l1_ratio=0`
for L2-penalty, `l1_ratio=1` for L1-penalty and `0 < l1_ratio < 1` for
Elastic-Net) and on (multinomial) multiclass support:

================= ======================== ======================
solver l1_ratio multinomial multiclass
================= ======================== ======================
'lbfgs' l1_ratio=0 yes
'liblinear' l1_ratio=1 or l1_ratio=0 no
'newton-cg' l1_ratio=0 yes
'newton-cholesky' l1_ratio=0 yes
'sag' l1_ratio=0 yes
'saga' 0<=l1_ratio<=1 yes
================= ======================== ======================

.. note::
'sag' and 'saga' fast convergence is only guaranteed on features
with approximately the same scale. You can preprocess the data with
a scaler from :mod:`sklearn.preprocessing`.

.. seealso::
Refer to the :ref:`User Guide ` for more
information regarding :class:`LogisticRegression` and more specifically the
:ref:`Table `
summarizing solver/penalty supports.

.. versionadded:: 0.17
Stochastic Average Gradient (SAG) descent solver. Multinomial support in
version 0.18.
.. versionadded:: 0.19
SAGA solver.
.. versionchanged:: 0.22
The default solver changed from 'liblinear' to 'lbfgs' in 0.22.
.. versionadded:: 1.2
newton-cholesky solver. Multinomial support in version 1.6.
'lbfgs'
max_iter max_iter: int, default=100

Maximum number of iterations taken for the solvers to converge.
100
verbose verbose: int, default=0

For the liblinear and lbfgs solvers set verbose to any positive
number for verbosity.
0
warm_start warm_start: bool, default=False

When set to True, reuse the solution of the previous call to fit as
initialization, otherwise, just erase the previous solution.
Useless for liblinear solver. See :term:`the Glossary `.

.. versionadded:: 0.17
*warm_start* to support *lbfgs*, *newton-cg*, *sag*, *saga* solvers.
False
n_jobs n_jobs: int, default=None

Does not have any effect.

.. deprecated:: 1.8
`n_jobs` is deprecated in version 1.8 and will be removed in 1.10.
None

Para seguir con la validación cruzada, es importante saber que según 3.1. Cross-validation: Evaluating Estimator Performance, (s. f.), dicha validación se hace para obtener una estimación del desempeño del modelo después de aplicar la separación de los datos en entrenamiento y prueba.

Como parámetro de la validación, se toma k = 5 ya que además de ser un valor común, esto indica que el conjunto de datos se dividen en 5 subconjuntos - uno de ellos es para validar, mientras que el resto es para entrenar.

In [196]:
scores = cross_val_score(model1, X, y, cv = 5)
print(scores.mean())
0.775
In [197]:
OR = np.exp(model1.coef_[0])
coef_df = pd.DataFrame({
  "Variables": X.columns,
  "Coeficientes": model1.coef_[0],
  "Odds Ratio": OR
})

print(coef_df)
                       Variables  Coeficientes  Odds Ratio
0                           edad     -0.025575    0.974749
1                       sexo_num      0.536856    1.710620
2  institucion_unidad_medica_num     -1.831048    0.160246

Según Egbuchulem (2025), los significados del valor de la razón de probabilidades de Odd Ratios son los siguientes:

  • OR de 1: No habría asociación entre la exposición y el resultado en la clase 1.

  • OR > 1: Sugiere que las probabilidades de exposición/intervención están asociadas positivamente con el resultado adverso en la clase 1.

  • OR < 1: Sugiere que la probabilidad de exposición se asocia negativamente con los resultados adversos en la clase 1.

Entonces podemos concluir de estos resultados que:

  • sexo_num y descripcion_enfermedad_num generan algo de efecto para el trastorno mental.

  • edad genera casi nada de efecto para el trastorno mental ya que está casi en 1.

  • institucion_unidad_medica_num genera mucho efecto para el trastorno mental.

Checamos cómo pronostica este modelo construido:

In [198]:
# Validar si el modelo pronostica adecuadamente.
y_pred_test = model1.predict(X_test)
print(y_pred_test[0:5])
print(y_test.head())
[1 1 1 1 1]
46382    0
32275    0
7409     1
32248    1
46407    1
Name: descripcion_grupo_enfermedad_num, dtype: int64

Vamos a probar como cambian los pronósticos que hace el modelo mediante el ajuste del umbral de clasificación, con model1.predict ya trabajamos con este establecido en 0.5 por automático.

Probabilidad cortando en mayor o igual a 0.8:

In [199]:
y_proba = model1.predict_proba(X)[:, 1]
y_pred = (y_proba >= 0.8).astype(int)
print(y_pred[0:5])
[1 1 1 1 1]
In [200]:
accuracy_train = model1.score(X_train, y_train)
print('Accuracy train = {:.2f}'.format(accuracy_train))
accuracy_test = model1.score(X_test, y_test)
print('Accuracy test = {:.2f}'.format(accuracy_test))
print('Diferencia = {:.4f}%'.format(np.abs(accuracy_train-accuracy_test)*100))
Accuracy train = 0.79
Accuracy test = 0.82
Diferencia = 3.2986%

Hasta este punto, obtenemos que el modelo presenta algo de sobreajuste, es decir, que generalizar hasta cierta escala entre los positivos reales y negativos reales (1 y 0 respectivamente).

In [201]:
predRF = model1.predict(X_test)
repDT = classification_report(y_test, predRF)
print(repDT)

from imblearn.metrics import geometric_mean_score
print('G-mean =', geometric_mean_score(y_test, predRF))

disp = ConfusionMatrixDisplay.from_predictions(y_test, predRF, cmap = plt.cm.Blues)

# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
              precision    recall  f1-score   support

           0       0.76      0.37      0.50        35
           1       0.83      0.96      0.89       109

    accuracy                           0.82       144
   macro avg       0.80      0.67      0.69       144
weighted avg       0.81      0.82      0.80       144

G-mean = 0.5981623234019464
No description has been provided for this image

Matriz de confusión.

  • 13 + 105 = 118 aciertos.

  • Porcentaje de aciertos → 118 / 144 = 0.81 → 81%.

  • 4 + 22 = 26 errores.

144 datos en total.

Reporte de clasificación.

  • 35 datos de prueba de Factores influyendo en el estado de salud.

    • 37% de positivos encontrados.
    • 76% fueron clasificados correctamente.
  • 109 datos de prueba de Trastorno mental y del comportamiento.

    • 96% de positivos encontrados.
    • 83% fueron clasificados correctamente.

F1-Score: promedio ponderado de las clases.

  • Factores influyendo en el estado de salud: 50%.
  • Trastorno mental y del comportamiento: 89%.

Porcentaje de proporción a encontrar todas las clasificaciones de este estudio (G-mean): 59%.

Según Torres (2024), para evaluar el rendimiento del modelo de clasificación, se utiliza la curva ROC, codificada bajo los estándares de Roc_Curve (s. f) con Scikit-learn.

In [202]:
from IPython.display import Image
Image(filename='/Users/estefaniadelarosa/Downloads/roc-curve-v2.png')
Out[202]:
No description has been provided for this image

Imagen 1. ROC.

In [203]:
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import roc_curve, roc_auc_score

# Validación cruzada.
y_probs = cross_val_predict(model1, X, y, cv = 5, method = 'predict_proba')[:,1] # Agregando método como scores.

# Curva ROC.
fpr, tpr, thresholds = roc_curve(y, y_probs)
auc = roc_auc_score(y, y_probs)

# Graficar
plt.plot(fpr, tpr)
plt.plot([0,1], [0,1])
plt.xlabel("False Positive Rate.")
plt.ylabel("True Positive Rate.")
plt.title("ROC Curve.")
plt.show()

print("AUC:", auc)
No description has been provided for this image
AUC: 0.7687227229977491

Basándonos en la imagen de Draelos, podemos delimitar de forma visual que el clasificador no es tan bueno al ejemplo ilustrado anteriormente, su curva de clasificación es muy lejana a la que se considera perfecta, de igual forma, el área bajo la curva de 0.76 indica que este modelo se necesita mejorar ya que no es bueno en definitiva (aunque pudiera está peor).

Logistic-SMOTE.

Comenzamos la técnica de balanceo de SMOTE, donde dicha técnica permite que se imputen datos sintéticos para que las clases se encuentren equilibradas - en este caso, ya no estamos alterando los pesos, sino que brindando datos para rellenar.

In [204]:
from imblearn.over_sampling import SMOTE

# Definir la técnica.
smote = SMOTE(random_state = 0)
# Aplicamos.
X_smote, y_smote = smote.fit_resample(X, y)

# Comprobar si funicona.
print('Tamaño de X antes de SMOTE:', X.shape)
print('Tamaño de X después de SMOTE:', X_smote.shape)
print('Balance de clases con SMOTE:', y_smote.value_counts())
print('Nuestras clases están balanceadas.')
Tamaño de X antes de SMOTE: (720, 3)
Tamaño de X después de SMOTE: (1094, 3)
Balance de clases con SMOTE: descripcion_grupo_enfermedad_num
1    547
0    547
Name: count, dtype: int64
Nuestras clases están balanceadas.
In [205]:
# Dividir los tratos en train y test.
X_train, X_test, y_train, y_test = train_test_split(X_smote, y_smote, test_size = 0.2, random_state = 0, stratify = y_smote)
print(y_train.value_counts())
X_train.shape, X_test.shape, y_train.shape, y_test.shape
# No dividir de manera que se desbalanceen los datos -> cuidado con el random_state).
descripcion_grupo_enfermedad_num
1    438
0    437
Name: count, dtype: int64
Out[205]:
((875, 3), (219, 3), (875,), (219,))

Construimos el modelo:

In [206]:
# Definimos el modelo.
model2 = LogisticRegression(random_state = 0)
# Entrenamos el modelo.
model2.fit(X_train, y_train)
Out[206]:
LogisticRegression(random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
penalty penalty: {'l1', 'l2', 'elasticnet', None}, default='l2'

Specify the norm of the penalty:

- `None`: no penalty is added;
- `'l2'`: add a L2 penalty term and it is the default choice;
- `'l1'`: add a L1 penalty term;
- `'elasticnet'`: both L1 and L2 penalty terms are added.

.. warning::
Some penalties may not work with some solvers. See the parameter
`solver` below, to know the compatibility between the penalty and
solver.

.. versionadded:: 0.19
l1 penalty with SAGA solver (allowing 'multinomial' + L1)

.. deprecated:: 1.8
`penalty` was deprecated in version 1.8 and will be removed in 1.10.
Use `l1_ratio` instead. `l1_ratio=0` for `penalty='l2'`, `l1_ratio=1` for
`penalty='l1'` and `l1_ratio` set to any float between 0 and 1 for
`'penalty='elasticnet'`.
'deprecated'
C C: float, default=1.0

Inverse of regularization strength; must be a positive float.
Like in support vector machines, smaller values specify stronger
regularization. `C=np.inf` results in unpenalized logistic regression.
For a visual example on the effect of tuning the `C` parameter
with an L1 penalty, see:
:ref:`sphx_glr_auto_examples_linear_model_plot_logistic_path.py`.
1.0
l1_ratio l1_ratio: float, default=0.0

The Elastic-Net mixing parameter, with `0 <= l1_ratio <= 1`. Setting
`l1_ratio=1` gives a pure L1-penalty, setting `l1_ratio=0` a pure L2-penalty.
Any value between 0 and 1 gives an Elastic-Net penalty of the form
`l1_ratio * L1 + (1 - l1_ratio) * L2`.

.. warning::
Certain values of `l1_ratio`, i.e. some penalties, may not work with some
solvers. See the parameter `solver` below, to know the compatibility between
the penalty and solver.

.. versionchanged:: 1.8
Default value changed from None to 0.0.

.. deprecated:: 1.8
`None` is deprecated and will be removed in version 1.10. Always use
`l1_ratio` to specify the penalty type.
0.0
dual dual: bool, default=False

Dual (constrained) or primal (regularized, see also
:ref:`this equation `) formulation. Dual formulation
is only implemented for l2 penalty with liblinear solver. Prefer `dual=False`
when n_samples > n_features.
False
tol tol: float, default=1e-4

Tolerance for stopping criteria.
0.0001
fit_intercept fit_intercept: bool, default=True

Specifies if a constant (a.k.a. bias or intercept) should be
added to the decision function.
True
intercept_scaling intercept_scaling: float, default=1

Useful only when the solver `liblinear` is used
and `self.fit_intercept` is set to `True`. In this case, `x` becomes
`[x, self.intercept_scaling]`,
i.e. a "synthetic" feature with constant value equal to
`intercept_scaling` is appended to the instance vector.
The intercept becomes
``intercept_scaling * synthetic_feature_weight``.

.. note::
The synthetic feature weight is subject to L1 or L2
regularization as all other features.
To lessen the effect of regularization on synthetic feature weight
(and therefore on the intercept) `intercept_scaling` has to be increased.
1
class_weight class_weight: dict or 'balanced', default=None

Weights associated with classes in the form ``{class_label: weight}``.
If not given, all classes are supposed to have weight one.

The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``.

Note that these weights will be multiplied with sample_weight (passed
through the fit method) if sample_weight is specified.

.. versionadded:: 0.17
*class_weight='balanced'*
None
random_state random_state: int, RandomState instance, default=None

Used when ``solver`` == 'sag', 'saga' or 'liblinear' to shuffle the
data. See :term:`Glossary ` for details.
0
solver solver: {'lbfgs', 'liblinear', 'newton-cg', 'newton-cholesky', 'sag', 'saga'}, default='lbfgs'

Algorithm to use in the optimization problem. Default is 'lbfgs'.
To choose a solver, you might want to consider the following aspects:

- 'lbfgs' is a good default solver because it works reasonably well for a wide
class of problems.
- For :term:`multiclass` problems (`n_classes >= 3`), all solvers except
'liblinear' minimize the full multinomial loss, 'liblinear' will raise an
error.
- 'newton-cholesky' is a good choice for
`n_samples` >> `n_features * n_classes`, especially with one-hot encoded
categorical features with rare categories. Be aware that the memory usage
of this solver has a quadratic dependency on `n_features * n_classes`
because it explicitly computes the full Hessian matrix.
- For small datasets, 'liblinear' is a good choice, whereas 'sag'
and 'saga' are faster for large ones;
- 'liblinear' can only handle binary classification by default. To apply a
one-versus-rest scheme for the multiclass setting one can wrap it with the
:class:`~sklearn.multiclass.OneVsRestClassifier`.

.. warning::
The choice of the algorithm depends on the penalty chosen (`l1_ratio=0`
for L2-penalty, `l1_ratio=1` for L1-penalty and `0 < l1_ratio < 1` for
Elastic-Net) and on (multinomial) multiclass support:

================= ======================== ======================
solver l1_ratio multinomial multiclass
================= ======================== ======================
'lbfgs' l1_ratio=0 yes
'liblinear' l1_ratio=1 or l1_ratio=0 no
'newton-cg' l1_ratio=0 yes
'newton-cholesky' l1_ratio=0 yes
'sag' l1_ratio=0 yes
'saga' 0<=l1_ratio<=1 yes
================= ======================== ======================

.. note::
'sag' and 'saga' fast convergence is only guaranteed on features
with approximately the same scale. You can preprocess the data with
a scaler from :mod:`sklearn.preprocessing`.

.. seealso::
Refer to the :ref:`User Guide ` for more
information regarding :class:`LogisticRegression` and more specifically the
:ref:`Table `
summarizing solver/penalty supports.

.. versionadded:: 0.17
Stochastic Average Gradient (SAG) descent solver. Multinomial support in
version 0.18.
.. versionadded:: 0.19
SAGA solver.
.. versionchanged:: 0.22
The default solver changed from 'liblinear' to 'lbfgs' in 0.22.
.. versionadded:: 1.2
newton-cholesky solver. Multinomial support in version 1.6.
'lbfgs'
max_iter max_iter: int, default=100

Maximum number of iterations taken for the solvers to converge.
100
verbose verbose: int, default=0

For the liblinear and lbfgs solvers set verbose to any positive
number for verbosity.
0
warm_start warm_start: bool, default=False

When set to True, reuse the solution of the previous call to fit as
initialization, otherwise, just erase the previous solution.
Useless for liblinear solver. See :term:`the Glossary `.

.. versionadded:: 0.17
*warm_start* to support *lbfgs*, *newton-cg*, *sag*, *saga* solvers.
False
n_jobs n_jobs: int, default=None

Does not have any effect.

.. deprecated:: 1.8
`n_jobs` is deprecated in version 1.8 and will be removed in 1.10.
None

Para esta técnica también evaluaremos Cross-validation bajo los mismos parámetros.

In [207]:
scores = cross_val_score(model2, X_smote, y_smote, cv = 5)
print(scores.mean())
0.7194294332034686
In [208]:
OR = np.exp(model2.coef_[0])
coef_df = pd.DataFrame({
  "Variables": X.columns,
  "Coeficientes": model2.coef_[0],
  "Odds Ratio": OR
})

print(coef_df)
                       Variables  Coeficientes  Odds Ratio
0                           edad     -0.022775    0.977482
1                       sexo_num      1.005345    2.732851
2  institucion_unidad_medica_num     -1.639948    0.193990

Según Egbuchulem (2025), los significados del valor de la razón de probabilidades de Odd Ratios son los siguientes:

  • OR de 1: No habría asociación entre la exposición y el resultado en la clase 1.

  • OR > 1: Sugiere que las probabilidades de exposición/intervención están asociadas positivamente con el resultado adverso en la clase 1.

  • OR < 1: Sugiere que la probabilidad de exposición se asocia negativamente con los resultados adversos en la clase 1.

Entonces podemos concluir de estos resultados que:

  • sexo_num genera mucho más impacto que el modelo desbalanceado, es la variable más fuerte.

  • descripcion_enfermedad_num genera algo de efecto para el trastorno mental.

  • edad genera casi nada de efecto para el trastorno mental ya que está casi en 1.

  • institucion_unidad_medica_num genera mucho efecto para el trastorno mental.

Checamos cómo pronostica este modelo construido:

In [209]:
# Validar si el modelo pronostica adecuadamente.
y_pred_test = model2.predict(X_test)
print(y_pred_test[0:5])
print(y_test.head())
[0 1 0 1 0]
944     0
199     1
1034    0
405     0
727     0
Name: descripcion_grupo_enfermedad_num, dtype: int64

Veremos cómo pronostica con un umbral de clasificación en mayor o igual a 0.8:

In [210]:
y_proba = model2.predict_proba(X)[:, 1]
y_pred = (y_proba >= 0.8).astype(int)
print(y_pred[0:5])
[1 0 0 0 0]
In [211]:
accuracy_train = model2.score(X_train, y_train)
print('Accuracy train = {:.2f}'.format(accuracy_train))
accuracy_test = model2.score(X_test, y_test)
print('Accuracy test = {:.2f}'.format(accuracy_test))
print('Diferencia = {:.4f}%'.format(np.abs(accuracy_train-accuracy_test)*100))
Accuracy train = 0.72
Accuracy test = 0.74
Diferencia = 2.6578%

Este modelo con la técnica de balanceo SMOTE con imputación de datos sintéticos, nos muestra que tiene un leve sobreajuste de 2.6%, sin embargo, esto no indica el no saber generalizar entre los positivos reales y negativos reales (1 y 0 respectivamente), además de que es un valor mejor a aquel del modelo desbalanceado.

In [212]:
predRF = model2.predict(X_test)
repDT = classification_report(y_test, predRF)
print(repDT)

from imblearn.metrics import geometric_mean_score
print('G-mean =', geometric_mean_score(y_test, predRF))

disp = ConfusionMatrixDisplay.from_predictions(y_test, predRF, cmap = plt.cm.Blues)

# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
              precision    recall  f1-score   support

           0       0.72      0.80      0.76       110
           1       0.77      0.69      0.73       109

    accuracy                           0.74       219
   macro avg       0.75      0.74      0.74       219
weighted avg       0.75      0.74      0.74       219

G-mean = 0.7419290502442469
No description has been provided for this image

Matriz de confusión.

  • 88 + 75 = 163 aciertos.

  • Porcentaje de aciertos → 163 / 219 = 0.74 → 74%.

  • 34 + 22 = 56 errores.

219 datos en total.

Reporte de clasificación.

  • 110 datos de prueba de Factores influyendo en el estado de salud.

    • 80% de positivos encontrados.
    • 72% fueron clasificados correctamente.
  • 109 datos de prueba de Trastorno mental y del comportamiento.

    • 69% de positivos encontrados.
    • 77% fueron clasificados correctamente.

F1-Score: promedio ponderado de las clases.

  • Factores influyendo en el estado de salud: 76%.
  • Trastorno mental y del comportamiento: 73%.

Porcentaje de proporción a encontrar todas las clasificaciones de este estudio (G-mean): 74%.

In [213]:
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Validación cruzada.
y_probs = cross_val_predict(model2, X, y, cv = 5, method = 'predict_proba')[:,1] # Agregando método como scores.

# Curva ROC.
fpr, tpr, thresholds = roc_curve(y, y_probs)
auc = roc_auc_score(y, y_probs)

# Graficar.
plt.plot(fpr, tpr)
plt.plot([0,1], [0,1])
plt.xlabel("False Positive Rate.")
plt.ylabel("True Positive Rate.")
plt.title("ROC Curve.")
plt.show()

print("AUC:", auc)
No description has been provided for this image
AUC: 0.7687227229977491

La curva ROC de la técnica SMOTE arroja que el clasificador del mismo (tomando también en consideración el área bajo la curva de 0.90) que este está muy parecido a la curva del es el mejor de todos hasta el momento con una diferencia pequeña a comparación del balanceo manual, pero existente.

Métricas.

Las métricas de desempeño evaluadas para estos modelos fueron:

  • Accuracy.

  • Matriz de confusión.

  • G-mean.

  • Precision.

  • Recall (Sensibilidad).

  • F1-Score.

  • Curva ROC-AUC.

Linear Discriminant Analysis.¶

Para el desarrollo de este modelo, se tomó como guía aquella de Kavlakoglu (s. f.), llamada: "How to implement linear discriminant analysis in Python." publicada en el sitio de IBM.

In [214]:
# Definimos entrada y salida.
X = df2[['edad', 'sexo_num', 'institucion_unidad_medica_num']]
y = df2['descripcion_grupo_enfermedad_num']
print(X.shape)
print(y.shape)
(720, 3)
(720,)

LDA by Default.

Separamos los datos de entrenamiento y prueba:

In [215]:
# Dividir los datos en train y test.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0, stratify = y)
X_train.shape, X_test.shape, y_train.shape, y_test.shape
# No dividir de manera que se desbalanceen los datos -> cuidado con el random_state).
Out[215]:
((576, 3), (144, 3), (576,), (144,))

Visualizamos con Pair Plot:

In [216]:
# Create a pair plot to visualize relationships between different features and species.
ax = sns.pairplot(df2, hue = 'descripcion_grupo_enfermedad_num', markers = ["o", "s", "D"])
plt.suptitle("Pair Plot: Grupos de Enfermedades.")
sns.move_legend(
    ax, "lower center",
    bbox_to_anchor = (.5, 1), ncol = 3, title = None, frameon = False)
plt.tight_layout()
plt.show()
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
/opt/anaconda3/lib/python3.13/site-packages/seaborn/axisgrid.py:1615: UserWarning: The markers list has more values (3) than needed (2), which may not be intended.
  func(x=x, y=y, **kwargs)
No description has been provided for this image

Visualizamos con Histogramas:

In [217]:
# Visualize the distribution of each feature using histograms.
plt.figure(figsize =( 12, 6))
for i, feature in enumerate(X[:-1]):
    plt.subplot(2, 2, i + 1)
    sns.histplot(data = df2, x = feature, hue = 'descripcion_grupo_enfermedad_num', kde = True)
    plt.title(f'{feature} Distribution.')

plt.tight_layout()
plt.show()
No description has been provided for this image

En este caso, se pueden observar diversas cosas:

  • En el rango de edad de los 20 a los tardíos 30, la mayoría se encuentran en el grupo 1, es decir, dentro de padecer trastornos mentales y del comportamiento.

    • Dentro de aquí, existe un hecho atípico ya que en los 40 y 50, la mayoría se encuentra en el grupo 0, es decir, dentro de estar viviendo con factores que influyen en su salud mental, no de forma tan grave como los trastornos.

    • Pero en edades más avanzadas, vuelve a repuntar el grupo 1 de los trastornos, esto sería interesante de estudiar y cosas así que se descubran de forma visual mediante histogramas u otros gráficos.

In [218]:
correlation_matrix = df2.corr(numeric_only = True)
plt.figure(figsize=(8, 6))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', linewidths=0.5)
plt.title("Correlation Heatmap")
plt.show()
No description has been provided for this image

Más adelante, al momento de implementar LDA, se tiene que establecer un número de componentes, en el caso de este dataset, solo se tiene que incorporar 1 componente ya que solo vamos a estar tratando dos clases.

In [219]:
# Verificación de n_components = 1 siendo 2 clases.
print(np.unique(y_train))
print(len(np.unique(y_train)))
[0 1]
2

Implementamos el modelo de LDA gracias a la librería importada anteriormente de sklearn.discriminant_analysis que contiene el LinearDiscriminantAnalysis:

In [220]:
# Apply Linear Discriminant Analysis.
lda = LinearDiscriminantAnalysis(n_components = 1)
X_train_lda = lda.fit_transform(X_train, y_train)
X_test_lda = lda.transform(X_test)

Importante aquí notar el nombre de las variables, estamos trabajando con el modelo desbalanceado.

In [221]:
tmp_Df = pd.DataFrame(X_train_lda, columns = ['LDA Component 1'])
tmp_Df['Class'] = y_train.values  

# Grafica.
sns.histplot(data = tmp_Df, x = 'LDA Component 1', hue = 'Class', kde = True)
plt.show()
No description has been provided for this image

Este histograma por sí mismo nos muestra la distribución de cada clase a lo largo del LDA, podemos observar cómo se delimitaron las regiones en el respectivo lado izquierdo y lado derecho.

  • Lado izquierdo: clase 0 - Factores influyendo en el estado de salud.

  • Lado derecho: clase 1 - Trastorno mental y del comportamiento.

Ahora, ¿por qué visualizamos con un histograma dicha distribución lineal?

  • Esto es debido a que estamos trabajando en 1D, es decir, que estamos bajo una dimensión, por lo que no podemos generar gráficos que se extiendan de las barras.

En el modelo desbalanceado, se puede observar que la separación entre clases existe pero no es perfecta — las distribuciones presentan un traslape notable, reflejo directo del desbalance entre las clases 0 y 1.

In [222]:
print(tmp_Df.head())
   LDA Component 1  Class
0        -0.695258      0
1        -1.551494      0
2         0.251022      1
3        -1.727778      1
4         0.580071      1

LDA SMOTE.

Separamos los datos de entrenamiento y prueba:

In [223]:
# Dividir los datos en train y test.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0, stratify = y)
X_train.shape, X_test.shape, y_train.shape, y_test.shape
# No dividir de manera que se desbalanceen los datos -> cuidado con el random_state).
Out[223]:
((576, 3), (144, 3), (576,), (144,))

Aplicamos el SMOTE:

In [224]:
from imblearn.over_sampling import SMOTE
smote = SMOTE(random_state = 0)
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)

Implemetamos el modelo de LDA:

In [225]:
# Apply Linear Discriminant Analysis.
lda = LinearDiscriminantAnalysis(n_components = 1)
X_train_lda = lda.fit_transform(X_train_smote, y_train_smote)
X_test_lda = lda.transform(X_test)

Ahora, aquí estamos trabajando con el modelo balanceado bajo SMOTE, notando esto en las variables.

In [226]:
tmp_Df = pd.DataFrame(X_train_lda, columns = ['LDA Component 1'])
tmp_Df['Class'] = y_train_smote.values  

# Grafica.
sns.histplot(data = tmp_Df, x = 'LDA Component 1', hue = 'Class', kde = True)
plt.show()
No description has been provided for this image
In [227]:
print(tmp_Df.head())
   LDA Component 1  Class
0        -0.571684      0
1        -1.299334      0
2         0.177260      1
3        -1.449144      1
4         1.296899      1

Este histograma nos muestra la distribución de cada clase a lo largo del LDA bajo la técnica de balanceo SMOTE.

  • Lado izquierdo: clase 0 - Factores influyendo en el estado de salud.

  • Lado derecho: clase 1 - Trastorno mental y del comportamiento.

Nuevamente, ¿por qué lo visualizamos con un histograma dicha distribución lineal?

  • Esto es debido a que estamos trabajando en 1D, es decir, que estamos bajo una dimensión, por lo que no podemos generar gráficos que se extiendan de las barras.

A diferencia del modelo desbalanceado, con SMOTE se puede observar que la separación entre clases mejora visiblemente: las distribuciones están más definidas hacia sus respectivos lados, reduciendo el traslape entre ambas clases. Esto es el resultado directo de haber equilibrado las clases con datos sintéticos.

In [228]:
print(tmp_Df.head())
   LDA Component 1  Class
0        -0.571684      0
1        -1.299334      0
2         0.177260      1
3        -1.449144      1
4         1.296899      1

Métricas.

Para el LDA, al ser un modelo de reducción de dimensionalidad que proyecta los datos en 1D para separar las clases, no se obtienen métricas de clasificación directas como Accuracy o F1-score propias del modelo. Lo que el LDA nos brinda es la proyección lineal que alimenta al Árbol de Decisión para que este pueda clasificar con datos más ordenados y balanceados.

  • LDA Desbalanceado: la proyección muestra traslape entre clases, lo que anticipa un menor desempeño del clasificador sobre estos datos.

  • LDA SMOTE: la proyección muestra clases más separadas y equilibradas, favoreciendo un mejor desempeño del árbol de decisión posterior.

Árbol de Decisión.¶

Árbol de Decisión by Default.

Generamos el árbol gracias a la librería de sklearn:

In [229]:
tree = DTC().fit(X_train, y_train)

Visualizamos el árbol:

In [230]:
from sklearn.tree import plot_tree
plt.figure(figsize=(15,10))
plot_tree(tree, filled = True, feature_names = X_train.columns)
Out[230]:
[Text(0.4519230769230769, 0.9615384615384616, 'institucion_unidad_medica_num <= 0.5\ngini = 0.364\nsamples = 576\nvalue = [138, 438]'),
 Text(0.2097902097902098, 0.8846153846153846, 'sexo_num <= 0.5\ngini = 0.144\nsamples = 308\nvalue = [24, 284]'),
 Text(0.33085664335664333, 0.9230769230769231, 'True  '),
 Text(0.1258741258741259, 0.8076923076923077, 'edad <= 42.5\ngini = 0.206\nsamples = 137\nvalue = [16, 121]'),
 Text(0.08391608391608392, 0.7307692307692307, 'edad <= 41.5\ngini = 0.287\nsamples = 69\nvalue = [12, 57]'),
 Text(0.06993006993006994, 0.6538461538461539, 'edad <= 27.5\ngini = 0.216\nsamples = 65\nvalue = [8, 57]'),
 Text(0.04195804195804196, 0.5769230769230769, 'edad <= 23.5\ngini = 0.053\nsamples = 37\nvalue = [1, 36]'),
 Text(0.027972027972027972, 0.5, 'edad <= 21.5\ngini = 0.105\nsamples = 18\nvalue = [1, 17]'),
 Text(0.013986013986013986, 0.4230769230769231, 'gini = 0.0\nsamples = 17\nvalue = [0, 17]'),
 Text(0.04195804195804196, 0.4230769230769231, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.055944055944055944, 0.5, 'gini = 0.0\nsamples = 19\nvalue = [0, 19]'),
 Text(0.0979020979020979, 0.5769230769230769, 'edad <= 29.0\ngini = 0.375\nsamples = 28\nvalue = [7, 21]'),
 Text(0.08391608391608392, 0.5, 'gini = 0.49\nsamples = 7\nvalue = [4, 3]'),
 Text(0.11188811188811189, 0.5, 'edad <= 34.0\ngini = 0.245\nsamples = 21\nvalue = [3, 18]'),
 Text(0.0979020979020979, 0.4230769230769231, 'gini = 0.0\nsamples = 8\nvalue = [0, 8]'),
 Text(0.1258741258741259, 0.4230769230769231, 'edad <= 36.0\ngini = 0.355\nsamples = 13\nvalue = [3, 10]'),
 Text(0.11188811188811189, 0.34615384615384615, 'gini = 0.444\nsamples = 9\nvalue = [3, 6]'),
 Text(0.13986013986013987, 0.34615384615384615, 'gini = 0.0\nsamples = 4\nvalue = [0, 4]'),
 Text(0.0979020979020979, 0.6538461538461539, 'gini = 0.0\nsamples = 4\nvalue = [4, 0]'),
 Text(0.16783216783216784, 0.7307692307692307, 'edad <= 53.5\ngini = 0.111\nsamples = 68\nvalue = [4, 64]'),
 Text(0.13986013986013987, 0.6538461538461539, 'edad <= 49.0\ngini = 0.042\nsamples = 47\nvalue = [1, 46]'),
 Text(0.1258741258741259, 0.5769230769230769, 'gini = 0.0\nsamples = 24\nvalue = [0, 24]'),
 Text(0.15384615384615385, 0.5769230769230769, 'edad <= 50.5\ngini = 0.083\nsamples = 23\nvalue = [1, 22]'),
 Text(0.13986013986013987, 0.5, 'gini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.16783216783216784, 0.5, 'gini = 0.0\nsamples = 15\nvalue = [0, 15]'),
 Text(0.1958041958041958, 0.6538461538461539, 'edad <= 54.5\ngini = 0.245\nsamples = 21\nvalue = [3, 18]'),
 Text(0.18181818181818182, 0.5769230769230769, 'gini = 0.5\nsamples = 4\nvalue = [2, 2]'),
 Text(0.2097902097902098, 0.5769230769230769, 'edad <= 58.5\ngini = 0.111\nsamples = 17\nvalue = [1, 16]'),
 Text(0.1958041958041958, 0.5, 'gini = 0.0\nsamples = 13\nvalue = [0, 13]'),
 Text(0.22377622377622378, 0.5, 'edad <= 60.5\ngini = 0.375\nsamples = 4\nvalue = [1, 3]'),
 Text(0.2097902097902098, 0.4230769230769231, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.23776223776223776, 0.4230769230769231, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.2937062937062937, 0.8076923076923077, 'edad <= 46.0\ngini = 0.089\nsamples = 171\nvalue = [8, 163]'),
 Text(0.26573426573426573, 0.7307692307692307, 'edad <= 32.5\ngini = 0.058\nsamples = 133\nvalue = [4, 129]'),
 Text(0.2517482517482518, 0.6538461538461539, 'edad <= 21.5\ngini = 0.081\nsamples = 95\nvalue = [4, 91]'),
 Text(0.23776223776223776, 0.5769230769230769, 'gini = 0.0\nsamples = 26\nvalue = [0, 26]'),
 Text(0.26573426573426573, 0.5769230769230769, 'edad <= 23.0\ngini = 0.109\nsamples = 69\nvalue = [4, 65]'),
 Text(0.2517482517482518, 0.5, 'gini = 0.5\nsamples = 2\nvalue = [1, 1]'),
 Text(0.27972027972027974, 0.5, 'edad <= 31.5\ngini = 0.086\nsamples = 67\nvalue = [3, 64]'),
 Text(0.26573426573426573, 0.4230769230769231, 'edad <= 25.5\ngini = 0.065\nsamples = 59\nvalue = [2, 57]'),
 Text(0.23776223776223776, 0.34615384615384615, 'edad <= 24.5\ngini = 0.198\nsamples = 9\nvalue = [1, 8]'),
 Text(0.22377622377622378, 0.2692307692307692, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.2517482517482518, 0.2692307692307692, 'gini = 0.278\nsamples = 6\nvalue = [1, 5]'),
 Text(0.2937062937062937, 0.34615384615384615, 'edad <= 28.5\ngini = 0.039\nsamples = 50\nvalue = [1, 49]'),
 Text(0.27972027972027974, 0.2692307692307692, 'edad <= 27.5\ngini = 0.071\nsamples = 27\nvalue = [1, 26]'),
 Text(0.26573426573426573, 0.19230769230769232, 'gini = 0.0\nsamples = 7\nvalue = [0, 7]'),
 Text(0.2937062937062937, 0.19230769230769232, 'gini = 0.095\nsamples = 20\nvalue = [1, 19]'),
 Text(0.3076923076923077, 0.2692307692307692, 'gini = 0.0\nsamples = 23\nvalue = [0, 23]'),
 Text(0.2937062937062937, 0.4230769230769231, 'gini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.27972027972027974, 0.6538461538461539, 'gini = 0.0\nsamples = 38\nvalue = [0, 38]'),
 Text(0.32167832167832167, 0.7307692307692307, 'edad <= 48.0\ngini = 0.188\nsamples = 38\nvalue = [4, 34]'),
 Text(0.3076923076923077, 0.6538461538461539, 'gini = 0.494\nsamples = 9\nvalue = [4, 5]'),
 Text(0.3356643356643357, 0.6538461538461539, 'gini = 0.0\nsamples = 29\nvalue = [0, 29]'),
 Text(0.6940559440559441, 0.8846153846153846, 'edad <= 40.5\ngini = 0.489\nsamples = 268\nvalue = [114, 154]'),
 Text(0.5729895104895105, 0.9230769230769231, '  False'),
 Text(0.4991258741258741, 0.8076923076923077, 'sexo_num <= 0.5\ngini = 0.426\nsamples = 143\nvalue = [44, 99]'),
 Text(0.42482517482517484, 0.7307692307692307, 'edad <= 21.5\ngini = 0.461\nsamples = 114\nvalue = [41, 73]'),
 Text(0.36363636363636365, 0.6538461538461539, 'edad <= 19.5\ngini = 0.298\nsamples = 22\nvalue = [4, 18]'),
 Text(0.3356643356643357, 0.5769230769230769, 'edad <= 18.5\ngini = 0.444\nsamples = 3\nvalue = [2, 1]'),
 Text(0.32167832167832167, 0.5, 'gini = 0.0\nsamples = 1\nvalue = [0, 1]'),
 Text(0.34965034965034963, 0.5, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.3916083916083916, 0.5769230769230769, 'edad <= 20.5\ngini = 0.188\nsamples = 19\nvalue = [2, 17]'),
 Text(0.3776223776223776, 0.5, 'gini = 0.245\nsamples = 7\nvalue = [1, 6]'),
 Text(0.40559440559440557, 0.5, 'gini = 0.153\nsamples = 12\nvalue = [1, 11]'),
 Text(0.486013986013986, 0.6538461538461539, 'edad <= 23.5\ngini = 0.481\nsamples = 92\nvalue = [37, 55]'),
 Text(0.44755244755244755, 0.5769230769230769, 'edad <= 22.5\ngini = 0.48\nsamples = 15\nvalue = [9, 6]'),
 Text(0.43356643356643354, 0.5, 'gini = 0.48\nsamples = 10\nvalue = [6, 4]'),
 Text(0.46153846153846156, 0.5, 'gini = 0.48\nsamples = 5\nvalue = [3, 2]'),
 Text(0.5244755244755245, 0.5769230769230769, 'edad <= 25.5\ngini = 0.463\nsamples = 77\nvalue = [28, 49]'),
 Text(0.48951048951048953, 0.5, 'edad <= 24.5\ngini = 0.32\nsamples = 20\nvalue = [4, 16]'),
 Text(0.4755244755244755, 0.4230769230769231, 'gini = 0.42\nsamples = 10\nvalue = [3, 7]'),
 Text(0.5034965034965035, 0.4230769230769231, 'gini = 0.18\nsamples = 10\nvalue = [1, 9]'),
 Text(0.5594405594405595, 0.5, 'edad <= 29.5\ngini = 0.488\nsamples = 57\nvalue = [24, 33]'),
 Text(0.5314685314685315, 0.4230769230769231, 'edad <= 28.5\ngini = 0.219\nsamples = 8\nvalue = [7, 1]'),
 Text(0.5174825174825175, 0.34615384615384615, 'gini = 0.0\nsamples = 5\nvalue = [5, 0]'),
 Text(0.5454545454545454, 0.34615384615384615, 'gini = 0.444\nsamples = 3\nvalue = [2, 1]'),
 Text(0.5874125874125874, 0.4230769230769231, 'edad <= 31.5\ngini = 0.453\nsamples = 49\nvalue = [17.0, 32.0]'),
 Text(0.5734265734265734, 0.34615384615384615, 'gini = 0.0\nsamples = 2\nvalue = [0, 2]'),
 Text(0.6013986013986014, 0.34615384615384615, 'edad <= 36.5\ngini = 0.462\nsamples = 47\nvalue = [17, 30]'),
 Text(0.5664335664335665, 0.2692307692307692, 'edad <= 34.5\ngini = 0.478\nsamples = 33\nvalue = [13, 20]'),
 Text(0.5384615384615384, 0.19230769230769232, 'edad <= 33.5\ngini = 0.444\nsamples = 18\nvalue = [6, 12]'),
 Text(0.5244755244755245, 0.11538461538461539, 'edad <= 32.5\ngini = 0.457\nsamples = 17\nvalue = [6, 11]'),
 Text(0.5104895104895105, 0.038461538461538464, 'gini = 0.5\nsamples = 2\nvalue = [1, 1]'),
 Text(0.5384615384615384, 0.038461538461538464, 'gini = 0.444\nsamples = 15\nvalue = [5, 10]'),
 Text(0.5524475524475524, 0.11538461538461539, 'gini = 0.0\nsamples = 1\nvalue = [0, 1]'),
 Text(0.5944055944055944, 0.19230769230769232, 'edad <= 35.5\ngini = 0.498\nsamples = 15\nvalue = [7, 8]'),
 Text(0.5804195804195804, 0.11538461538461539, 'gini = 0.444\nsamples = 3\nvalue = [2, 1]'),
 Text(0.6083916083916084, 0.11538461538461539, 'gini = 0.486\nsamples = 12\nvalue = [5, 7]'),
 Text(0.6363636363636364, 0.2692307692307692, 'edad <= 38.5\ngini = 0.408\nsamples = 14\nvalue = [4, 10]'),
 Text(0.6223776223776224, 0.19230769230769232, 'gini = 0.346\nsamples = 9\nvalue = [2, 7]'),
 Text(0.6503496503496503, 0.19230769230769232, 'gini = 0.48\nsamples = 5\nvalue = [2, 3]'),
 Text(0.5734265734265734, 0.7307692307692307, 'edad <= 19.0\ngini = 0.185\nsamples = 29\nvalue = [3, 26]'),
 Text(0.5594405594405595, 0.6538461538461539, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.5874125874125874, 0.6538461538461539, 'edad <= 32.0\ngini = 0.071\nsamples = 27\nvalue = [1, 26]'),
 Text(0.5734265734265734, 0.5769230769230769, 'gini = 0.0\nsamples = 17\nvalue = [0, 17]'),
 Text(0.6013986013986014, 0.5769230769230769, 'edad <= 34.0\ngini = 0.18\nsamples = 10\nvalue = [1, 9]'),
 Text(0.5874125874125874, 0.5, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.6153846153846154, 0.5, 'gini = 0.0\nsamples = 9\nvalue = [0, 9]'),
 Text(0.888986013986014, 0.8076923076923077, 'edad <= 56.5\ngini = 0.493\nsamples = 125\nvalue = [70, 55]'),
 Text(0.833916083916084, 0.7307692307692307, 'edad <= 52.5\ngini = 0.462\nsamples = 91\nvalue = [58.0, 33.0]'),
 Text(0.7937062937062938, 0.6538461538461539, 'sexo_num <= 0.5\ngini = 0.5\nsamples = 47\nvalue = [23.0, 24.0]'),
 Text(0.7797202797202797, 0.5769230769230769, 'edad <= 48.5\ngini = 0.465\nsamples = 38\nvalue = [14, 24]'),
 Text(0.7132867132867133, 0.5, 'edad <= 43.5\ngini = 0.494\nsamples = 27\nvalue = [12, 15]'),
 Text(0.6643356643356644, 0.4230769230769231, 'edad <= 41.5\ngini = 0.444\nsamples = 9\nvalue = [3, 6]'),
 Text(0.6503496503496503, 0.34615384615384615, 'gini = 0.444\nsamples = 3\nvalue = [2, 1]'),
 Text(0.6783216783216783, 0.34615384615384615, 'edad <= 42.5\ngini = 0.278\nsamples = 6\nvalue = [1, 5]'),
 Text(0.6643356643356644, 0.2692307692307692, 'gini = 0.444\nsamples = 3\nvalue = [1, 2]'),
 Text(0.6923076923076923, 0.2692307692307692, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.7622377622377622, 0.4230769230769231, 'edad <= 45.5\ngini = 0.5\nsamples = 18\nvalue = [9, 9]'),
 Text(0.7342657342657343, 0.34615384615384615, 'edad <= 44.5\ngini = 0.444\nsamples = 6\nvalue = [4, 2]'),
 Text(0.7202797202797203, 0.2692307692307692, 'gini = 0.5\nsamples = 4\nvalue = [2, 2]'),
 Text(0.7482517482517482, 0.2692307692307692, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.7902097902097902, 0.34615384615384615, 'edad <= 47.0\ngini = 0.486\nsamples = 12\nvalue = [5, 7]'),
 Text(0.7762237762237763, 0.2692307692307692, 'gini = 0.32\nsamples = 5\nvalue = [1, 4]'),
 Text(0.8041958041958042, 0.2692307692307692, 'gini = 0.49\nsamples = 7\nvalue = [4, 3]'),
 Text(0.8461538461538461, 0.5, 'edad <= 51.0\ngini = 0.298\nsamples = 11\nvalue = [2, 9]'),
 Text(0.8321678321678322, 0.4230769230769231, 'edad <= 49.5\ngini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.8181818181818182, 0.34615384615384615, 'gini = 0.278\nsamples = 6\nvalue = [1, 5]'),
 Text(0.8461538461538461, 0.34615384615384615, 'gini = 0.0\nsamples = 2\nvalue = [0, 2]'),
 Text(0.8601398601398601, 0.4230769230769231, 'gini = 0.444\nsamples = 3\nvalue = [1, 2]'),
 Text(0.8076923076923077, 0.5769230769230769, 'gini = 0.0\nsamples = 9\nvalue = [9, 0]'),
 Text(0.8741258741258742, 0.6538461538461539, 'edad <= 53.5\ngini = 0.325\nsamples = 44\nvalue = [35, 9]'),
 Text(0.8601398601398601, 0.5769230769230769, 'gini = 0.0\nsamples = 6\nvalue = [6, 0]'),
 Text(0.8881118881118881, 0.5769230769230769, 'edad <= 54.5\ngini = 0.361\nsamples = 38\nvalue = [29, 9]'),
 Text(0.8741258741258742, 0.5, 'gini = 0.408\nsamples = 14\nvalue = [10, 4]'),
 Text(0.9020979020979021, 0.5, 'edad <= 55.5\ngini = 0.33\nsamples = 24\nvalue = [19, 5]'),
 Text(0.8881118881118881, 0.4230769230769231, 'gini = 0.198\nsamples = 9\nvalue = [8, 1]'),
 Text(0.916083916083916, 0.4230769230769231, 'gini = 0.391\nsamples = 15\nvalue = [11, 4]'),
 Text(0.9440559440559441, 0.7307692307692307, 'edad <= 58.0\ngini = 0.457\nsamples = 34\nvalue = [12, 22]'),
 Text(0.9300699300699301, 0.6538461538461539, 'gini = 0.278\nsamples = 18\nvalue = [3, 15]'),
 Text(0.958041958041958, 0.6538461538461539, 'edad <= 61.0\ngini = 0.492\nsamples = 16\nvalue = [9, 7]'),
 Text(0.9440559440559441, 0.5769230769230769, 'gini = 0.0\nsamples = 6\nvalue = [6, 0]'),
 Text(0.972027972027972, 0.5769230769230769, 'edad <= 64.5\ngini = 0.42\nsamples = 10\nvalue = [3, 7]'),
 Text(0.958041958041958, 0.5, 'edad <= 62.5\ngini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.9440559440559441, 0.4230769230769231, 'gini = 0.0\nsamples = 4\nvalue = [0, 4]'),
 Text(0.972027972027972, 0.4230769230769231, 'edad <= 63.5\ngini = 0.375\nsamples = 4\nvalue = [1, 3]'),
 Text(0.958041958041958, 0.34615384615384615, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.986013986013986, 0.34615384615384615, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.986013986013986, 0.5, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]')]
No description has been provided for this image

El árbol generado cuenta con los siguientes datos:

  • Su profundidad (longitud vertical), es de 12.

  • Cuenta con 69 hojas.

Esto nos ayuda a dimensionar que es un árbol complejo respectivo al tamaño del dataset.

In [231]:
print(tree.tree_.max_depth)
12
In [232]:
print(tree.get_n_leaves())
69

Métricas: aquí evaluaremos dos aspectos:

  • Accuracy: aciertos totales.

  • F1-score: promedio ponderado de las clases.

In [233]:
from sklearn.metrics import accuracy_score, f1_score
yhat0 = tree.predict(X_test)
acc0 = accuracy_score(y_test, yhat0)
f10 = f1_score(y_test, yhat0, average='weighted')
print("Accuracy inicial: ", acc0)
print("F1-score inicial: ", f10)
Accuracy inicial:  0.8402777777777778
F1-score inicial:  0.8377859946121579

El Accuracy del modelo es bueno, obtenemos un valor de 0.84, por lo que se puede concluir que bajo el árbol original se acierta la clasificación un 84%.

El F1-score del modelo también es bueno, obtenemos un valor de 0.83; este resultado puede presentar sesgo hacia la clase mayoritaria dada la ausencia de balanceo.

In [234]:
# Matriz de confusión - Árbol by Default.
from sklearn.metrics import confusion_matrix

conf_m = confusion_matrix(y_test, yhat0)
sns.heatmap(conf_m, annot = True, fmt = "d", cmap = "Blues", cbar = False, square = True)
plt.ylabel('y_true')
plt.xlabel('y_pred')
plt.title('Matriz de Confusión - Árbol Inicial.')
plt.show()
No description has been provided for this image

Matriz de confusión.

  • 25 + 92 = 117 aciertos.

  • Porcentaje de aciertos → 117/144 = 0.81 → 81%.

  • 17 + 10 = 27 errores.

144 datos en total.

Árbol de Decisión SMOTE.

Generamos el árbol:

In [235]:
tree_smote = DTC().fit(X_train_smote, y_train_smote)

Visualizamos el árbol:

In [236]:
from sklearn.tree import plot_tree
plt.figure(figsize=(15,10))
plot_tree(tree_smote, filled = True, feature_names = X_train_smote.columns)
Out[236]:
[Text(0.4680059523809524, 0.96875, 'institucion_unidad_medica_num <= 0.5\ngini = 0.5\nsamples = 876\nvalue = [438, 438]'),
 Text(0.2222222222222222, 0.90625, 'sexo_num <= 0.5\ngini = 0.38\nsamples = 381\nvalue = [97, 284]'),
 Text(0.3451140873015873, 0.9375, 'True  '),
 Text(0.09523809523809523, 0.84375, 'edad <= 21.5\ngini = 0.484\nsamples = 205\nvalue = [84, 121]'),
 Text(0.07936507936507936, 0.78125, 'gini = 0.0\nsamples = 17\nvalue = [0, 17]'),
 Text(0.1111111111111111, 0.78125, 'edad <= 49.5\ngini = 0.494\nsamples = 188\nvalue = [84, 104]'),
 Text(0.047619047619047616, 0.71875, 'edad <= 24.5\ngini = 0.498\nsamples = 136\nvalue = [72, 64]'),
 Text(0.031746031746031744, 0.65625, 'gini = 0.0\nsamples = 7\nvalue = [7, 0]'),
 Text(0.06349206349206349, 0.65625, 'edad <= 27.5\ngini = 0.5\nsamples = 129\nvalue = [65, 64]'),
 Text(0.031746031746031744, 0.59375, 'edad <= 25.5\ngini = 0.172\nsamples = 21\nvalue = [2, 19]'),
 Text(0.015873015873015872, 0.53125, 'gini = 0.408\nsamples = 7\nvalue = [2, 5]'),
 Text(0.047619047619047616, 0.53125, 'gini = 0.0\nsamples = 14\nvalue = [0, 14]'),
 Text(0.09523809523809523, 0.59375, 'edad <= 29.0\ngini = 0.486\nsamples = 108\nvalue = [63.0, 45.0]'),
 Text(0.07936507936507936, 0.53125, 'gini = 0.255\nsamples = 20\nvalue = [17, 3]'),
 Text(0.1111111111111111, 0.53125, 'edad <= 34.0\ngini = 0.499\nsamples = 88\nvalue = [46, 42]'),
 Text(0.07142857142857142, 0.46875, 'edad <= 32.5\ngini = 0.32\nsamples = 10\nvalue = [2, 8]'),
 Text(0.05555555555555555, 0.40625, 'edad <= 31.0\ngini = 0.5\nsamples = 4\nvalue = [2, 2]'),
 Text(0.03968253968253968, 0.34375, 'gini = 0.0\nsamples = 2\nvalue = [0, 2]'),
 Text(0.07142857142857142, 0.34375, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.0873015873015873, 0.40625, 'gini = 0.0\nsamples = 6\nvalue = [0, 6]'),
 Text(0.15079365079365079, 0.46875, 'edad <= 42.5\ngini = 0.492\nsamples = 78\nvalue = [44, 34]'),
 Text(0.11904761904761904, 0.40625, 'edad <= 41.5\ngini = 0.401\nsamples = 36\nvalue = [26, 10]'),
 Text(0.10317460317460317, 0.34375, 'edad <= 36.0\ngini = 0.473\nsamples = 26\nvalue = [16, 10]'),
 Text(0.0873015873015873, 0.28125, 'gini = 0.444\nsamples = 18\nvalue = [12, 6]'),
 Text(0.11904761904761904, 0.28125, 'edad <= 38.5\ngini = 0.5\nsamples = 8\nvalue = [4, 4]'),
 Text(0.10317460317460317, 0.21875, 'gini = 0.0\nsamples = 2\nvalue = [0, 2]'),
 Text(0.1349206349206349, 0.21875, 'edad <= 40.5\ngini = 0.444\nsamples = 6\nvalue = [4, 2]'),
 Text(0.11904761904761904, 0.15625, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.15079365079365079, 0.15625, 'gini = 0.5\nsamples = 4\nvalue = [2, 2]'),
 Text(0.1349206349206349, 0.34375, 'gini = 0.0\nsamples = 10\nvalue = [10, 0]'),
 Text(0.18253968253968253, 0.40625, 'edad <= 45.0\ngini = 0.49\nsamples = 42\nvalue = [18, 24]'),
 Text(0.16666666666666666, 0.34375, 'gini = 0.0\nsamples = 12\nvalue = [0, 12]'),
 Text(0.1984126984126984, 0.34375, 'edad <= 48.5\ngini = 0.48\nsamples = 30\nvalue = [18, 12]'),
 Text(0.18253968253968253, 0.28125, 'edad <= 46.5\ngini = 0.494\nsamples = 27\nvalue = [15, 12]'),
 Text(0.16666666666666666, 0.21875, 'gini = 0.375\nsamples = 4\nvalue = [3, 1]'),
 Text(0.1984126984126984, 0.21875, 'edad <= 47.5\ngini = 0.499\nsamples = 23\nvalue = [12, 11]'),
 Text(0.18253968253968253, 0.15625, 'gini = 0.498\nsamples = 17\nvalue = [8, 9]'),
 Text(0.21428571428571427, 0.15625, 'gini = 0.444\nsamples = 6\nvalue = [4, 2]'),
 Text(0.21428571428571427, 0.28125, 'gini = 0.0\nsamples = 3\nvalue = [3, 0]'),
 Text(0.1746031746031746, 0.71875, 'edad <= 53.5\ngini = 0.355\nsamples = 52\nvalue = [12, 40]'),
 Text(0.14285714285714285, 0.65625, 'edad <= 50.5\ngini = 0.083\nsamples = 23\nvalue = [1, 22]'),
 Text(0.12698412698412698, 0.59375, 'gini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.15873015873015872, 0.59375, 'gini = 0.0\nsamples = 15\nvalue = [0, 15]'),
 Text(0.20634920634920634, 0.65625, 'edad <= 54.5\ngini = 0.471\nsamples = 29\nvalue = [11, 18]'),
 Text(0.19047619047619047, 0.59375, 'gini = 0.298\nsamples = 11\nvalue = [9, 2]'),
 Text(0.2222222222222222, 0.59375, 'edad <= 58.5\ngini = 0.198\nsamples = 18\nvalue = [2, 16]'),
 Text(0.20634920634920634, 0.53125, 'gini = 0.0\nsamples = 13\nvalue = [0, 13]'),
 Text(0.23809523809523808, 0.53125, 'edad <= 60.5\ngini = 0.48\nsamples = 5\nvalue = [2, 3]'),
 Text(0.2222222222222222, 0.46875, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.25396825396825395, 0.46875, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.3492063492063492, 0.84375, 'edad <= 46.0\ngini = 0.137\nsamples = 176\nvalue = [13, 163]'),
 Text(0.31746031746031744, 0.78125, 'edad <= 32.5\ngini = 0.058\nsamples = 133\nvalue = [4, 129]'),
 Text(0.30158730158730157, 0.71875, 'edad <= 21.5\ngini = 0.081\nsamples = 95\nvalue = [4, 91]'),
 Text(0.2857142857142857, 0.65625, 'gini = 0.0\nsamples = 26\nvalue = [0, 26]'),
 Text(0.31746031746031744, 0.65625, 'edad <= 23.0\ngini = 0.109\nsamples = 69\nvalue = [4, 65]'),
 Text(0.30158730158730157, 0.59375, 'gini = 0.5\nsamples = 2\nvalue = [1, 1]'),
 Text(0.3333333333333333, 0.59375, 'edad <= 31.5\ngini = 0.086\nsamples = 67\nvalue = [3, 64]'),
 Text(0.31746031746031744, 0.53125, 'edad <= 25.5\ngini = 0.065\nsamples = 59\nvalue = [2, 57]'),
 Text(0.2857142857142857, 0.46875, 'edad <= 24.5\ngini = 0.198\nsamples = 9\nvalue = [1, 8]'),
 Text(0.2698412698412698, 0.40625, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.30158730158730157, 0.40625, 'gini = 0.278\nsamples = 6\nvalue = [1, 5]'),
 Text(0.3492063492063492, 0.46875, 'edad <= 28.5\ngini = 0.039\nsamples = 50\nvalue = [1, 49]'),
 Text(0.3333333333333333, 0.40625, 'edad <= 27.5\ngini = 0.071\nsamples = 27\nvalue = [1, 26]'),
 Text(0.31746031746031744, 0.34375, 'gini = 0.0\nsamples = 7\nvalue = [0, 7]'),
 Text(0.3492063492063492, 0.34375, 'gini = 0.095\nsamples = 20\nvalue = [1, 19]'),
 Text(0.36507936507936506, 0.40625, 'gini = 0.0\nsamples = 23\nvalue = [0, 23]'),
 Text(0.3492063492063492, 0.53125, 'gini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.3333333333333333, 0.71875, 'gini = 0.0\nsamples = 38\nvalue = [0, 38]'),
 Text(0.38095238095238093, 0.78125, 'edad <= 48.0\ngini = 0.331\nsamples = 43\nvalue = [9, 34]'),
 Text(0.36507936507936506, 0.71875, 'gini = 0.459\nsamples = 14\nvalue = [9, 5]'),
 Text(0.3968253968253968, 0.71875, 'gini = 0.0\nsamples = 29\nvalue = [0, 29]'),
 Text(0.7137896825396826, 0.90625, 'edad <= 42.5\ngini = 0.429\nsamples = 495\nvalue = [341, 154]'),
 Text(0.5908978174603174, 0.9375, '  False'),
 Text(0.5466269841269841, 0.84375, 'sexo_num <= 0.5\ngini = 0.493\nsamples = 232\nvalue = [130, 102]'),
 Text(0.4742063492063492, 0.78125, 'edad <= 19.5\ngini = 0.468\nsamples = 203\nvalue = [127, 76]'),
 Text(0.42857142857142855, 0.71875, 'edad <= 18.5\ngini = 0.124\nsamples = 15\nvalue = [14, 1]'),
 Text(0.4126984126984127, 0.65625, 'gini = 0.18\nsamples = 10\nvalue = [9, 1]'),
 Text(0.4444444444444444, 0.65625, 'gini = 0.0\nsamples = 5\nvalue = [5, 0]'),
 Text(0.5198412698412699, 0.71875, 'edad <= 21.5\ngini = 0.48\nsamples = 188\nvalue = [113, 75]'),
 Text(0.47619047619047616, 0.65625, 'edad <= 20.5\ngini = 0.188\nsamples = 19\nvalue = [2, 17]'),
 Text(0.4603174603174603, 0.59375, 'gini = 0.245\nsamples = 7\nvalue = [1, 6]'),
 Text(0.49206349206349204, 0.59375, 'gini = 0.153\nsamples = 12\nvalue = [1, 11]'),
 Text(0.5634920634920635, 0.65625, 'edad <= 23.5\ngini = 0.451\nsamples = 169\nvalue = [111.0, 58.0]'),
 Text(0.5238095238095238, 0.59375, 'edad <= 22.5\ngini = 0.26\nsamples = 39\nvalue = [33, 6]'),
 Text(0.5079365079365079, 0.53125, 'gini = 0.238\nsamples = 29\nvalue = [25, 4]'),
 Text(0.5396825396825397, 0.53125, 'gini = 0.32\nsamples = 10\nvalue = [8, 2]'),
 Text(0.6031746031746031, 0.59375, 'edad <= 25.5\ngini = 0.48\nsamples = 130\nvalue = [78, 52]'),
 Text(0.5714285714285714, 0.53125, 'edad <= 24.5\ngini = 0.473\nsamples = 26\nvalue = [10, 16]'),
 Text(0.5555555555555556, 0.46875, 'gini = 0.486\nsamples = 12\nvalue = [5, 7]'),
 Text(0.5873015873015873, 0.46875, 'gini = 0.459\nsamples = 14\nvalue = [5, 9]'),
 Text(0.6349206349206349, 0.53125, 'edad <= 28.5\ngini = 0.453\nsamples = 104\nvalue = [68, 36]'),
 Text(0.6190476190476191, 0.46875, 'gini = 0.0\nsamples = 12\nvalue = [12, 0]'),
 Text(0.6507936507936508, 0.46875, 'edad <= 41.5\ngini = 0.476\nsamples = 92\nvalue = [56, 36]'),
 Text(0.6349206349206349, 0.40625, 'edad <= 40.5\ngini = 0.472\nsamples = 89\nvalue = [55.0, 34.0]'),
 Text(0.6190476190476191, 0.34375, 'edad <= 36.5\ngini = 0.479\nsamples = 83\nvalue = [50.0, 33.0]'),
 Text(0.5793650793650794, 0.28125, 'edad <= 31.5\ngini = 0.46\nsamples = 64\nvalue = [41, 23]'),
 Text(0.5476190476190477, 0.21875, 'edad <= 29.5\ngini = 0.48\nsamples = 5\nvalue = [2, 3]'),
 Text(0.5317460317460317, 0.15625, 'gini = 0.444\nsamples = 3\nvalue = [2, 1]'),
 Text(0.5634920634920635, 0.15625, 'gini = 0.0\nsamples = 2\nvalue = [0, 2]'),
 Text(0.6111111111111112, 0.21875, 'edad <= 32.5\ngini = 0.448\nsamples = 59\nvalue = [39, 20]'),
 Text(0.5952380952380952, 0.15625, 'gini = 0.32\nsamples = 5\nvalue = [4, 1]'),
 Text(0.626984126984127, 0.15625, 'edad <= 34.5\ngini = 0.456\nsamples = 54\nvalue = [35, 19]'),
 Text(0.5952380952380952, 0.09375, 'edad <= 33.5\ngini = 0.471\nsamples = 29\nvalue = [18, 11]'),
 Text(0.5793650793650794, 0.03125, 'gini = 0.459\nsamples = 28\nvalue = [18, 10]'),
 Text(0.6111111111111112, 0.03125, 'gini = 0.0\nsamples = 1\nvalue = [0, 1]'),
 Text(0.6587301587301587, 0.09375, 'edad <= 35.5\ngini = 0.435\nsamples = 25\nvalue = [17, 8]'),
 Text(0.6428571428571429, 0.03125, 'gini = 0.278\nsamples = 6\nvalue = [5, 1]'),
 Text(0.6746031746031746, 0.03125, 'gini = 0.465\nsamples = 19\nvalue = [12, 7]'),
 Text(0.6587301587301587, 0.28125, 'edad <= 38.5\ngini = 0.499\nsamples = 19\nvalue = [9, 10]'),
 Text(0.6428571428571429, 0.21875, 'gini = 0.463\nsamples = 11\nvalue = [4, 7]'),
 Text(0.6746031746031746, 0.21875, 'gini = 0.469\nsamples = 8\nvalue = [5, 3]'),
 Text(0.6507936507936508, 0.34375, 'gini = 0.278\nsamples = 6\nvalue = [5, 1]'),
 Text(0.6666666666666666, 0.40625, 'gini = 0.444\nsamples = 3\nvalue = [1, 2]'),
 Text(0.6190476190476191, 0.78125, 'edad <= 19.0\ngini = 0.185\nsamples = 29\nvalue = [3, 26]'),
 Text(0.6031746031746031, 0.71875, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.6349206349206349, 0.71875, 'edad <= 32.0\ngini = 0.071\nsamples = 27\nvalue = [1, 26]'),
 Text(0.6190476190476191, 0.65625, 'gini = 0.0\nsamples = 17\nvalue = [0, 17]'),
 Text(0.6507936507936508, 0.65625, 'edad <= 34.0\ngini = 0.18\nsamples = 10\nvalue = [1, 9]'),
 Text(0.6349206349206349, 0.59375, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.6666666666666666, 0.59375, 'gini = 0.0\nsamples = 9\nvalue = [0, 9]'),
 Text(0.8809523809523809, 0.84375, 'edad <= 56.5\ngini = 0.317\nsamples = 263\nvalue = [211, 52]'),
 Text(0.8253968253968254, 0.78125, 'edad <= 52.5\ngini = 0.249\nsamples = 206\nvalue = [176.0, 30.0]'),
 Text(0.7936507936507936, 0.71875, 'sexo_num <= 0.5\ngini = 0.363\nsamples = 88\nvalue = [67, 21]'),
 Text(0.7777777777777778, 0.65625, 'edad <= 48.5\ngini = 0.458\nsamples = 59\nvalue = [38, 21]'),
 Text(0.746031746031746, 0.59375, 'edad <= 47.0\ngini = 0.397\nsamples = 44\nvalue = [32, 12]'),
 Text(0.7301587301587301, 0.53125, 'edad <= 45.5\ngini = 0.495\nsamples = 20\nvalue = [11, 9]'),
 Text(0.7142857142857143, 0.46875, 'edad <= 43.5\ngini = 0.444\nsamples = 15\nvalue = [10, 5]'),
 Text(0.6984126984126984, 0.40625, 'gini = 0.375\nsamples = 4\nvalue = [1, 3]'),
 Text(0.7301587301587301, 0.40625, 'edad <= 44.5\ngini = 0.298\nsamples = 11\nvalue = [9, 2]'),
 Text(0.7142857142857143, 0.34375, 'gini = 0.444\nsamples = 6\nvalue = [4, 2]'),
 Text(0.746031746031746, 0.34375, 'gini = 0.0\nsamples = 5\nvalue = [5, 0]'),
 Text(0.746031746031746, 0.46875, 'gini = 0.32\nsamples = 5\nvalue = [1, 4]'),
 Text(0.7619047619047619, 0.53125, 'gini = 0.219\nsamples = 24\nvalue = [21, 3]'),
 Text(0.8095238095238095, 0.59375, 'edad <= 51.0\ngini = 0.48\nsamples = 15\nvalue = [6, 9]'),
 Text(0.7936507936507936, 0.53125, 'edad <= 49.5\ngini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.7777777777777778, 0.46875, 'gini = 0.278\nsamples = 6\nvalue = [1, 5]'),
 Text(0.8095238095238095, 0.46875, 'gini = 0.0\nsamples = 2\nvalue = [0, 2]'),
 Text(0.8253968253968254, 0.53125, 'gini = 0.408\nsamples = 7\nvalue = [5, 2]'),
 Text(0.8095238095238095, 0.65625, 'gini = 0.0\nsamples = 29\nvalue = [29, 0]'),
 Text(0.8571428571428571, 0.71875, 'edad <= 53.5\ngini = 0.141\nsamples = 118\nvalue = [109, 9]'),
 Text(0.8412698412698413, 0.65625, 'gini = 0.0\nsamples = 22\nvalue = [22, 0]'),
 Text(0.873015873015873, 0.65625, 'edad <= 54.5\ngini = 0.17\nsamples = 96\nvalue = [87, 9]'),
 Text(0.8571428571428571, 0.59375, 'gini = 0.208\nsamples = 34\nvalue = [30, 4]'),
 Text(0.8888888888888888, 0.59375, 'edad <= 55.5\ngini = 0.148\nsamples = 62\nvalue = [57, 5]'),
 Text(0.873015873015873, 0.53125, 'gini = 0.08\nsamples = 24\nvalue = [23, 1]'),
 Text(0.9047619047619048, 0.53125, 'gini = 0.188\nsamples = 38\nvalue = [34, 4]'),
 Text(0.9365079365079365, 0.78125, 'edad <= 58.0\ngini = 0.474\nsamples = 57\nvalue = [35, 22]'),
 Text(0.9206349206349206, 0.71875, 'gini = 0.332\nsamples = 19\nvalue = [4, 15]'),
 Text(0.9523809523809523, 0.71875, 'edad <= 61.5\ngini = 0.301\nsamples = 38\nvalue = [31, 7]'),
 Text(0.9365079365079365, 0.65625, 'gini = 0.0\nsamples = 23\nvalue = [23, 0]'),
 Text(0.9682539682539683, 0.65625, 'edad <= 64.5\ngini = 0.498\nsamples = 15\nvalue = [8, 7]'),
 Text(0.9523809523809523, 0.59375, 'edad <= 63.5\ngini = 0.497\nsamples = 13\nvalue = [6, 7]'),
 Text(0.9365079365079365, 0.53125, 'edad <= 62.5\ngini = 0.494\nsamples = 9\nvalue = [5, 4]'),
 Text(0.9206349206349206, 0.46875, 'gini = 0.49\nsamples = 7\nvalue = [3, 4]'),
 Text(0.9523809523809523, 0.46875, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.9682539682539683, 0.53125, 'gini = 0.375\nsamples = 4\nvalue = [1, 3]'),
 Text(0.9841269841269841, 0.59375, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]')]
No description has been provided for this image

El árbol generado cuenta con los siguientes datos:

  • Su profundidad (longitud vertical), es de 15.

  • Cuenta con 78 hojas.

Esto nos ayuda a dimensionar que es un árbol complejo respectivo al tamaño del dataset.

In [237]:
print(tree_smote.tree_.max_depth)
15
In [238]:
print(tree_smote.get_n_leaves())
78
In [239]:
from sklearn.metrics import accuracy_score, f1_score
yhat0 = tree_smote.predict(X_test)
acc0 = accuracy_score(y_test, yhat0)
f10 = f1_score(y_test, yhat0, average='weighted')
print("Accuracy inicial: ", acc0)
print("F1-score inicial: ", f10)
Accuracy inicial:  0.8125
F1-score inicial:  0.8179125376992675

El Accuracy del modelo es bueno, obtenemos un valor de 0.81, por lo que se puede concluir que bajo el árbol original se acierta la clasificación un 81 - 3 cifras menos que el árbol de decisión sin balancear.

El F1-score del modelo también es bueno, obtenemos el mismo valor de 0.81 (81%), siendo 2 cifras menos que el árbol de decisión sin balancear.

In [240]:
# Matriz de confusión - Árbol inicial.
from sklearn.metrics import confusion_matrix

conf_m = confusion_matrix(y_test, yhat0)
sns.heatmap(conf_m, annot = True, fmt = "d", cmap = "Blues", cbar = False, square = True)
plt.ylabel('y_true')
plt.xlabel('y_pred')
plt.title('Matriz de Confusión - Árbol Inicial.')
plt.show()
No description has been provided for this image

Matriz de confusión.

  • 25 + 92 = 117 aciertos.

  • Porcentaje de aciertos → 117/144 = 0.81 → 81%.

  • 17 + 10 = 27 errores.

144 datos en total.

Árbol de Decisión Podado by Default.

Vamos a comenzar con los árboles podados mediante la creación con StratifiedKFold un sistema de separación de datos con validación cruzada de 4 folds, seguido de un iterador que recorre todos los valores posibles de alpha hasta encontrar el adecuado.

  • Alpha es un coeficiente que ayuda para optimizar búsquedas.
In [241]:
from sklearn.model_selection import cross_val_score, StratifiedKFold
skf = StratifiedKFold(n_splits = 4)
ccp = np.linspace(0.001, 0.2, 250)
cv_scores = []
for alpha in ccp:
    pruned_tree = DTC(ccp_alpha = alpha, class_weight='balanced')
    cv_scores.append(np.mean(cross_val_score(pruned_tree, X_train, y_train, cv = skf, scoring = 'f1')))

Imprimimos el valor de alpha para ver cuánto nos dió, este será el que se trabajará para la poda.

In [242]:
alpha = ccp[np.argmax(cv_scores)]
print("Best alpha: ", alpha)
Best alpha:  0.001

Visualizamos el árbol podado:

In [243]:
pruned_tree = DTC(ccp_alpha = alpha).fit(X_train, y_train)
plot_tree(pruned_tree, filled = True, feature_names = X_train.columns)
Out[243]:
[Text(0.4642857142857143, 0.95, 'institucion_unidad_medica_num <= 0.5\ngini = 0.364\nsamples = 576\nvalue = [138, 438]'),
 Text(0.25, 0.85, 'sexo_num <= 0.5\ngini = 0.144\nsamples = 308\nvalue = [24, 284]'),
 Text(0.35714285714285715, 0.8999999999999999, 'True  '),
 Text(0.19047619047619047, 0.75, 'edad <= 42.5\ngini = 0.206\nsamples = 137\nvalue = [16, 121]'),
 Text(0.14285714285714285, 0.65, 'edad <= 41.5\ngini = 0.287\nsamples = 69\nvalue = [12, 57]'),
 Text(0.11904761904761904, 0.55, 'edad <= 27.5\ngini = 0.216\nsamples = 65\nvalue = [8, 57]'),
 Text(0.07142857142857142, 0.45, 'edad <= 23.5\ngini = 0.053\nsamples = 37\nvalue = [1, 36]'),
 Text(0.047619047619047616, 0.35, 'edad <= 21.5\ngini = 0.105\nsamples = 18\nvalue = [1, 17]'),
 Text(0.023809523809523808, 0.25, 'gini = 0.0\nsamples = 17\nvalue = [0, 17]'),
 Text(0.07142857142857142, 0.25, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.09523809523809523, 0.35, 'gini = 0.0\nsamples = 19\nvalue = [0, 19]'),
 Text(0.16666666666666666, 0.45, 'edad <= 29.0\ngini = 0.375\nsamples = 28\nvalue = [7, 21]'),
 Text(0.14285714285714285, 0.35, 'gini = 0.49\nsamples = 7\nvalue = [4, 3]'),
 Text(0.19047619047619047, 0.35, 'gini = 0.245\nsamples = 21\nvalue = [3, 18]'),
 Text(0.16666666666666666, 0.55, 'gini = 0.0\nsamples = 4\nvalue = [4, 0]'),
 Text(0.23809523809523808, 0.65, 'edad <= 53.5\ngini = 0.111\nsamples = 68\nvalue = [4, 64]'),
 Text(0.21428571428571427, 0.55, 'gini = 0.042\nsamples = 47\nvalue = [1, 46]'),
 Text(0.2619047619047619, 0.55, 'edad <= 54.5\ngini = 0.245\nsamples = 21\nvalue = [3, 18]'),
 Text(0.23809523809523808, 0.45, 'gini = 0.5\nsamples = 4\nvalue = [2, 2]'),
 Text(0.2857142857142857, 0.45, 'edad <= 58.5\ngini = 0.111\nsamples = 17\nvalue = [1, 16]'),
 Text(0.2619047619047619, 0.35, 'gini = 0.0\nsamples = 13\nvalue = [0, 13]'),
 Text(0.30952380952380953, 0.35, 'edad <= 60.5\ngini = 0.375\nsamples = 4\nvalue = [1, 3]'),
 Text(0.2857142857142857, 0.25, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.3333333333333333, 0.25, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.30952380952380953, 0.75, 'edad <= 46.0\ngini = 0.089\nsamples = 171\nvalue = [8, 163]'),
 Text(0.2857142857142857, 0.65, 'gini = 0.058\nsamples = 133\nvalue = [4, 129]'),
 Text(0.3333333333333333, 0.65, 'edad <= 48.0\ngini = 0.188\nsamples = 38\nvalue = [4, 34]'),
 Text(0.30952380952380953, 0.55, 'gini = 0.494\nsamples = 9\nvalue = [4, 5]'),
 Text(0.35714285714285715, 0.55, 'gini = 0.0\nsamples = 29\nvalue = [0, 29]'),
 Text(0.6785714285714286, 0.85, 'edad <= 40.5\ngini = 0.489\nsamples = 268\nvalue = [114, 154]'),
 Text(0.5714285714285714, 0.8999999999999999, '  False'),
 Text(0.5119047619047619, 0.75, 'sexo_num <= 0.5\ngini = 0.426\nsamples = 143\nvalue = [44, 99]'),
 Text(0.4523809523809524, 0.65, 'edad <= 21.5\ngini = 0.461\nsamples = 114\nvalue = [41, 73]'),
 Text(0.40476190476190477, 0.55, 'edad <= 19.5\ngini = 0.298\nsamples = 22\nvalue = [4, 18]'),
 Text(0.38095238095238093, 0.45, 'edad <= 18.5\ngini = 0.444\nsamples = 3\nvalue = [2, 1]'),
 Text(0.35714285714285715, 0.35, 'gini = 0.0\nsamples = 1\nvalue = [0, 1]'),
 Text(0.40476190476190477, 0.35, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.42857142857142855, 0.45, 'gini = 0.188\nsamples = 19\nvalue = [2, 17]'),
 Text(0.5, 0.55, 'edad <= 23.5\ngini = 0.481\nsamples = 92\nvalue = [37, 55]'),
 Text(0.47619047619047616, 0.45, 'gini = 0.48\nsamples = 15\nvalue = [9, 6]'),
 Text(0.5238095238095238, 0.45, 'edad <= 25.5\ngini = 0.463\nsamples = 77\nvalue = [28, 49]'),
 Text(0.5, 0.35, 'gini = 0.32\nsamples = 20\nvalue = [4, 16]'),
 Text(0.5476190476190477, 0.35, 'edad <= 29.5\ngini = 0.488\nsamples = 57\nvalue = [24, 33]'),
 Text(0.5238095238095238, 0.25, 'gini = 0.219\nsamples = 8\nvalue = [7, 1]'),
 Text(0.5714285714285714, 0.25, 'gini = 0.453\nsamples = 49\nvalue = [17.0, 32.0]'),
 Text(0.5714285714285714, 0.65, 'edad <= 19.0\ngini = 0.185\nsamples = 29\nvalue = [3, 26]'),
 Text(0.5476190476190477, 0.55, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.5952380952380952, 0.55, 'edad <= 32.0\ngini = 0.071\nsamples = 27\nvalue = [1, 26]'),
 Text(0.5714285714285714, 0.45, 'gini = 0.0\nsamples = 17\nvalue = [0, 17]'),
 Text(0.6190476190476191, 0.45, 'edad <= 34.0\ngini = 0.18\nsamples = 10\nvalue = [1, 9]'),
 Text(0.5952380952380952, 0.35, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.6428571428571429, 0.35, 'gini = 0.0\nsamples = 9\nvalue = [0, 9]'),
 Text(0.8452380952380952, 0.75, 'edad <= 56.5\ngini = 0.493\nsamples = 125\nvalue = [70, 55]'),
 Text(0.7857142857142857, 0.65, 'edad <= 52.5\ngini = 0.462\nsamples = 91\nvalue = [58.0, 33.0]'),
 Text(0.7380952380952381, 0.55, 'sexo_num <= 0.5\ngini = 0.5\nsamples = 47\nvalue = [23.0, 24.0]'),
 Text(0.7142857142857143, 0.45, 'edad <= 48.5\ngini = 0.465\nsamples = 38\nvalue = [14, 24]'),
 Text(0.6904761904761905, 0.35, 'edad <= 43.5\ngini = 0.494\nsamples = 27\nvalue = [12, 15]'),
 Text(0.6309523809523809, 0.25, 'edad <= 41.5\ngini = 0.444\nsamples = 9\nvalue = [3, 6]'),
 Text(0.6071428571428571, 0.15, 'gini = 0.444\nsamples = 3\nvalue = [2, 1]'),
 Text(0.6547619047619048, 0.15, 'gini = 0.278\nsamples = 6\nvalue = [1, 5]'),
 Text(0.75, 0.25, 'edad <= 45.5\ngini = 0.5\nsamples = 18\nvalue = [9, 9]'),
 Text(0.7023809523809523, 0.15, 'edad <= 44.5\ngini = 0.444\nsamples = 6\nvalue = [4, 2]'),
 Text(0.6785714285714286, 0.05, 'gini = 0.5\nsamples = 4\nvalue = [2, 2]'),
 Text(0.7261904761904762, 0.05, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.7976190476190477, 0.15, 'edad <= 47.0\ngini = 0.486\nsamples = 12\nvalue = [5, 7]'),
 Text(0.7738095238095238, 0.05, 'gini = 0.32\nsamples = 5\nvalue = [1, 4]'),
 Text(0.8214285714285714, 0.05, 'gini = 0.49\nsamples = 7\nvalue = [4, 3]'),
 Text(0.7380952380952381, 0.35, 'gini = 0.298\nsamples = 11\nvalue = [2, 9]'),
 Text(0.7619047619047619, 0.45, 'gini = 0.0\nsamples = 9\nvalue = [9, 0]'),
 Text(0.8333333333333334, 0.55, 'edad <= 53.5\ngini = 0.325\nsamples = 44\nvalue = [35, 9]'),
 Text(0.8095238095238095, 0.45, 'gini = 0.0\nsamples = 6\nvalue = [6, 0]'),
 Text(0.8571428571428571, 0.45, 'gini = 0.361\nsamples = 38\nvalue = [29, 9]'),
 Text(0.9047619047619048, 0.65, 'edad <= 58.0\ngini = 0.457\nsamples = 34\nvalue = [12, 22]'),
 Text(0.8809523809523809, 0.55, 'gini = 0.278\nsamples = 18\nvalue = [3, 15]'),
 Text(0.9285714285714286, 0.55, 'edad <= 61.0\ngini = 0.492\nsamples = 16\nvalue = [9, 7]'),
 Text(0.9047619047619048, 0.45, 'gini = 0.0\nsamples = 6\nvalue = [6, 0]'),
 Text(0.9523809523809523, 0.45, 'edad <= 64.5\ngini = 0.42\nsamples = 10\nvalue = [3, 7]'),
 Text(0.9285714285714286, 0.35, 'edad <= 62.5\ngini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.9047619047619048, 0.25, 'gini = 0.0\nsamples = 4\nvalue = [0, 4]'),
 Text(0.9523809523809523, 0.25, 'edad <= 63.5\ngini = 0.375\nsamples = 4\nvalue = [1, 3]'),
 Text(0.9285714285714286, 0.15, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.9761904761904762, 0.15, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.9761904761904762, 0.35, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]')]
No description has been provided for this image

El árbol generado cuenta con los siguientes datos:

  • Su profundidad (longitud vertical), es de 9.

  • Cuenta con 41 hojas.

In [244]:
print(pruned_tree.tree_.max_depth)
9
In [245]:
print(pruned_tree.get_n_leaves())
41
In [246]:
yhat_p = pruned_tree.predict(X_test)
acc_p = accuracy_score(y_test, yhat_p)
f1_p = f1_score(y_test, yhat_p, average = 'weighted')
print("Accuracy final: ", acc_p)
print("F1-score final: ", f1_p)
Accuracy final:  0.8611111111111112
F1-score final:  0.8546087480572208

El Accuracy del modelo podado es bueno, obtenemos un valor que se puede concluir que bajo el árbol podado se acierta la clasificación. La poda reduce la complejidad del árbol sin sacrificar significativamente el desempeño.

El F1-score del modelo también es bueno; este resultado puede presentar cierto sesgo hacia la clase mayoritaria dada la ausencia de balanceo, sin embargo, es comparable al árbol inicial desbalanceado.

In [247]:
# Matriz de confusión - Árbol inicial
from sklearn.metrics import confusion_matrix

conf_m = confusion_matrix(y_test, yhat_p)
sns.heatmap(conf_m, annot=True, fmt="d", cmap="Blues", cbar=False, square=True)
plt.ylabel('y_true')
plt.xlabel('y_pred')
plt.title('Matriz de Confusión - Árbol Podado.')
plt.show()
No description has been provided for this image

Matriz de confusión.

  • 21 + 103 = 124 aciertos.

  • Porcentaje de aciertos → 124/144 = 0.81 → 86%.

  • 6 + 14 = 20 errores.

144 datos en total.

Árbol de Decisión Podado SMOTE.

In [248]:
from sklearn.model_selection import cross_val_score, StratifiedKFold
skf = StratifiedKFold(n_splits = 4)
ccp = np.linspace(0.001, 0.2, 250)
cv_scores = []
for alpha in ccp:
    pruned_tree = DTC(ccp_alpha = alpha, class_weight='balanced')
    cv_scores.append(np.mean(cross_val_score(pruned_tree, X_train_smote, y_train_smote, cv = skf, scoring = 'f1')))
In [249]:
alpha = ccp[np.argmax(cv_scores)]
print("Best alpha: ", alpha)
Best alpha:  0.001
In [250]:
pruned_tree = DTC(ccp_alpha = alpha).fit(X_train_smote, y_train_smote)
plot_tree(pruned_tree, filled = True, feature_names = X_train_smote.columns)
Out[250]:
[Text(0.4253472222222222, 0.9583333333333334, 'institucion_unidad_medica_num <= 0.5\ngini = 0.5\nsamples = 876\nvalue = [438, 438]'),
 Text(0.19791666666666666, 0.875, 'sexo_num <= 0.5\ngini = 0.38\nsamples = 381\nvalue = [97, 284]'),
 Text(0.3116319444444444, 0.9166666666666667, 'True  '),
 Text(0.11805555555555555, 0.7916666666666666, 'edad <= 21.5\ngini = 0.484\nsamples = 205\nvalue = [84, 121]'),
 Text(0.09027777777777778, 0.7083333333333334, 'gini = 0.0\nsamples = 17\nvalue = [0, 17]'),
 Text(0.14583333333333334, 0.7083333333333334, 'edad <= 49.5\ngini = 0.494\nsamples = 188\nvalue = [84, 104]'),
 Text(0.06944444444444445, 0.625, 'edad <= 24.5\ngini = 0.498\nsamples = 136\nvalue = [72, 64]'),
 Text(0.041666666666666664, 0.5416666666666666, 'gini = 0.0\nsamples = 7\nvalue = [7, 0]'),
 Text(0.09722222222222222, 0.5416666666666666, 'edad <= 27.5\ngini = 0.5\nsamples = 129\nvalue = [65, 64]'),
 Text(0.06944444444444445, 0.4583333333333333, 'gini = 0.172\nsamples = 21\nvalue = [2, 19]'),
 Text(0.125, 0.4583333333333333, 'edad <= 29.0\ngini = 0.486\nsamples = 108\nvalue = [63.0, 45.0]'),
 Text(0.09722222222222222, 0.375, 'gini = 0.255\nsamples = 20\nvalue = [17, 3]'),
 Text(0.1527777777777778, 0.375, 'edad <= 34.0\ngini = 0.499\nsamples = 88\nvalue = [46, 42]'),
 Text(0.08333333333333333, 0.2916666666666667, 'edad <= 32.5\ngini = 0.32\nsamples = 10\nvalue = [2, 8]'),
 Text(0.05555555555555555, 0.20833333333333334, 'edad <= 31.0\ngini = 0.5\nsamples = 4\nvalue = [2, 2]'),
 Text(0.027777777777777776, 0.125, 'gini = 0.0\nsamples = 2\nvalue = [0, 2]'),
 Text(0.08333333333333333, 0.125, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.1111111111111111, 0.20833333333333334, 'gini = 0.0\nsamples = 6\nvalue = [0, 6]'),
 Text(0.2222222222222222, 0.2916666666666667, 'edad <= 42.5\ngini = 0.492\nsamples = 78\nvalue = [44, 34]'),
 Text(0.16666666666666666, 0.20833333333333334, 'edad <= 41.5\ngini = 0.401\nsamples = 36\nvalue = [26, 10]'),
 Text(0.1388888888888889, 0.125, 'gini = 0.473\nsamples = 26\nvalue = [16, 10]'),
 Text(0.19444444444444445, 0.125, 'gini = 0.0\nsamples = 10\nvalue = [10, 0]'),
 Text(0.2777777777777778, 0.20833333333333334, 'edad <= 45.0\ngini = 0.49\nsamples = 42\nvalue = [18, 24]'),
 Text(0.25, 0.125, 'gini = 0.0\nsamples = 12\nvalue = [0, 12]'),
 Text(0.3055555555555556, 0.125, 'edad <= 48.5\ngini = 0.48\nsamples = 30\nvalue = [18, 12]'),
 Text(0.2777777777777778, 0.041666666666666664, 'gini = 0.494\nsamples = 27\nvalue = [15, 12]'),
 Text(0.3333333333333333, 0.041666666666666664, 'gini = 0.0\nsamples = 3\nvalue = [3, 0]'),
 Text(0.2222222222222222, 0.625, 'edad <= 53.5\ngini = 0.355\nsamples = 52\nvalue = [12, 40]'),
 Text(0.19444444444444445, 0.5416666666666666, 'gini = 0.083\nsamples = 23\nvalue = [1, 22]'),
 Text(0.25, 0.5416666666666666, 'edad <= 54.5\ngini = 0.471\nsamples = 29\nvalue = [11, 18]'),
 Text(0.2222222222222222, 0.4583333333333333, 'gini = 0.298\nsamples = 11\nvalue = [9, 2]'),
 Text(0.2777777777777778, 0.4583333333333333, 'edad <= 58.5\ngini = 0.198\nsamples = 18\nvalue = [2, 16]'),
 Text(0.25, 0.375, 'gini = 0.0\nsamples = 13\nvalue = [0, 13]'),
 Text(0.3055555555555556, 0.375, 'edad <= 60.5\ngini = 0.48\nsamples = 5\nvalue = [2, 3]'),
 Text(0.2777777777777778, 0.2916666666666667, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.3333333333333333, 0.2916666666666667, 'gini = 0.0\nsamples = 3\nvalue = [0, 3]'),
 Text(0.2777777777777778, 0.7916666666666666, 'edad <= 46.0\ngini = 0.137\nsamples = 176\nvalue = [13, 163]'),
 Text(0.25, 0.7083333333333334, 'gini = 0.058\nsamples = 133\nvalue = [4, 129]'),
 Text(0.3055555555555556, 0.7083333333333334, 'edad <= 48.0\ngini = 0.331\nsamples = 43\nvalue = [9, 34]'),
 Text(0.2777777777777778, 0.625, 'gini = 0.459\nsamples = 14\nvalue = [9, 5]'),
 Text(0.3333333333333333, 0.625, 'gini = 0.0\nsamples = 29\nvalue = [0, 29]'),
 Text(0.6527777777777778, 0.875, 'edad <= 42.5\ngini = 0.429\nsamples = 495\nvalue = [341, 154]'),
 Text(0.5390625, 0.9166666666666667, '  False'),
 Text(0.4722222222222222, 0.7916666666666666, 'sexo_num <= 0.5\ngini = 0.493\nsamples = 232\nvalue = [130, 102]'),
 Text(0.4166666666666667, 0.7083333333333334, 'edad <= 19.5\ngini = 0.468\nsamples = 203\nvalue = [127, 76]'),
 Text(0.3888888888888889, 0.625, 'gini = 0.124\nsamples = 15\nvalue = [14, 1]'),
 Text(0.4444444444444444, 0.625, 'edad <= 21.5\ngini = 0.48\nsamples = 188\nvalue = [113, 75]'),
 Text(0.4166666666666667, 0.5416666666666666, 'gini = 0.188\nsamples = 19\nvalue = [2, 17]'),
 Text(0.4722222222222222, 0.5416666666666666, 'edad <= 23.5\ngini = 0.451\nsamples = 169\nvalue = [111.0, 58.0]'),
 Text(0.4444444444444444, 0.4583333333333333, 'gini = 0.26\nsamples = 39\nvalue = [33, 6]'),
 Text(0.5, 0.4583333333333333, 'edad <= 25.5\ngini = 0.48\nsamples = 130\nvalue = [78, 52]'),
 Text(0.4722222222222222, 0.375, 'gini = 0.473\nsamples = 26\nvalue = [10, 16]'),
 Text(0.5277777777777778, 0.375, 'edad <= 28.5\ngini = 0.453\nsamples = 104\nvalue = [68, 36]'),
 Text(0.5, 0.2916666666666667, 'gini = 0.0\nsamples = 12\nvalue = [12, 0]'),
 Text(0.5555555555555556, 0.2916666666666667, 'gini = 0.476\nsamples = 92\nvalue = [56, 36]'),
 Text(0.5277777777777778, 0.7083333333333334, 'edad <= 19.0\ngini = 0.185\nsamples = 29\nvalue = [3, 26]'),
 Text(0.5, 0.625, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]'),
 Text(0.5555555555555556, 0.625, 'edad <= 32.0\ngini = 0.071\nsamples = 27\nvalue = [1, 26]'),
 Text(0.5277777777777778, 0.5416666666666666, 'gini = 0.0\nsamples = 17\nvalue = [0, 17]'),
 Text(0.5833333333333334, 0.5416666666666666, 'edad <= 34.0\ngini = 0.18\nsamples = 10\nvalue = [1, 9]'),
 Text(0.5555555555555556, 0.4583333333333333, 'gini = 0.0\nsamples = 1\nvalue = [1, 0]'),
 Text(0.6111111111111112, 0.4583333333333333, 'gini = 0.0\nsamples = 9\nvalue = [0, 9]'),
 Text(0.8333333333333334, 0.7916666666666666, 'edad <= 56.5\ngini = 0.317\nsamples = 263\nvalue = [211, 52]'),
 Text(0.7777777777777778, 0.7083333333333334, 'edad <= 52.5\ngini = 0.249\nsamples = 206\nvalue = [176.0, 30.0]'),
 Text(0.75, 0.625, 'sexo_num <= 0.5\ngini = 0.363\nsamples = 88\nvalue = [67, 21]'),
 Text(0.7222222222222222, 0.5416666666666666, 'edad <= 48.5\ngini = 0.458\nsamples = 59\nvalue = [38, 21]'),
 Text(0.6666666666666666, 0.4583333333333333, 'edad <= 47.0\ngini = 0.397\nsamples = 44\nvalue = [32, 12]'),
 Text(0.6388888888888888, 0.375, 'edad <= 45.5\ngini = 0.495\nsamples = 20\nvalue = [11, 9]'),
 Text(0.6111111111111112, 0.2916666666666667, 'edad <= 43.5\ngini = 0.444\nsamples = 15\nvalue = [10, 5]'),
 Text(0.5833333333333334, 0.20833333333333334, 'gini = 0.375\nsamples = 4\nvalue = [1, 3]'),
 Text(0.6388888888888888, 0.20833333333333334, 'gini = 0.298\nsamples = 11\nvalue = [9, 2]'),
 Text(0.6666666666666666, 0.2916666666666667, 'gini = 0.32\nsamples = 5\nvalue = [1, 4]'),
 Text(0.6944444444444444, 0.375, 'gini = 0.219\nsamples = 24\nvalue = [21, 3]'),
 Text(0.7777777777777778, 0.4583333333333333, 'edad <= 51.0\ngini = 0.48\nsamples = 15\nvalue = [6, 9]'),
 Text(0.75, 0.375, 'gini = 0.219\nsamples = 8\nvalue = [1, 7]'),
 Text(0.8055555555555556, 0.375, 'gini = 0.408\nsamples = 7\nvalue = [5, 2]'),
 Text(0.7777777777777778, 0.5416666666666666, 'gini = 0.0\nsamples = 29\nvalue = [29, 0]'),
 Text(0.8055555555555556, 0.625, 'gini = 0.141\nsamples = 118\nvalue = [109, 9]'),
 Text(0.8888888888888888, 0.7083333333333334, 'edad <= 58.0\ngini = 0.474\nsamples = 57\nvalue = [35, 22]'),
 Text(0.8611111111111112, 0.625, 'gini = 0.332\nsamples = 19\nvalue = [4, 15]'),
 Text(0.9166666666666666, 0.625, 'edad <= 61.5\ngini = 0.301\nsamples = 38\nvalue = [31, 7]'),
 Text(0.8888888888888888, 0.5416666666666666, 'gini = 0.0\nsamples = 23\nvalue = [23, 0]'),
 Text(0.9444444444444444, 0.5416666666666666, 'edad <= 64.5\ngini = 0.498\nsamples = 15\nvalue = [8, 7]'),
 Text(0.9166666666666666, 0.4583333333333333, 'gini = 0.497\nsamples = 13\nvalue = [6, 7]'),
 Text(0.9722222222222222, 0.4583333333333333, 'gini = 0.0\nsamples = 2\nvalue = [2, 0]')]
No description has been provided for this image

De este otro árbol generado, también vamos a abordar estos dos datos que arroja el mismo:

  • Su profundidad (longitud vertical), es de 11.

  • Cuenta con 42 hojas.

In [251]:
print(pruned_tree.tree_.max_depth)
11
In [252]:
print(pruned_tree.get_n_leaves())
42
In [253]:
yhat_p = pruned_tree.predict(X_test)
acc_p = accuracy_score(y_test, yhat_p)
f1_p = f1_score(y_test, yhat_p, average = 'weighted')
print("Accuracy final: ", acc_p)
print("F1-score final: ", f1_p)
Accuracy final:  0.7916666666666666
F1-score final:  0.8018790849673203

El Accuracy del modelo podado con SMOTE es bueno, obtenemos un valor de 0.79, por lo que se puede concluir que bajo el árbol podado balanceado se acierta la clasificación un 79%.

El F1-score del modelo también es bueno, obtenemos un valor de 0.80 y esto viene debido al SMOTE — no se tendrá un sesgo hacia una clase en específico, confirmando lo dicho a un 80%. La leve diferencia con el árbol inicial SMOTE confirma que la poda no degradó el desempeño de forma importante.

In [254]:
# Matriz de confusión - Árbol inicial
from sklearn.metrics import confusion_matrix

conf_m = confusion_matrix(y_test, yhat_p)
sns.heatmap(conf_m, annot=True, fmt="d", cmap="Blues", cbar=False, square=True)
plt.ylabel('y_true')
plt.xlabel('y_pred')
plt.title('Matriz de Confusión - Árbol Podado.')
plt.show()
No description has been provided for this image

Matriz de confusión.

  • 27 + 87 = 114 aciertos.

  • Porcentaje de aciertos → 114/144 = 0.79 → 79%.

  • 22 + 8 = 30 errores.

144 datos en total.

Métricas.

Las métricas de desempeño evaluadas para estos modelos fueron:

  • Accuracy: aciertos totales.

  • F1-score: promedio ponderado de las clases.

Random Forest Classifier.¶

Random Forest Classifier - by Default.

Comenzamos con el modelo de Random Forest Classifier donde no se está implementando una técnica de balanceo de clases para ver cómo se comporta el algoritmo con el desbalance que existe y que se mencionó anteriormente.

Definimos variables:

In [255]:
# Definimos entrada y salida.
X = df2[['edad', 'sexo_num', 'institucion_unidad_medica_num']]
y = df2['descripcion_grupo_enfermedad_num']
print(X.shape)
print(y.shape)
(720, 3)
(720,)

Separamos los datos en entrenamiento y prueba:

In [256]:
# Dividir los datos en train y test.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0, stratify = y)
print(y_train.value_counts())
X_train.shape, X_test.shape, y_train.shape, y_test.shape
descripcion_grupo_enfermedad_num
1    438
0    138
Name: count, dtype: int64
Out[256]:
((576, 3), (144, 3), (576,), (144,))

Definimos el modelo de Random Forest Classifier sin balancear con los siguientes hiperparámetros principales:

  • n_estimators: 100 → valor por default.

  • criterion: Gini → definir la mejor regla de separación.

  • max_depth: none → no existe límite para el crecimiento del bosque.

  • class_weight: none → no se está aplicando ninguna técnica de balanceo.

  • random_state = 0 → con esta condición, al momento de hacer el bootstrap se asegura que los datos se dividan igual en cada ocasión.

In [257]:
# Definir el modelo by Default.
model1 = RandomForestClassifier(random_state = 0)
# Entrenemos el modelo FS.
model1.fit(X_train, y_train)
Out[257]:
RandomForestClassifier(random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
n_estimators n_estimators: int, default=100

The number of trees in the forest.

.. versionchanged:: 0.22
The default value of ``n_estimators`` changed from 10 to 100
in 0.22.
100
criterion criterion: {"gini", "entropy", "log_loss"}, default="gini"

The function to measure the quality of a split. Supported criteria are
"gini" for the Gini impurity and "log_loss" and "entropy" both for the
Shannon information gain, see :ref:`tree_mathematical_formulation`.
Note: This parameter is tree-specific.
'gini'
max_depth max_depth: int, default=None

The maximum depth of the tree. If None, then nodes are expanded until
all leaves are pure or until all leaves contain less than
min_samples_split samples.
None
min_samples_split min_samples_split: int or float, default=2

The minimum number of samples required to split an internal node:

- If int, then consider `min_samples_split` as the minimum number.
- If float, then `min_samples_split` is a fraction and
`ceil(min_samples_split * n_samples)` are the minimum
number of samples for each split.

.. versionchanged:: 0.18
Added float values for fractions.
2
min_samples_leaf min_samples_leaf: int or float, default=1

The minimum number of samples required to be at a leaf node.
A split point at any depth will only be considered if it leaves at
least ``min_samples_leaf`` training samples in each of the left and
right branches. This may have the effect of smoothing the model,
especially in regression.

- If int, then consider `min_samples_leaf` as the minimum number.
- If float, then `min_samples_leaf` is a fraction and
`ceil(min_samples_leaf * n_samples)` are the minimum
number of samples for each node.

.. versionchanged:: 0.18
Added float values for fractions.
1
min_weight_fraction_leaf min_weight_fraction_leaf: float, default=0.0

The minimum weighted fraction of the sum total of weights (of all
the input samples) required to be at a leaf node. Samples have
equal weight when sample_weight is not provided.
0.0
max_features max_features: {"sqrt", "log2", None}, int or float, default="sqrt"

The number of features to consider when looking for the best split:

- If int, then consider `max_features` features at each split.
- If float, then `max_features` is a fraction and
`max(1, int(max_features * n_features_in_))` features are considered at each
split.
- If "sqrt", then `max_features=sqrt(n_features)`.
- If "log2", then `max_features=log2(n_features)`.
- If None, then `max_features=n_features`.

.. versionchanged:: 1.1
The default of `max_features` changed from `"auto"` to `"sqrt"`.

Note: the search for a split does not stop until at least one
valid partition of the node samples is found, even if it requires to
effectively inspect more than ``max_features`` features.
'sqrt'
max_leaf_nodes max_leaf_nodes: int, default=None

Grow trees with ``max_leaf_nodes`` in best-first fashion.
Best nodes are defined as relative reduction in impurity.
If None then unlimited number of leaf nodes.
None
min_impurity_decrease min_impurity_decrease: float, default=0.0

A node will be split if this split induces a decrease of the impurity
greater than or equal to this value.

The weighted impurity decrease equation is the following::

N_t / N * (impurity - N_t_R / N_t * right_impurity
- N_t_L / N_t * left_impurity)

where ``N`` is the total number of samples, ``N_t`` is the number of
samples at the current node, ``N_t_L`` is the number of samples in the
left child, and ``N_t_R`` is the number of samples in the right child.

``N``, ``N_t``, ``N_t_R`` and ``N_t_L`` all refer to the weighted sum,
if ``sample_weight`` is passed.

.. versionadded:: 0.19
0.0
bootstrap bootstrap: bool, default=True

Whether bootstrap samples are used when building trees. If False, the
whole dataset is used to build each tree.
True
oob_score oob_score: bool or callable, default=False

Whether to use out-of-bag samples to estimate the generalization score.
By default, :func:`~sklearn.metrics.accuracy_score` is used.
Provide a callable with signature `metric(y_true, y_pred)` to use a
custom metric. Only available if `bootstrap=True`.

For an illustration of out-of-bag (OOB) error estimation, see the example
:ref:`sphx_glr_auto_examples_ensemble_plot_ensemble_oob.py`.
False
n_jobs n_jobs: int, default=None

The number of jobs to run in parallel. :meth:`fit`, :meth:`predict`,
:meth:`decision_path` and :meth:`apply` are all parallelized over the
trees. ``None`` means 1 unless in a :obj:`joblib.parallel_backend`
context. ``-1`` means using all processors. See :term:`Glossary
` for more details.
None
random_state random_state: int, RandomState instance or None, default=None

Controls both the randomness of the bootstrapping of the samples used
when building trees (if ``bootstrap=True``) and the sampling of the
features to consider when looking for the best split at each node
(if ``max_features < n_features``).
See :term:`Glossary ` for details.
0
verbose verbose: int, default=0

Controls the verbosity when fitting and predicting.
0
warm_start warm_start: bool, default=False

When set to ``True``, reuse the solution of the previous call to fit
and add more estimators to the ensemble, otherwise, just fit a whole
new forest. See :term:`Glossary ` and
:ref:`tree_ensemble_warm_start` for details.
False
class_weight class_weight: {"balanced", "balanced_subsample"}, dict or list of dicts, default=None

Weights associated with classes in the form ``{class_label: weight}``.
If not given, all classes are supposed to have weight one. For
multi-output problems, a list of dicts can be provided in the same
order as the columns of y.

Note that for multioutput (including multilabel) weights should be
defined for each class of every column in its own dict. For example,
for four-class multilabel classification weights should be
[{0: 1, 1: 1}, {0: 1, 1: 5}, {0: 1, 1: 1}, {0: 1, 1: 1}] instead of
[{1:1}, {2:5}, {3:1}, {4:1}].

The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``

The "balanced_subsample" mode is the same as "balanced" except that
weights are computed based on the bootstrap sample for every tree
grown.

For multi-output, the weights of each column of y will be multiplied.

Note that these weights will be multiplied with sample_weight (passed
through the fit method) if sample_weight is specified.
None
ccp_alpha ccp_alpha: non-negative float, default=0.0

Complexity parameter used for Minimal Cost-Complexity Pruning. The
subtree with the largest cost complexity that is smaller than
``ccp_alpha`` will be chosen. By default, no pruning is performed. See
:ref:`minimal_cost_complexity_pruning` for details. See
:ref:`sphx_glr_auto_examples_tree_plot_cost_complexity_pruning.py`
for an example of such pruning.

.. versionadded:: 0.22
0.0
max_samples max_samples: int or float, default=None

If bootstrap is True, the number of samples to draw from X
to train each base estimator.

- If None (default), then draw `X.shape[0]` samples.
- If int, then draw `max_samples` samples.
- If float, then draw `max(round(n_samples * max_samples), 1)` samples. Thus,
`max_samples` should be in the interval `(0.0, 1.0]`.

.. versionadded:: 0.22
None
monotonic_cst monotonic_cst: array-like of int of shape (n_features), default=None

Indicates the monotonicity constraint to enforce on each feature.
- 1: monotonic increase
- 0: no constraint
- -1: monotonic decrease

If monotonic_cst is None, no constraints are applied.

Monotonicity constraints are not supported for:
- multiclass classifications (i.e. when `n_classes > 2`),
- multioutput classifications (i.e. when `n_outputs_ > 1`),
- classifications trained on data with missing values.

The constraints hold over the probability of the positive class.

Read more in the :ref:`User Guide `.

.. versionadded:: 1.4
None

Validamos si el modelo pronostica adecuadamente:

In [258]:
# Validar si el modelo pronostica adecuadamente.
y_pred_test = model1.predict(X_test)
print(y_pred_test[0:5])
print(y_test.head())
[1 0 0 1 1]
46382    0
32275    0
7409     1
32248    1
46407    1
Name: descripcion_grupo_enfermedad_num, dtype: int64

3 aciertos de 5.

In [259]:
accuracy_train = model1.score(X_train, y_train)
print('Accuracy train = {:.2f}'.format(accuracy_train))
accuracy_test = model1.score(X_test, y_test)
print('Accuracy test = {:.2f}'.format(accuracy_test))
print('Diferencia = {:.4f}%'.format(np.abs(accuracy_train-accuracy_test)*100))
Accuracy train = 0.88
Accuracy test = 0.86
Diferencia = 2.0833%

Hasta este punto, obtenemos que el modelo tiene un leve sobreajuste.

Sin embargo, su Accuracy de 86% apunta a que sabe generalizar bien entre los positivos reales y negativos reales (1 y 0 respectivamente).

In [260]:
predRF = model1.predict(X_test)
repDT = classification_report(y_test, predRF)
print(repDT)

from imblearn.metrics import geometric_mean_score
print('G-mean =', geometric_mean_score(y_test, predRF))

disp = ConfusionMatrixDisplay.from_predictions(y_test, predRF, cmap = plt.cm.Blues)

# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
              precision    recall  f1-score   support

           0       0.78      0.60      0.68        35
           1       0.88      0.94      0.91       109

    accuracy                           0.86       144
   macro avg       0.83      0.77      0.79       144
weighted avg       0.86      0.86      0.85       144

G-mean = 0.7529757479920719
No description has been provided for this image

Matriz de confusión.

  • 21 + 103 = 124 aciertos.

  • Porcentaje de aciertos → 124 / 144 = 0.86 → 86%.

  • 6 + 14 = 20 errores.

144 datos en total.

Reporte de clasificación.

  • 35 datos de prueba de Factores influyendo en el estado de salud.

    • 60% de positivos encontrados.
    • 78% fueron clasificados correctamente.
  • 109 datos de prueba de Trastorno mental y del comportamiento.

    • 94% de positivos encontrados.
    • 88% fueron clasificados correctamente.

F1-Score: promedio ponderado de las clases.

  • Factores influyendo en el estado de salud: 68%.
  • Trastorno mental y del comportamiento: 91%.

Porcentaje de proporción a encontrar todas las clasificaciones de este estudio (G-mean): 75%.

Este porcentaje de proporción es el que queremos mejorar lo más que se pueda con la técnica de balanceo.

Random Forest Classifier - SMOTE.

Procedemos a realizar el Random Forest Classifier con la técnica de balanceo de SMOTE, esta se escogió ya que fue la técnica con un mejor desempeño — esto es mejor que SMOTEENN que va a eliminar datos, escenario que en este caso no es factible ya que se tienen relativamente pocos datos con el dataset construido.

In [261]:
from imblearn.over_sampling import SMOTE

# Definir la técnica.
smote = SMOTE(random_state = 0)
# Aplicamos.
X_smote, y_smote = smote.fit_resample(X, y)

# Comprobar si funicona.
print('Tamaño de X antes de SMOTE:', X.shape)
print('Tamaño de X después de SMOTE:', X_smote.shape)
print('Balance de clases con SMOTE:', y_smote.value_counts())
print('Nuestras clases están balanceadas.')
Tamaño de X antes de SMOTE: (720, 3)
Tamaño de X después de SMOTE: (1094, 3)
Balance de clases con SMOTE: descripcion_grupo_enfermedad_num
1    547
0    547
Name: count, dtype: int64
Nuestras clases están balanceadas.

Separamos los datos en entrenamiento y prueba:

In [262]:
# Dividir los tratos en train y test.
X_train, X_test, y_train, y_test = train_test_split(X_smote, y_smote, test_size = 0.2, random_state = 0, stratify = y_smote)
print(y_train.value_counts())
X_train.shape, X_test.shape, y_train.shape, y_test.shape
# No dividir de manera que se desbalanceen los datos -> cuidado con el random_state).
descripcion_grupo_enfermedad_num
1    438
0    437
Name: count, dtype: int64
Out[262]:
((875, 3), (219, 3), (875,), (219,))

Definimos el modelo de Random Forest Classifier balanceado con los siguientes hiperparámetros principales:

  • n_estimators: 100 → valor por default.

  • criterion: Gini → definir la mejor regla de separación.

  • max_depth: none → no existe límite para el crecimiento del bosque.

  • class_weight: none → el balanceo viene de la librería SMOTE, no del hiperparámetro.

  • random_state = 0 → con esta condición, al momento de hacer el bootstrap se asegura que los datos se dividan igual en cada ocasión.

Son los mismos al Random Forest Classifier by Default.

In [263]:
# Definir el modelo balanceado.
model2 = RandomForestClassifier(random_state = 0)
# Entrenemos el modelo FS.
model2.fit(X_smote, y_smote)
Out[263]:
RandomForestClassifier(random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
n_estimators n_estimators: int, default=100

The number of trees in the forest.

.. versionchanged:: 0.22
The default value of ``n_estimators`` changed from 10 to 100
in 0.22.
100
criterion criterion: {"gini", "entropy", "log_loss"}, default="gini"

The function to measure the quality of a split. Supported criteria are
"gini" for the Gini impurity and "log_loss" and "entropy" both for the
Shannon information gain, see :ref:`tree_mathematical_formulation`.
Note: This parameter is tree-specific.
'gini'
max_depth max_depth: int, default=None

The maximum depth of the tree. If None, then nodes are expanded until
all leaves are pure or until all leaves contain less than
min_samples_split samples.
None
min_samples_split min_samples_split: int or float, default=2

The minimum number of samples required to split an internal node:

- If int, then consider `min_samples_split` as the minimum number.
- If float, then `min_samples_split` is a fraction and
`ceil(min_samples_split * n_samples)` are the minimum
number of samples for each split.

.. versionchanged:: 0.18
Added float values for fractions.
2
min_samples_leaf min_samples_leaf: int or float, default=1

The minimum number of samples required to be at a leaf node.
A split point at any depth will only be considered if it leaves at
least ``min_samples_leaf`` training samples in each of the left and
right branches. This may have the effect of smoothing the model,
especially in regression.

- If int, then consider `min_samples_leaf` as the minimum number.
- If float, then `min_samples_leaf` is a fraction and
`ceil(min_samples_leaf * n_samples)` are the minimum
number of samples for each node.

.. versionchanged:: 0.18
Added float values for fractions.
1
min_weight_fraction_leaf min_weight_fraction_leaf: float, default=0.0

The minimum weighted fraction of the sum total of weights (of all
the input samples) required to be at a leaf node. Samples have
equal weight when sample_weight is not provided.
0.0
max_features max_features: {"sqrt", "log2", None}, int or float, default="sqrt"

The number of features to consider when looking for the best split:

- If int, then consider `max_features` features at each split.
- If float, then `max_features` is a fraction and
`max(1, int(max_features * n_features_in_))` features are considered at each
split.
- If "sqrt", then `max_features=sqrt(n_features)`.
- If "log2", then `max_features=log2(n_features)`.
- If None, then `max_features=n_features`.

.. versionchanged:: 1.1
The default of `max_features` changed from `"auto"` to `"sqrt"`.

Note: the search for a split does not stop until at least one
valid partition of the node samples is found, even if it requires to
effectively inspect more than ``max_features`` features.
'sqrt'
max_leaf_nodes max_leaf_nodes: int, default=None

Grow trees with ``max_leaf_nodes`` in best-first fashion.
Best nodes are defined as relative reduction in impurity.
If None then unlimited number of leaf nodes.
None
min_impurity_decrease min_impurity_decrease: float, default=0.0

A node will be split if this split induces a decrease of the impurity
greater than or equal to this value.

The weighted impurity decrease equation is the following::

N_t / N * (impurity - N_t_R / N_t * right_impurity
- N_t_L / N_t * left_impurity)

where ``N`` is the total number of samples, ``N_t`` is the number of
samples at the current node, ``N_t_L`` is the number of samples in the
left child, and ``N_t_R`` is the number of samples in the right child.

``N``, ``N_t``, ``N_t_R`` and ``N_t_L`` all refer to the weighted sum,
if ``sample_weight`` is passed.

.. versionadded:: 0.19
0.0
bootstrap bootstrap: bool, default=True

Whether bootstrap samples are used when building trees. If False, the
whole dataset is used to build each tree.
True
oob_score oob_score: bool or callable, default=False

Whether to use out-of-bag samples to estimate the generalization score.
By default, :func:`~sklearn.metrics.accuracy_score` is used.
Provide a callable with signature `metric(y_true, y_pred)` to use a
custom metric. Only available if `bootstrap=True`.

For an illustration of out-of-bag (OOB) error estimation, see the example
:ref:`sphx_glr_auto_examples_ensemble_plot_ensemble_oob.py`.
False
n_jobs n_jobs: int, default=None

The number of jobs to run in parallel. :meth:`fit`, :meth:`predict`,
:meth:`decision_path` and :meth:`apply` are all parallelized over the
trees. ``None`` means 1 unless in a :obj:`joblib.parallel_backend`
context. ``-1`` means using all processors. See :term:`Glossary
` for more details.
None
random_state random_state: int, RandomState instance or None, default=None

Controls both the randomness of the bootstrapping of the samples used
when building trees (if ``bootstrap=True``) and the sampling of the
features to consider when looking for the best split at each node
(if ``max_features < n_features``).
See :term:`Glossary ` for details.
0
verbose verbose: int, default=0

Controls the verbosity when fitting and predicting.
0
warm_start warm_start: bool, default=False

When set to ``True``, reuse the solution of the previous call to fit
and add more estimators to the ensemble, otherwise, just fit a whole
new forest. See :term:`Glossary ` and
:ref:`tree_ensemble_warm_start` for details.
False
class_weight class_weight: {"balanced", "balanced_subsample"}, dict or list of dicts, default=None

Weights associated with classes in the form ``{class_label: weight}``.
If not given, all classes are supposed to have weight one. For
multi-output problems, a list of dicts can be provided in the same
order as the columns of y.

Note that for multioutput (including multilabel) weights should be
defined for each class of every column in its own dict. For example,
for four-class multilabel classification weights should be
[{0: 1, 1: 1}, {0: 1, 1: 5}, {0: 1, 1: 1}, {0: 1, 1: 1}] instead of
[{1:1}, {2:5}, {3:1}, {4:1}].

The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``

The "balanced_subsample" mode is the same as "balanced" except that
weights are computed based on the bootstrap sample for every tree
grown.

For multi-output, the weights of each column of y will be multiplied.

Note that these weights will be multiplied with sample_weight (passed
through the fit method) if sample_weight is specified.
None
ccp_alpha ccp_alpha: non-negative float, default=0.0

Complexity parameter used for Minimal Cost-Complexity Pruning. The
subtree with the largest cost complexity that is smaller than
``ccp_alpha`` will be chosen. By default, no pruning is performed. See
:ref:`minimal_cost_complexity_pruning` for details. See
:ref:`sphx_glr_auto_examples_tree_plot_cost_complexity_pruning.py`
for an example of such pruning.

.. versionadded:: 0.22
0.0
max_samples max_samples: int or float, default=None

If bootstrap is True, the number of samples to draw from X
to train each base estimator.

- If None (default), then draw `X.shape[0]` samples.
- If int, then draw `max_samples` samples.
- If float, then draw `max(round(n_samples * max_samples), 1)` samples. Thus,
`max_samples` should be in the interval `(0.0, 1.0]`.

.. versionadded:: 0.22
None
monotonic_cst monotonic_cst: array-like of int of shape (n_features), default=None

Indicates the monotonicity constraint to enforce on each feature.
- 1: monotonic increase
- 0: no constraint
- -1: monotonic decrease

If monotonic_cst is None, no constraints are applied.

Monotonicity constraints are not supported for:
- multiclass classifications (i.e. when `n_classes > 2`),
- multioutput classifications (i.e. when `n_outputs_ > 1`),
- classifications trained on data with missing values.

The constraints hold over the probability of the positive class.

Read more in the :ref:`User Guide `.

.. versionadded:: 1.4
None

Validamos si el modelo pronostica adecuadamente:

In [264]:
# Validar si el modelo pronostica adecuadamente.
y_pred_test = model2.predict(X_test)
print(y_pred_test[0:5])
print(y_test.head())
[0 1 0 0 0]
944     0
199     1
1034    0
405     0
727     0
Name: descripcion_grupo_enfermedad_num, dtype: int64

5 aciertos de 5.

In [265]:
accuracy_train = model2.score(X_train, y_train)
print('Accuracy train = {:.2f}'.format(accuracy_train))
accuracy_test = model2.score(X_test, y_test)
print('Accuracy test = {:.2f}'.format(accuracy_test))
print('Diferencia = {:.4f}%'.format(np.abs(accuracy_train-accuracy_test)*100))
Accuracy train = 0.87
Accuracy test = 0.90
Diferencia = 2.7543%

Hasta este punto, obtenemos que el modelo balanceado también tiene un leve sobreajuste.

Sin embargo, su Accuracy de 90% apunta a que sabe generalizar muy bien entre los positivos reales y negativos reales (1 y 0 respectivamente).

In [266]:
predRF = model2.predict(X_test)
repDT = classification_report(y_test, predRF)
print(repDT)

from imblearn.metrics import geometric_mean_score
print('G-mean =', geometric_mean_score(y_test, predRF))

disp = ConfusionMatrixDisplay.from_predictions(y_test, predRF, cmap = plt.cm.Blues)

# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
              precision    recall  f1-score   support

           0       0.89      0.91      0.90       110
           1       0.91      0.89      0.90       109

    accuracy                           0.90       219
   macro avg       0.90      0.90      0.90       219
weighted avg       0.90      0.90      0.90       219

G-mean = 0.8994484455794076
No description has been provided for this image

Matriz de confusión.

  • 100 + 97 = 197 aciertos.

  • Porcentaje de aciertos → 197 / 219 = 0.89 → 89%.

  • 12 + 10 = 22 errores.

219 datos en total.

Reporte de clasificación.

  • 110 datos de prueba de Factores influyendo en el estado de salud.

    • 91% de positivos encontrados.
    • 89% fueron clasificados correctamente.
  • 109 datos de prueba de Trastorno mental y del comportamiento.

    • 89% de positivos encontrados.
    • 91% fueron clasificados correctamente.

F1-Score: promedio ponderado de las clases.

  • Factores influyendo en el estado de salud: 90%.
  • Trastorno mental y del comportamiento: 90%.

Porcentaje de proporción a encontrar todas las clasificaciones de este estudio (G-mean): 89%.

Este G-mean de 89% representa una mejora significativa respecto al modelo desbalanceado, gracias a la técnica de SMOTE.

Métricas.

Las métricas de desempeño evaluadas para estos modelos fueron:

  • Accuracy.

  • Matriz de confusión.

  • G-mean.

  • Precision.

  • Recall (Sensibilidad).

  • F1-Score.

Support Vector Machine (SVM).¶

SVM - by Default.

Procederemos a realizar el SVM sin técnica de balanceo para ver cómo se comporta con el desbalance existente.

In [277]:
from sklearn.svm import SVC

# Definimos entrada y salida.
X = df2[['edad', 'sexo_num', 'institucion_unidad_medica_num']]
y = df2['descripcion_grupo_enfermedad_num']

Separamos los datos en entrenamiento y prueba:

In [278]:
# Dividimos en train y test.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0, stratify = y)
print(y_train.value_counts())
X_train.shape, X_test.shape
descripcion_grupo_enfermedad_num
1    438
0    138
Name: count, dtype: int64
Out[278]:
((576, 3), (144, 3))

En este punto vale la pena mencionar que se utilizará un escalador de características, siendo en este caso el StandardScaler.

Según Aylin Tokuç (2025), este escalado de características consiste en asignar los valores de las características de un conjunto de datos al mismo rango — es crucial para algoritmos como el SVM ya que este considera las distancias entre observaciones (margen), dicha distancia difiere entre los datos sin escalar a aquellos escalados, aparte de la sensibilidad en las características.

In [279]:
# Escalamos las características (recomendado para SVM).
scaler_svm = StandardScaler()
X_train_svm = scaler_svm.fit_transform(X_train)
X_test_svm = scaler_svm.transform(X_test)

Definimos el modelo de SVM sin balancear con los siguientes hiperparámetros principales:

  • C: 1 → penalización proporcional al valor absoluto de los coeficientes del modelo.

  • kernel: rbf → definir la mejor regla de separación.

  • degree: 3 → grado del kernel.

  • gamma: scale → coeficiente del kernel.

  • class_weight: none → no se está aplicando ninguna técnica de balanceo.

In [280]:
# Definir el modelo by Default.
model_svm1 = SVC(kernel = 'rbf', random_state = 0)
# Entrenemos el modelo SVM.
model_svm1.fit(X_train_svm, y_train)
Out[280]:
SVC(random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
C C: float, default=1.0

Regularization parameter. The strength of the regularization is
inversely proportional to C. Must be strictly positive. The penalty
is a squared l2 penalty. For an intuitive visualization of the effects
of scaling the regularization parameter C, see
:ref:`sphx_glr_auto_examples_svm_plot_svm_scale_c.py`.
1.0
kernel kernel: {'linear', 'poly', 'rbf', 'sigmoid', 'precomputed'} or callable, default='rbf'

Specifies the kernel type to be used in the algorithm. If
none is given, 'rbf' will be used. If a callable is given it is used to
pre-compute the kernel matrix from data matrices; that matrix should be
an array of shape ``(n_samples, n_samples)``. For an intuitive
visualization of different kernel types see
:ref:`sphx_glr_auto_examples_svm_plot_svm_kernels.py`.
'rbf'
degree degree: int, default=3

Degree of the polynomial kernel function ('poly').
Must be non-negative. Ignored by all other kernels.
3
gamma gamma: {'scale', 'auto'} or float, default='scale'

Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.

- if ``gamma='scale'`` (default) is passed then it uses
1 / (n_features * X.var()) as value of gamma,
- if 'auto', uses 1 / n_features
- if float, must be non-negative.

.. versionchanged:: 0.22
The default value of ``gamma`` changed from 'auto' to 'scale'.
'scale'
coef0 coef0: float, default=0.0

Independent term in kernel function.
It is only significant in 'poly' and 'sigmoid'.
0.0
shrinking shrinking: bool, default=True

Whether to use the shrinking heuristic.
See the :ref:`User Guide `.
True
probability probability: bool, default=False

Whether to enable probability estimates. This must be enabled prior
to calling `fit`, will slow down that method as it internally uses
5-fold cross-validation, and `predict_proba` may be inconsistent with
`predict`. Read more in the :ref:`User Guide `.
False
tol tol: float, default=1e-3

Tolerance for stopping criterion.
0.001
cache_size cache_size: float, default=200

Specify the size of the kernel cache (in MB).
200
class_weight class_weight: dict or 'balanced', default=None

Set the parameter C of class i to class_weight[i]*C for
SVC. If not given, all classes are supposed to have
weight one.
The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``.
None
verbose verbose: bool, default=False

Enable verbose output. Note that this setting takes advantage of a
per-process runtime setting in libsvm that, if enabled, may not work
properly in a multithreaded context.
False
max_iter max_iter: int, default=-1

Hard limit on iterations within solver, or -1 for no limit.
-1
decision_function_shape decision_function_shape: {'ovo', 'ovr'}, default='ovr'

Whether to return a one-vs-rest ('ovr') decision function of shape
(n_samples, n_classes) as all other classifiers, or the original
one-vs-one ('ovo') decision function of libsvm which has shape
(n_samples, n_classes * (n_classes - 1) / 2). However, note that
internally, one-vs-one ('ovo') is always used as a multi-class strategy
to train models; an ovr matrix is only constructed from the ovo matrix.
The parameter is ignored for binary classification.

.. versionchanged:: 0.19
decision_function_shape is 'ovr' by default.

.. versionadded:: 0.17
*decision_function_shape='ovr'* is recommended.

.. versionchanged:: 0.17
Deprecated *decision_function_shape='ovo' and None*.
'ovr'
break_ties break_ties: bool, default=False

If true, ``decision_function_shape='ovr'``, and number of classes > 2,
:term:`predict` will break ties according to the confidence values of
:term:`decision_function`; otherwise the first class among the tied
classes is returned. Please note that breaking ties comes at a
relatively high computational cost compared to a simple predict. See
:ref:`sphx_glr_auto_examples_svm_plot_svm_tie_breaking.py` for an
example of its usage with ``decision_function_shape='ovr'``.

.. versionadded:: 0.22
False
random_state random_state: int, RandomState instance or None, default=None

Controls the pseudo random number generation for shuffling the data for
probability estimates. Ignored when `probability` is False.
Pass an int for reproducible output across multiple function calls.
See :term:`Glossary `.
0
In [281]:
accuracy_train_svm1 = model_svm1.score(X_train_svm, y_train)
print('Accuracy train = {:.2f}'.format(accuracy_train_svm1))
accuracy_test_svm1 = model_svm1.score(X_test_svm, y_test)
print('Accuracy test = {:.2f}'.format(accuracy_test_svm1))
print('Diferencia = {:.4f}%'.format(np.abs(accuracy_train_svm1-accuracy_test_svm1)*100))
Accuracy train = 0.79
Accuracy test = 0.81
Diferencia = 1.9097%

El modelo tiene un pequeño sobreajuste.

Su Accuracy de 81% apunta a que sabe generalizar bien entre los positivos reales y negativos reales, sin embargo, puede aún mejorar.

In [282]:
pred_svm1 = model_svm1.predict(X_test_svm)
rep_svm1 = classification_report(y_test, pred_svm1)
print(rep_svm1)

from imblearn.metrics import geometric_mean_score
print('G-mean =', geometric_mean_score(y_test, pred_svm1))

disp = ConfusionMatrixDisplay.from_predictions(y_test, pred_svm1, cmap = plt.cm.Blues)

# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
              precision    recall  f1-score   support

           0       0.64      0.51      0.57        35
           1       0.85      0.91      0.88       109

    accuracy                           0.81       144
   macro avg       0.75      0.71      0.73       144
weighted avg       0.80      0.81      0.81       144

G-mean = 0.6834497338233234
No description has been provided for this image

Matriz de confusión.

  • 18 + 99 = 117 aciertos.

  • Porcentaje de aciertos → 117 / 144 = 0.81 → 81%.

  • 10 + 17 = 27 errores.

144 datos en total.

Reporte de clasificación.

  • 35 datos de prueba de Factores influyendo en el estado de salud.

    • 51% de positivos encontrados.
    • 64% fueron clasificados correctamente.
  • 109 datos de prueba de Trastorno mental y del comportamiento.

    • 91% de positivos encontrados.
    • 85% fueron clasificados correctamente.

F1-Score: promedio ponderado de las clases.

  • Factores influyendo en el estado de salud: 57%.
  • Trastorno mental y del comportamiento: 88%.

Porcentaje de proporción a encontrar todas las clasificaciones de este estudio (G-mean): 68%.

SVM - SMOTE.

Procederemos a realizar el SVM con la técnica de balanceo de SMOTE.

In [283]:
from imblearn.over_sampling import SMOTE

# Aplicamos SMOTE sobre los datos originales.
smote = SMOTE(random_state=0)
X_smote_svm, y_smote_svm = smote.fit_resample(X, y)

print('Tamaño de X antes de SMOTE:', X.shape)
print('Tamaño de X después de SMOTE:', X_smote_svm.shape)
print('Balance de clases con SMOTE:')
print(y_smote_svm.value_counts())
print('Nuestras clases están balanceadas.')
Tamaño de X antes de SMOTE: (720, 3)
Tamaño de X después de SMOTE: (1094, 3)
Balance de clases con SMOTE:
descripcion_grupo_enfermedad_num
1    547
0    547
Name: count, dtype: int64
Nuestras clases están balanceadas.

Separamos los datos en entrenamiento y prueba:

In [284]:
# Dividir los datos balanceados en train y test.
X_train, X_test, y_train, y_test = train_test_split(X_smote_svm, y_smote_svm, test_size=0.2, random_state=0, stratify=y_smote_svm)
print(y_train.value_counts())
X_train.shape, X_test.shape
descripcion_grupo_enfermedad_num
1    438
0    437
Name: count, dtype: int64
Out[284]:
((875, 3), (219, 3))

Seguimos con SVM, aunque utilicemos la técnica de balanceo de SMOTE, todavía se aplica el escalador StandardScaler.

In [285]:
# Escalamos las características.
scaler_svm2 = StandardScaler()
X_train_svm2 = scaler_svm2.fit_transform(X_train)
X_test_svm2 = scaler_svm2.transform(X_test)

Definimos el modelo de SVM balanceado con los siguientes hiperparámetros principales:

  • C: 1 → penalización proporcional al valor absoluto de los coeficientes del modelo.

  • kernel: rbf → definir la mejor regla de separación.

  • degree: 3 → grado del kernel.

  • gamma: scale → coeficiente del kernel.

  • class_weight: none → el balanceo viene de la librería SMOTE.

In [286]:
# Definir el modelo balanceado.
model_svm2 = SVC(kernel = 'rbf', random_state = 0)
# Entrenemos el modelo SVM.
model_svm2.fit(X_train_svm2, y_train)
Out[286]:
SVC(random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
C C: float, default=1.0

Regularization parameter. The strength of the regularization is
inversely proportional to C. Must be strictly positive. The penalty
is a squared l2 penalty. For an intuitive visualization of the effects
of scaling the regularization parameter C, see
:ref:`sphx_glr_auto_examples_svm_plot_svm_scale_c.py`.
1.0
kernel kernel: {'linear', 'poly', 'rbf', 'sigmoid', 'precomputed'} or callable, default='rbf'

Specifies the kernel type to be used in the algorithm. If
none is given, 'rbf' will be used. If a callable is given it is used to
pre-compute the kernel matrix from data matrices; that matrix should be
an array of shape ``(n_samples, n_samples)``. For an intuitive
visualization of different kernel types see
:ref:`sphx_glr_auto_examples_svm_plot_svm_kernels.py`.
'rbf'
degree degree: int, default=3

Degree of the polynomial kernel function ('poly').
Must be non-negative. Ignored by all other kernels.
3
gamma gamma: {'scale', 'auto'} or float, default='scale'

Kernel coefficient for 'rbf', 'poly' and 'sigmoid'.

- if ``gamma='scale'`` (default) is passed then it uses
1 / (n_features * X.var()) as value of gamma,
- if 'auto', uses 1 / n_features
- if float, must be non-negative.

.. versionchanged:: 0.22
The default value of ``gamma`` changed from 'auto' to 'scale'.
'scale'
coef0 coef0: float, default=0.0

Independent term in kernel function.
It is only significant in 'poly' and 'sigmoid'.
0.0
shrinking shrinking: bool, default=True

Whether to use the shrinking heuristic.
See the :ref:`User Guide `.
True
probability probability: bool, default=False

Whether to enable probability estimates. This must be enabled prior
to calling `fit`, will slow down that method as it internally uses
5-fold cross-validation, and `predict_proba` may be inconsistent with
`predict`. Read more in the :ref:`User Guide `.
False
tol tol: float, default=1e-3

Tolerance for stopping criterion.
0.001
cache_size cache_size: float, default=200

Specify the size of the kernel cache (in MB).
200
class_weight class_weight: dict or 'balanced', default=None

Set the parameter C of class i to class_weight[i]*C for
SVC. If not given, all classes are supposed to have
weight one.
The "balanced" mode uses the values of y to automatically adjust
weights inversely proportional to class frequencies in the input data
as ``n_samples / (n_classes * np.bincount(y))``.
None
verbose verbose: bool, default=False

Enable verbose output. Note that this setting takes advantage of a
per-process runtime setting in libsvm that, if enabled, may not work
properly in a multithreaded context.
False
max_iter max_iter: int, default=-1

Hard limit on iterations within solver, or -1 for no limit.
-1
decision_function_shape decision_function_shape: {'ovo', 'ovr'}, default='ovr'

Whether to return a one-vs-rest ('ovr') decision function of shape
(n_samples, n_classes) as all other classifiers, or the original
one-vs-one ('ovo') decision function of libsvm which has shape
(n_samples, n_classes * (n_classes - 1) / 2). However, note that
internally, one-vs-one ('ovo') is always used as a multi-class strategy
to train models; an ovr matrix is only constructed from the ovo matrix.
The parameter is ignored for binary classification.

.. versionchanged:: 0.19
decision_function_shape is 'ovr' by default.

.. versionadded:: 0.17
*decision_function_shape='ovr'* is recommended.

.. versionchanged:: 0.17
Deprecated *decision_function_shape='ovo' and None*.
'ovr'
break_ties break_ties: bool, default=False

If true, ``decision_function_shape='ovr'``, and number of classes > 2,
:term:`predict` will break ties according to the confidence values of
:term:`decision_function`; otherwise the first class among the tied
classes is returned. Please note that breaking ties comes at a
relatively high computational cost compared to a simple predict. See
:ref:`sphx_glr_auto_examples_svm_plot_svm_tie_breaking.py` for an
example of its usage with ``decision_function_shape='ovr'``.

.. versionadded:: 0.22
False
random_state random_state: int, RandomState instance or None, default=None

Controls the pseudo random number generation for shuffling the data for
probability estimates. Ignored when `probability` is False.
Pass an int for reproducible output across multiple function calls.
See :term:`Glossary `.
0
In [287]:
# Evaluación del modelo SVM balanceado.
accuracy_train_svm2 = model_svm2.score(X_train_svm2, y_train)
print('Accuracy train = {:.2f}'.format(accuracy_train_svm2))
accuracy_test_svm2 = model_svm2.score(X_test_svm2, y_test)
print('Accuracy test = {:.2f}'.format(accuracy_test_svm2))
print('Diferencia = {:.4f}%'.format(np.abs(accuracy_train_svm2-accuracy_test_svm2)*100))
Accuracy train = 0.75
Accuracy test = 0.75
Diferencia = 0.8282%

El modelo tiene un muy pequeño sobreajuste.

Su Accuracy de 75% apunta a que sabe generalizar entre los positivos reales y negativos reales, sin embargo, puede aún mejorar en gran escala ya que en otros modelos llegó hasta el 90%.

In [288]:
pred_svm2 = model_svm2.predict(X_test_svm2)
rep_svm2 = classification_report(y_test, pred_svm2)
print(rep_svm2)

print('G-mean =', geometric_mean_score(y_test, pred_svm2))

disp = ConfusionMatrixDisplay.from_predictions(y_test, pred_svm2, cmap = plt.cm.Blues)

# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
              precision    recall  f1-score   support

           0       0.73      0.82      0.77       110
           1       0.79      0.69      0.74       109

    accuracy                           0.75       219
   macro avg       0.76      0.75      0.75       219
weighted avg       0.76      0.75      0.75       219

G-mean = 0.7503126954482326
No description has been provided for this image

Matriz de confusión.

  • 90 + 75 = 165 aciertos.

  • Porcentaje de aciertos → 165 / 219 = 0.75 → 75%.

  • 34 + 20 = 54 errores.

219 datos en total.

Reporte de clasificación.

  • 110 datos de prueba de Factores influyendo en el estado de salud.

    • 82% de positivos encontrados.
    • 73% fueron clasificados correctamente.
  • 109 datos de prueba de Trastorno mental y del comportamiento.

    • 69% de positivos encontrados.
    • 79% fueron clasificados correctamente.

F1-Score: promedio ponderado de las clases.

  • Factores influyendo en el estado de salud: 77%.
  • Trastorno mental y del comportamiento: 74%.

Porcentaje de proporción a encontrar todas las clasificaciones de este estudio (G-mean): 75%.

Métricas.

Las métricas de desempeño evaluadas para estos modelos fueron:

  • Accuracy.

  • Matriz de confusión.

  • G-mean.

  • Precision.

  • Recall (Sensibilidad).

  • F1-Score.

Red Neuronal.¶

Construcción de Red Neuronal según Persson (2020).

In [289]:
# Activación y perdida.
def relu(z):
    return np.maximum(0, z)

def relu_deriv(z):
    return (z > 0).astype(float)

def sigmoid(z):
    return 1/(1+np.exp(-np.clip(z, -500, 500)))

def binary_cross_entropy(y_true, y_pred):
    eps = 1e-8
    return -np.mean(y_true*np.log(y_pred+eps)+(1-y_true)*np.log(1 - y_pred+eps))


# Generación de la Red Neuronal.
class RedNeuronal:
    """Red neuronal binaria de una capa oculta construida desde cero con NumPy."""

    def __init__(self, n_entrada = 3, n_oculta = 6, lr = 0.01, epochs = 1000, random_state = 0):
        self.n_entrada = n_entrada
        self.n_oculta = n_oculta
        self.lr = lr
        self.epochs = epochs
        self.random_state = random_state
        self.losses = []

    def _init_params(self):
        rng = np.random.default_rng(self.random_state)
        # He initialization (recomendada para ReLU).
        self.W1 = rng.standard_normal((self.n_entrada, self.n_oculta))*np.sqrt(2/self.n_entrada)
        self.b1 = np.zeros((1, self.n_oculta))
        self.W2 = rng.standard_normal((self.n_oculta, 1))*np.sqrt(2/self.n_oculta)
        self.b2 = np.zeros((1, 1))

    def _forward(self, X):
        self.Z1 = X @ self.W1+self.b1 # (n, n_oculta).
        self.A1 = relu(self.Z1) # (n, n_oculta).
        self.Z2 = self.A1 @ self.W2+self.b2 # (n, 1).
        self.A2 = sigmoid(self.Z2) # (n, 1).
        return self.A2

    def _backward(self, X, y):
        n = X.shape[0]
        # Gradiente capa de salida.
        dZ2 = self.A2-y # (n, 1).
        dW2 = (self.A1.T @ dZ2)/n
        db2 = np.mean(dZ2, axis = 0, keepdims = True)
        # Gradiente capa oculta.
        dA1 = dZ2 @ self.W2.T # (n, n_oculta).
        dZ1 = dA1*relu_deriv(self.Z1)
        dW1 = (X.T @ dZ1)/n
        db1 = np.mean(dZ1, axis = 0, keepdims = True)
        # Actualizar pesos.
        self.W2 -= self.lr*dW2
        self.b2 -= self.lr*db2
        self.W1 -= self.lr*dW1
        self.b1 -= self.lr*db1

    def fit(self, X, y):
        self._init_params()
        y = y.reshape(-1, 1).astype(float)
        for epoch in range(self.epochs):
            y_pred = self._forward(X)
            loss = binary_cross_entropy(y, y_pred)
            self.losses.append(loss)
            self._backward(X, y)
            if (epoch+1)%200 == 0:
                print(f'Epoch {epoch+1}/{self.epochs}  |  Loss: {loss:.4f}')
        return self

    def predict_proba(self, X):
        return self._forward(X).flatten()

    def predict(self, X, threshold=0.5):
        return (self.predict_proba(X) >= threshold).astype(int)

    def score(self, X, y):
        return accuracy_score(y, self.predict(X))

    def plot_loss(self, title='Curva de pérdida'):
        plt.figure(figsize=(7, 3))
        plt.plot(self.losses)
        plt.xlabel('Época')
        plt.ylabel('Binary Cross-Entropy')
        plt.title(title)
        plt.tight_layout()
        plt.show()

print('Clase RedNeuronal definida correctamente.')
Clase RedNeuronal definida correctamente.

Al momento de generar la clase de la Red Neuronal, se utilizarán 6 neuronas, esto debido a que se sigue la regla de 2 * entradas, resultando en:

2 * 3 = 6 → 6 neuronas.

Red Neuronal - by Default.

Procederemos a realizar la Red Neuronal sin técnica de balanceo.

Definimos variables:

In [290]:
# Definimos entrada y salida.
X = df2[['edad', 'sexo_num', 'institucion_unidad_medica_num']]
y = df2['descripcion_grupo_enfermedad_num']

Separamos los datos en entrenamiento y prueba:

In [291]:
# Dividimos en train y test.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0, stratify=y)
print(y_train.value_counts())
X_train.shape, X_test.shape
descripcion_grupo_enfermedad_num
1    438
0    138
Name: count, dtype: int64
Out[291]:
((576, 3), (144, 3))

Nuevamente, se utilizará un escalador de características, siendo el mismo de StandardScaler. Esto sucede debido a que la red neuronal repite el patrón de sensibilidad de SVM, solo que en este caso sucede (a diferencia de margen) por el gradiente descendente.

In [292]:
# Escalamos las características (necesario para la red neuronal).
scaler_nn1 = StandardScaler()
X_train_nn1 = scaler_nn1.fit_transform(X_train)
X_test_nn1 = scaler_nn1.transform(X_test)

Los hiperparámetros principales de la red neuronal son los siguientes:

  • n_entrada: 3 → número de variables independientes (edad, sexo_num, institucion_unidad_medica_num).

  • n_oculta: 6 → neuronas en la capa oculta, calculadas con la regla 2 × n_entrada = 2 × 3 = 6.

  • lr (learning rate): 0.01 → tasa de aprendizaje conservadora que permite convergencia estable sin saltar mínimos.

  • epochs: 1000 → número de iteraciones de entrenamiento suficientes para observar convergencia en la curva de pérdida.

  • Función de activación capa oculta: ReLU → eficiente y estable, evita el problema del gradiente desvaneciente.

  • Función de activación capa de salida: Sigmoid → convierte la salida a probabilidad entre 0 y 1.

In [293]:
# Definir el modelo by Default.
model_nn1 = RedNeuronal(n_entrada = 3, n_oculta = 6, lr = 0.01, epochs = 1000, random_state = 0)
# Entrenamos el modelo.
model_nn1.fit(X_train_nn1, y_train.values)
Epoch 200/1000  |  Loss: 0.4644
Epoch 400/1000  |  Loss: 0.4563
Epoch 600/1000  |  Loss: 0.4531
Epoch 800/1000  |  Loss: 0.4518
Epoch 1000/1000  |  Loss: 0.4511
Out[293]:
<__main__.RedNeuronal at 0x31f27d6a0>

Curva de pérdida:

In [294]:
# Curva de pérdida.
model_nn1.plot_loss(title='Curva de pérdida - Red Neuronal Desbalanceada')
No description has been provided for this image

La curva indica que el modelo disminuye a lo largo de las épocas del entrenamiento.

In [295]:
# Evaluación del modelo.
accuracy_train_nn1 = model_nn1.score(X_train_nn1, y_train.values)
print('Accuracy train = {:.2f}'.format(accuracy_train_nn1))
accuracy_test_nn1 = model_nn1.score(X_test_nn1, y_test.values)
print('Accuracy test = {:.2f}'.format(accuracy_test_nn1))
print('Diferencia = {:.4f}%'.format(np.abs(accuracy_train_nn1-accuracy_test_nn1)*100))
Accuracy train = 0.76
Accuracy test = 0.76
Diferencia = 0.3472%

El modelo tiene un muy pequeño sobreajuste.

Su Accuracy de 76% apunta a que sabe generalizar entre los positivos reales y negativos reales, sin embargo, puede aún mejorar en gran escala ya que en otros modelos llegó hasta el 90%.

In [296]:
pred_nn1 = model_nn1.predict(X_test_nn1)
rep_nn1  = classification_report(y_test, pred_nn1)
print(rep_nn1)

print('G-mean =', geometric_mean_score(y_test, pred_nn1))

disp = ConfusionMatrixDisplay.from_predictions(y_test, pred_nn1, cmap = plt.cm.Blues)

# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
              precision    recall  f1-score   support

           0       0.00      0.00      0.00        35
           1       0.76      1.00      0.86       109

    accuracy                           0.76       144
   macro avg       0.38      0.50      0.43       144
weighted avg       0.57      0.76      0.65       144

G-mean = 0.0
/opt/anaconda3/lib/python3.13/site-packages/sklearn/metrics/_classification.py:1833: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
  _warn_prf(average, modifier, f"{metric.capitalize()} is", result.shape[0])
/opt/anaconda3/lib/python3.13/site-packages/sklearn/metrics/_classification.py:1833: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
  _warn_prf(average, modifier, f"{metric.capitalize()} is", result.shape[0])
/opt/anaconda3/lib/python3.13/site-packages/sklearn/metrics/_classification.py:1833: UndefinedMetricWarning: Precision is ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
  _warn_prf(average, modifier, f"{metric.capitalize()} is", result.shape[0])
No description has been provided for this image

Matriz de confusión.

  • 0 + 109 = 109 aciertos.

  • Porcentaje de aciertos → 109 / 144 = 0.76 → 76%.

  • 35 + 0 = 35 errores.

144 datos en total.

Reporte de clasificación.

  • 35 datos de prueba de Factores influyendo en el estado de salud.

    • 0% de positivos encontrados.
    • 0% fueron clasificados correctamente.
  • 109 datos de prueba de Trastorno mental y del comportamiento.

    • 100% de positivos encontrados.
    • 76% fueron clasificados correctamente.

F1-Score: promedio ponderado de las clases.

  • Factores influyendo en el estado de salud: 0%.
  • Trastorno mental y del comportamiento: 86%.

Porcentaje de proporción a encontrar todas las clasificaciones de este estudio (G-mean): 0%.

Este G-mean de 0 es el resultado más revelador: la red neuronal sin balanceo predice únicamente la clase 1, ignorando por completo la clase minoritaria. Esto reafirma que la red neuronal es extremadamente sensible al desbalance de clases.

Red Neuronal - SMOTE.

Procederemos a realizar la Red Neuronal con la técnica de balanceo de SMOTE.

In [297]:
from imblearn.over_sampling import SMOTE

# Aplicamos SMOTE sobre los datos originales.
smote = SMOTE(random_state=0)
X_smote_nn, y_smote_nn = smote.fit_resample(X, y)

print('Tamaño de X antes de SMOTE:', X.shape)
print('Tamaño de X después de SMOTE:', X_smote_nn.shape)
print('Balance de clases con SMOTE:')
print(y_smote_nn.value_counts())
print('Nuestras clases están balanceadas.')
Tamaño de X antes de SMOTE: (720, 3)
Tamaño de X después de SMOTE: (1094, 3)
Balance de clases con SMOTE:
descripcion_grupo_enfermedad_num
1    547
0    547
Name: count, dtype: int64
Nuestras clases están balanceadas.

Separamos los datos en entrenamiento y prueba:

In [298]:
# Dividir los datos balanceados en train y test.
X_train, X_test, y_train, y_test = train_test_split(X_smote_nn, y_smote_nn, test_size = 0.2, random_state = 0, stratify = y_smote_nn)
print(y_train.value_counts())
X_train.shape, X_test.shape
descripcion_grupo_enfermedad_num
1    438
0    437
Name: count, dtype: int64
Out[298]:
((875, 3), (219, 3))

Repetimos el escalador StandardScaler:

In [299]:
# Escalamos las características.
scaler_nn2 = StandardScaler()
X_train_nn2 = scaler_nn2.fit_transform(X_train)
X_test_nn2 = scaler_nn2.transform(X_test)

Los hiperparámetros principales de la red neuronal son los siguientes:

  • n_entrada: 3 → número de variables independientes (edad, sexo_num, institucion_unidad_medica_num).

  • n_oculta: 6 → neuronas en la capa oculta, calculadas con la regla 2 × n_entrada = 2 × 3 = 6.

  • lr (learning rate): 0.01 → tasa de aprendizaje conservadora que permite convergencia estable sin saltar mínimos.

  • epochs: 1000 → número de iteraciones de entrenamiento suficientes para observar convergencia en la curva de pérdida.

  • Función de activación capa oculta: ReLU → eficiente y estable, evita el problema del gradiente desvaneciente.

  • Función de activación capa de salida: Sigmoid → convierte la salida a probabilidad entre 0 y 1.

Son los mismos a la Red Neuronal by Default.

In [300]:
# Definir el modelo balanceado.
model_nn2 = RedNeuronal(n_entrada = 3, n_oculta = 6, lr = 0.01, epochs = 1000, random_state = 0)
# Entrenemos el modelo.
model_nn2.fit(X_train_nn2, y_train.values)
Epoch 200/1000  |  Loss: 0.5910
Epoch 400/1000  |  Loss: 0.5781
Epoch 600/1000  |  Loss: 0.5706
Epoch 800/1000  |  Loss: 0.5658
Epoch 1000/1000  |  Loss: 0.5624
Out[300]:
<__main__.RedNeuronal at 0x327c92850>

Curva de pérdida:

In [301]:
# Curva de pérdida.
model_nn2.plot_loss(title='Curva de pérdida - Red Neuronal Balanceada (SMOTE)')
No description has been provided for this image

Nuevamente, la curva de pérdida va disminuyendo a lo largo del entrenamiento.

In [302]:
# Evaluación del modelo.
accuracy_train_nn2 = model_nn2.score(X_train_nn2, y_train.values)
print('Accuracy train = {:.2f}'.format(accuracy_train_nn2))
accuracy_test_nn2 = model_nn2.score(X_test_nn2, y_test.values)
print('Accuracy test = {:.2f}'.format(accuracy_test_nn2))
print('Diferencia = {:.4f}%'.format(np.abs(accuracy_train_nn2-accuracy_test_nn2)*100))
Accuracy train = 0.72
Accuracy test = 0.74
Diferencia = 2.6578%

El modelo tiene un pequeño sobreajuste.

Su Accuracy de 74% apunta a que sabe generalizar entre los positivos reales y negativos reales, sin embargo, puede aún mejorar en gran escala ya que en otros modelos llegó hasta el 90%.

In [303]:
pred_nn2 = model_nn2.predict(X_test_nn2)
rep_nn2  = classification_report(y_test, pred_nn2)
print(rep_nn2)

print('G-mean =', geometric_mean_score(y_test, pred_nn2))

disp = ConfusionMatrixDisplay.from_predictions(y_test, pred_nn2, cmap = plt.cm.Blues)

# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
              precision    recall  f1-score   support

           0       0.72      0.80      0.76       110
           1       0.77      0.69      0.73       109

    accuracy                           0.74       219
   macro avg       0.75      0.74      0.74       219
weighted avg       0.75      0.74      0.74       219

G-mean = 0.7419290502442469
No description has been provided for this image

Matriz de confusión.

  • 88 + 75 = 163 aciertos.

  • Porcentaje de aciertos → 163 / 219 = 0.74 → 74%.

  • 34 + 22 = 56 errores.

219 datos en total.

Reporte de clasificación.

  • 110 datos de prueba de Factores influyendo en el estado de salud.

    • 80% de positivos encontrados.
    • 72% fueron clasificados correctamente.
  • 109 datos de prueba de Trastorno mental y del comportamiento.

    • 69% de positivos encontrados.
    • 77% fueron clasificados correctamente.

F1-Score: promedio ponderado de las clases.

  • Factores influyendo en el estado de salud: 76%.
  • Trastorno mental y del comportamiento: 73%.

Porcentaje de proporción a encontrar todas las clasificaciones de este estudio (G-mean): 74%.

Este G-mean mejora considerablemente a comparación del obtenido con el modelo desbalanceado (0%), lo que confirma que la técnica de SMOTE es indispensable para la Red Neuronal.

Métricas.

Las métricas de desempeño evaluadas para estos modelos fueron:

  • Accuracy.

  • Matriz de confusión.

  • G-mean.

  • Precision.

  • Recall (Sensibilidad).

  • F1-Score.

  • Curva de pérdida.

7.1 Resultados.¶

Para evaluar todos los modelos desarrollados en este proyecto, se presenta la siguiente tabla comparativa que integra los resultados de:

  • Regresión Logística.

  • Linear Discriminant Analysis (LDA).

  • Árbol de Decisión.

  • Random Forest Classifier.

  • Boosting.

  • Support Vector Machine (SVM).

  • Red Neuronal.

Modelo Escenario Accuracy G-mean F1 Clase 0 F1 Clase 1
Regresión Logística Desbalanceado 81% 59% 50% 89%
Regresión Logística SMOTE 74% 74% 76% 73%
Árbol de Decisión Desbalanceado 84% - - -
Árbol de Decisión SMOTE 81% - - -
Árbol Podado Desbalanceado 86% - - -
Árbol Podado SMOTE 79% - - -
Random Forest Desbalanceado 86% 75% 68% 91%
Random Forest SMOTE 89% 89% 90% 90%
SVM Desbalanceado 81% 68% 57% 88%
SVM SMOTE 75% 75% 77% 74%
Red Neuronal Desbalanceado 76% 0% 0% 86%
Red Neuronal SMOTE 74% 74% 76% 73%

El mejor modelo de todos es el: Random Forest con SMOTE, es decir, un bosque balanceado.

Esto se debe a que combina el poder de múltiples árboles en paralelo con el balanceo de clases de SMOTE, logrando el mayor G-mean (89%) y un F1-score equilibrado entre ambas clases (90% para cada una).

Para justificar de forma cualitativa, lo que permite al Random Forest ser tan bueno es que sus submuestras no tienen correlación entre sí, es decir, que el mismo no generaliza y sabe muy bien a donde dirigir su resultado según las características.

8.1 Discusiones.¶

Para este proyecto del segundo parcial, no se presentaron dificultades al momento de estar desarrollando el código, incluyendo el aspecto que cada uno de estos modelos ya habían sido construidos anteriormente y por ende, se ahorró mucho tiempo y esfuerzo al tener esta organización de entregas que en este caso, fue de mucha ayuda ya que se mantuvo el uso de la misma base de datos.

9.1 Conclusiones.¶

Para concluir, la construcción e implementación de todos estos modelos en la base de datos elegida, brindó un enfoque sumamente profundo al mismo conjunto de datos para ver desde otra lupa el comportamiento de los datos que están disponibles a analizar, en este caso siendo la adaptación de modelos de clasificación, que brindan resultados sumamente diferentes a aquellos de una regresión - estos pueden llegar a ser menos interpretables y no nos ayudan a segmentar a los individuos según sus características importantes, algo que pudimos resolver y visualizar en clasificar el dataset.

Además, aunque ya se conocían diversos modelos de clasificación, aquel con el de la Red Neuronal construida desde cero fue sumamente interesante ya que es el primer paso a LLM, un alcance mucho más allá que ya entra dentro del análisis profundo y que sin duda, el implementar este con enfoques más amplios del conjunto de datos, brindaría mucha información interesante y que vale la pena analizar.

Vale la pena también conectar el cómo el modelo brindaría apoyo para la problemática establecida, donde el tener estudios y desarrollos que justifiquen los dos padecimientos de salud mental en el municipio, puede explicar el estado de las peronas que más brindan económica y socialmente al municipio - pudiendo profundizar en problemáticas que se tienen en dicha sociedad que como se ha mencionado, se puede considerar "perfecta", no es solo un estudio social, sino que también puede ser uno valioso para el gobierno en saber por dónde (por el lado de la salud mental) deben alinearse para brindar una mejor calidad de vida, algo que todos los municipios quieren para poder establecerse como los mejores seún sus habitantes.

10.1 Aprendizajes.¶

Los aprendizajes de este proyecto se pueden resumir en cómo aplicar y contrastar diversos modelos de clasificación para llegar a la conclusión de cuál es el mejor para el conjunto de datos que estemos utilizando - tomando en cuenta que primero hay que delimitar muy bien el objetivo del problema planteado para saber si se puede resolver el mismo mediante una clasificación, ya que pueden haber otras maneras de resolver a la pregunta detonante (como lo es en la regresión, esperando resultados cuantitativos), por lo que también, se obtiene el aprendizaje de aprender a hacer las preguntas correctas para llegar a descubrir el alcance que se desea abordar, en este caso, siendo el clasificar en qué tipo de padecimiento entran los individuos adultos en el municipio de San Pedro Garza García.

11.1 Posibles líneas futuras.¶

Como posibles líneas futuras, es que se puede resolver el mismo planteamiento establecido pero ahora con una cantidad de datos más grande (probablemente igual por municipio o por edades), donde ahora se tomen más registros como de años consecutivos para ir conociendo si dicha salud mental ha ido en aumento (buen caso) por los tratamientos y apoyos, o la misma ha ido disminuyendo (mal caso) por la falta de estos apoyos - esto siendo tratado con una Red Neuronal por la cantidad de datos y probablemente de más variables que se tengan que tomar en cuenta por este estudio que termina siendo más grande que el desarrollado en este proyecto.

12.1 Referencias.¶

3.1. Cross-validation: evaluating estimator performance. (s. f.). Scikit-learn. https://scikit-learn.org/stable/modules/cross_validation.html

Aladdin Persson. (2020). Neural Network from Scratch - Machine Learning Python [Vídeo]. YouTube. https://www.youtube.com/watch?v=NJvojeoTnNM

Aylin Tokuç, A. (2025). Why Feature Scaling in SVM? Baeldung. https://www.baeldung.com/cs/svm-feature-scaling#:~:text=SVM%20y%20escalado%20de%20caracter%C3%ADsticas,-SVM%20es%20un

Draelos, V. A. P. B. R., MD PhD. (2025). Measuring Performance: AUC (AUROC). Glass Box Medicine. https://glassboxmedicine.com/2019/02/23/measuring-performance-auc-auroc/

Egbuchulem, K. (2025). THE ODDS RATIO: a MEASURE OF STRENGTH IN CLINICAL RESEARCH AND AN ANTITHESIS TO ODDS IN GAMBLING. https://pmc.ncbi.nlm.nih.gov/articles/PMC12337969/#:~:text=%C2%BFQu%C3%A9%20significa%20el%20valor%20de%20la%20raz%C3%B3n%20de%20probabilidades%3F&text=OR%20de%201%3A%20No%20hay,la%20exposici%C3%B3n%20y%20el%20resultado.

GeeksforGeeks. (2025). Detecting Multicollinearity with VIF Python. GeeksforGeeks. https://www.geeksforgeeks.org/python/detecting-multicollinearity-with-vif-python/

Salud mental. (2026). OPS/OMS | Organización Panamericana de la Salud. https://www.paho.org/es/temas/salud-mental

Kavlakoglu, E. (s. f.). How to implement linear discriminant analysis in Python. IBM Developer. https://developer.ibm.com/tutorials/awb-implementing-linear-discriminant-analysis-python/

Mucci, T. (2025). What is Data Leakage in Machine Learning? Ibm.com. https://www.ibm.com/think/topics/data-leakage-machine-learning

Pardo, D. - Corresponsal de BBC News Mundo en México. (2025, 26 septiembre). «Somos un rancho, pero de primer mundo»: cómo es San Pedro Garza García, uno de los municipios más ricos de América Latina. Yahoo News. https://es-us.noticias.yahoo.com/somos-rancho-mundo-san-pedro-130147939.html?guccounter=1&guce_referrer=aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS8&guce_referrer_sig=AQAAAMmp2Bvqk5tPLjoZLTT6gG1nJpi7JuPC-S2SRk7bnb9FcZT43OxX7CFUYPWuQtbqKLrXcOkVJqszWl5x6TmjK1cyMBpiTlnnUiyESrYs7Sy00CyLDoKUkaZTu5bvX-dLaN9A7sqbnjMF3Kii-dMXw_3eT4rAl3O_6VOD4OLWU2FL

Torres, L. (2024). Curva ROC y AUC en Python. The Machine Learners. https://www.themachinelearners.com/curva-roc-vs-prec-recall/

13.1 Código de Honor de la Universidad de Monterrey.¶

Doy mi palabra que he realizado esta actividad con integridad académica.