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 2 de marzo del 2026.
#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 # Permite implementar la mayoría de modelos.
from sklearn.linear_model import LogisticRegression
import statsmodels.formula.api as smf
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.model_selection import cross_val_score
1.1 Introducción.¶
Para el presente estudio, se tomará como base el dataset utilizado en el proyecto del primer parcial de la materia de Inteligencia Artifical, el cuál para este caso se trata de registros de adultos en la edad económicamente activa (18-65 años) que residen en el municipio de San Pedro Garza García, ubicado en el estado de Nuevo León y que corresponde al área metropolitana de Monterrey.
A diferencia del proyecto del primer parcial donde se realizó tanto una Regresión Lineal Múltiple como un Random Forest Regressor, en esta ocasión se realizará una Regresión Logística, siendo este un modelo de clasificación donde podemos obtener únicamente 2 resultados bajo la predicción de la probabilidad de 0 a 1; para dicha regresión, se aplicarán sus técnicas y validaciones respectivas.
1.2 Objetivo.¶
El objetivo de este estudio es aplicar una Regresión Logística, donde para la misma (como se comentó anteriormente) se busca predecir la probabilidad de 0 a 1, esto con el propósito de obtener solo 2 respuestas probables, llamado clasificación; a diferencia de las pasadas regresiones tratadas en clase donde se pueden obtener n cantidad de predicciones según los pronósticos.
En este caso, se busca conocer si una persona recae en alguno de los siguientes grupos:
Trastornos mentales y del comportamiento.
Factores que influyen en el estado de salud y contacto con los servicios de salud.
Esto para saber si el/la individuo(a) padece una enfermedad como tal que necesita ser tratada profesionalmente (primer grupo) o si solo requiere de una terapia sin necesidar de caer en un tratamiento que necesite ser guiado bajo un especialista.
2.1 Descripción del conjunto de datos.¶
Como se mencionó anteriormente, el dataset utilizdo es el mismo que el del proyecto del primer parcial, estos provienen del portal de datos abiertos del Gobierno del Estado de Nuevo León dentro del periodo de octubre del 2024 a agosto del 2025.
El dataset seleccionado contiene datos de varios 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.
# 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)
| 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 |
2.2 Preparación y limpieza del conjunto de datos.¶
Justo como en el proyecto, se filtrarán los datos a que estos sean solamente del municipio de San Pedro Garza García y que dichos contengan a individuos dentro del rango de edad de 18 a 65 años.
df1 = df[df['municipio_unidad_medica'] == 'SAN PEDRO GARZA GARCIA']
print(df1.shape)
df1.head()
(1060, 14)
| 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 |
# 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)
| 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, sin embargo, se vuelve a recalcar en este punto que no existen datos vacíos.
# Verificar si hay datos vacíos.
df2.isna().sum().sort_values(ascending = False)
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.
Factores que influyen en el estado de salud y contacto con los servicios de salud con 173.
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% del dataset, 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.
df2['descripcion_grupo_enfermedad'].value_counts()
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
# Para el porcentaje.
df2['descripcion_grupo_enfermedad'].value_counts()/(df2['descripcion_grupo_enfermedad'].value_counts().sum())*100
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
Diferencia mencionada de forma visual:
# Gráfica del balance de clases.
df2['descripcion_grupo_enfermedad'].str[:43].value_counts().plot(kind = 'bar')
plt.show()
Nuevamente, se transforman las variables categóricas a numéricas para poderlas manipular.
df2.dtypes
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
# 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_47110/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_47110/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_47110/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_47110/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_47110/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'])
| 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 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 46621 | 26/08/2025 | SM_2025_33757 | 32 | 0 | 0 | Femenino | 71.8 | 167 | SAN PEDRO GARZA GARCIA | CENTRO DE SALUD CON SERVICIOS AMPLIADOS SAN PE... | XXI | FACTORES QUE INFLUYEN EN EL ESTADO DE SALUD Y ... | Z631 | PROBLEMAS EN LA RELACION CON LOS PADRES Y LOS ... | 0 | 1 | 0 | 47 | 15 |
| 18782 | 19/02/2025 | SM_2025_5918 | 21 | 0 | 0 | Femenino | 55.4 | 162 | SAN PEDRO GARZA GARCIA | CENTRO DE SALUD CON SERVICIOS AMPLIADOS SAN PE... | V | TRASTORNOS MENTALES Y DEL COMPORTAMIENTO | F412 | TRASTORNO MIXTO DE ANSIEDAD Y DEPRESION | 0 | 1 | 1 | 32 | 38 |
| 36865 | 13/06/2025 | SM_2025_24001 | 32 | 0 | 0 | Masculino | 75 | 175 | SAN PEDRO GARZA GARCIA | CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... | V | TRASTORNOS MENTALES Y DEL COMPORTAMIENTO | F140 | TRASTORNOS MENTALES Y DEL COMPORTAMIENTO DEBID... | 1 | 0 | 1 | 8 | 47 |
| 37238 | 11/06/2025 | SM_2025_24374 | 55 | 0 | 0 | Femenino | 82 | 154 | SAN PEDRO GARZA GARCIA | CENTRO DE SALUD CON SERVICIOS AMPLIADOS SAN PE... | XXI | FACTORES QUE INFLUYEN EN EL ESTADO DE SALUD Y ... | Z631 | PROBLEMAS EN LA RELACION CON LOS PADRES Y LOS ... | 0 | 1 | 0 | 47 | 15 |
| 36906 | 23/06/2025 | SM_2025_24042 | 28 | 0 | 0 | Masculino | 75 | 170 | SAN PEDRO GARZA GARCIA | CENTRO COMUNITARIO DE SALUD MENTAL Y ADICCIONE... | V | TRASTORNOS MENTALES Y DEL COMPORTAMIENTO | F122 | TRASTORNOS MENTALES Y DEL COMPORTAMIENTO DEBID... | 1 | 0 | 1 | 6 | 44 |
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.
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.
# 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
# Convertir altura a metros.
df2['altura_m'] = df2['altura'] / 100
# IMC.
df2['IMC'] = df2['peso'] / (df2['altura_m']**2)
3.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.
plt.figure(figsize = (10, 6))
sns.heatmap(df2.corr(numeric_only = True), annot = True)
plt.show()
# 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()
| 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: | Mon, 02 Mar 2026 | Prob (F-statistic): | 1.74e-122 |
| Time: | 13:09:38 | 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.
# Imprimimos los P value para ver cuál es más importante.
pvalue_percent = (modelFS.pvalues * 100).sort_values(ascending = True)
pvalue_percent
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
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.
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.
3.1 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
descripcion_enfermedad_num
# Definimos entrada y salida.
X = df2[['edad', 'sexo_num', 'institucion_unidad_medica_num', 'descripcion_enfermedad_num']]
y = df2['descripcion_grupo_enfermedad_num']
print(X.shape)
print(y.shape)
(720, 4) (720,)
4.1 Metodología.¶
Predicción.
Comenzamos a desarrollar el modelo con la separación de los datos de entrenamiento y prueba una vez que se delimitaron las variables independientes como la dependiente.
# 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).
((576, 4), (144, 4), (576,), (144,))
Para conocer cuáles son nuestros 0 y 1, lo checamos con value_counts y nos arroja lo siguiente:
0: Factores influyendo en el estado de salud.
1: Trastorno mental y del comportamiento.
y_train.value_counts()
# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
descripcion_grupo_enfermedad_num 1 438 0 138 Name: count, dtype: int64
y_test.value_counts()
# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
descripcion_grupo_enfermedad_num 1 109 0 35 Name: count, dtype: int64
Tanto para y_train y y_test (entrenamiento y preueba), se ve nuevamente del desbalance existente en entre las clases, sin embargo, el primer modelo se va a construir así sin modificaciones.
Logistic by Default.
# Definimos el modelo.
model1 = LogisticRegression(random_state = 0)
# Entrenamos el modelo.
model1.fit(X_train, y_train)
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 | 'l2' | |
| dual | False | |
| tol | 0.0001 | |
| C | 1.0 | |
| fit_intercept | True | |
| intercept_scaling | 1 | |
| class_weight | None | |
| random_state | 0 | |
| solver | 'lbfgs' | |
| max_iter | 100 | |
| multi_class | 'deprecated' | |
| verbose | 0 | |
| warm_start | False | |
| n_jobs | None | |
| l1_ratio | None |
Checamos cómo pronostica este modelo construido:
# 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
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.87 Accuracy test = 0.87 Diferencia = 0.0000%
Hasta este punto, obtenemos que el modelo no tiene sobreajuste, es decir, que sabe generalizar entre los positivos reales y negativos reales (1 y 0 respectivamente).
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, y_pred_test) # Siempre datos de validación.
cm
array([[29, 6],
[13, 96]])
Matriz de confusión:
29 + 96 = 125 aciertos.
Porcentaje de aciertos → 125 / 144 = 0.86 → 86%.
13 + 9 = 22 errores.
144 en total.
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.
scores = cross_val_score(model1, X, y, cv = 5)
print(scores.mean())
0.8722222222222221
Con dicha validación, obtenemos que el desempeño del modelo bajo la separación de los datos en entrenamiento y prueba es de 87%.
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.008490 0.991546 1 sexo_num 0.513594 1.671287 2 institucion_unidad_medica_num -1.923495 0.146096 3 descripcion_enfermedad_num 0.124889 1.133023
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 generán 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.
# Reporte de clasificación.
from sklearn.metrics import classification_report
from imblearn.metrics import geometric_mean_score
names = ['Factores influyendo en el estado de salud.', 'Trastorno mental y del comportamiento.']
print(classification_report(y_test, y_pred_test, target_names = names))
print('G-mean =', geometric_mean_score(y_test, y_pred_test))
precision recall f1-score support
Factores influyendo en el estado de salud. 0.69 0.83 0.75 35
Trastorno mental y del comportamiento. 0.94 0.88 0.91 109
accuracy 0.87 144
macro avg 0.82 0.85 0.83 144
weighted avg 0.88 0.87 0.87 144
G-mean = 0.8542546359031318
144 datos en prueba.
35 datos de prueba de Factores influyendo en el estado de salud.
- 83% de positivos encontrados.
- 69% fueron clasificados correctamente.
109 datos de prubea de Trastorno mental y del comportamiento.
- 88% de positivos encontrados.
- 94% fueron clasificados correctamente.
f1-score: promedio ponderado de las clases.
- Factores influyendo en el estado de salud: 75%.
- Trastorno mental y del comportamiento: 91%.
Porcentaje de proporción a encontrar todas las clasificaciones de este estudio: 85%.
Este porcentaje de proporción a encontrar todas las clasificaciones (G-mean) es el que queremos mejorar lo más que se pueda sin perjudicar a las clases de forma significativa con las técnicas de balanceo.
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.
from IPython.display import Image
Image(filename='/Users/estefaniadelarosa/Downloads/roc-curve-v2.png')
Figura 1.
How to interpret AUROC.
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/
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)
AUC: 0.9028014075725713
Basándonos en la imagen de Draelos, podemos delimitar de forma visual que el clasificador es bueno, este no llega al perfecto y es importante mencionar que esta declaración, aparte de tener una área bajo la curva muy buena de 0.90, no es 100% confiable ya que las clases presentan un desbalance entre si que se ha visto a lo largo del estudio hasta este punto, donde a partir de aquí es que emplearemos técnicas de balanceo para las clases para un análisis más equitativo y justo.
Logistic Balanced.
Para la primera técnica de balanceo de nuestras clases desbalanceadas, se usará el class_weight = 'balanced', proveniente de Scikit-learn y que se lanza como parámetro al modelo para que lo tome en consideración e implemente dicho balanceo que se está estableciendo.
model2 = LogisticRegression(class_weight = 'balanced', random_state = 0)
# Entrenamos el modelo.
model2.fit(X_train, y_train)
LogisticRegression(class_weight='balanced', 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 | 'l2' | |
| dual | False | |
| tol | 0.0001 | |
| C | 1.0 | |
| fit_intercept | True | |
| intercept_scaling | 1 | |
| class_weight | 'balanced' | |
| random_state | 0 | |
| solver | 'lbfgs' | |
| max_iter | 100 | |
| multi_class | 'deprecated' | |
| verbose | 0 | |
| warm_start | False | |
| n_jobs | None | |
| l1_ratio | None |
Checamos cómo pronostica este modelo construido:
# Validar si el modelo pronostica adecuadamente.
y_pred_test = model2.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
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.89 Accuracy test = 0.86 Diferencia = 2.6042%
Este modelo con la técnica de balanceo establecida, 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).
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, y_pred_test) # Siempre datos de validación.
cm
array([[32, 3],
[17, 92]])
Matriz de confusión:
32 + 92 = 124 aciertos.
Porcentaje de aciertos → 124 / 144 = 0.86 → 86%.
17 + 3 = 20 errores.
144 en total.
scores = cross_val_score(model2, X, y, cv = 5)
print(scores.mean())
0.8777777777777777
La validación cruzada nos indica que el desempeño del modelo bajo la separación de los datos en entrenamiento y prueba es de 87%.
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.004255 0.995754 1 sexo_num 0.659833 1.934469 2 institucion_unidad_medica_num -1.579543 0.206069 3 descripcion_enfermedad_num 0.165406 1.179871
Bajo la explicación brindada anteriormente de los Odd Ratio, podemos concluir lo siguiente:
sexo_num y descripcion_enfermedad_num generán 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.
Vale la pena mencionar que dichos valores son casi iguales a los resultados del modelo desbalanceado, por ende se clasifican de esa manera las variables.
# Reporte de clasificación.
from sklearn.metrics import classification_report
from imblearn.metrics import geometric_mean_score
names = ['Factores influyendo en el estado de salud.', 'Trastorno mental y del comportamiento.']
print(classification_report(y_test, y_pred_test, target_names = names))
print('G-mean =', geometric_mean_score(y_test, y_pred_test))
precision recall f1-score support
Factores influyendo en el estado de salud. 0.65 0.91 0.76 35
Trastorno mental y del comportamiento. 0.97 0.84 0.90 109
accuracy 0.86 144
macro avg 0.81 0.88 0.83 144
weighted avg 0.89 0.86 0.87 144
G-mean = 0.8784592731746159
144 datos en prueba.
35 datos de prueba de Factores influyendo en el estado de salud.
- 91% de positivos encontrados.
- 65% fueron clasificados correctamente.
109 datos de prubea de Trastorno mental y del comportamiento.
- 84% de positivos encontrados.
- 97% fueron clasificados correctamente.
f1-score: promedio ponderado de las clases.
- Factores influyendo en el estado de salud: 76%.
- Trastorno mental y del comportamiento: 90%.
Porcentaje de proporción a encontrar todas las clasificaciones de este estudio: 87%.
Este porcentaje de proporción a encontrar todas las clasificaciones (G-mean) es el que queremos mejorar aún lo más que se pueda sin perjudicar a las clases de forma significativa con las técnicas de balanceo, por lo que, vamos a seguir probando otras técnicas.
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(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)
AUC: 0.8935866682165464
Vale la pena mencionar que según la curva ROC, el clasificador es bueno pero todavía podría mejorar, tomando en cuenta que también dicha área bajo la curva de 0.89 es un valor adecuado.
Sin embargo, aunque ya aplicamos una técnica de balanceo, se mencionó a lo largo de la sección que dicho balanceo se puede mejorar aún más, por lo que seguiremos con más desarrollo.
Logistic Manual-Balanced.
Para manipular por nuestra cuenta el balanceo de clases, probaremos la técnica de balancear manualmente a nuestra consideración, tomando en cuenta que el total entre n cantidad de clases de la ponderación debe de ser igual a 100%, es decir, si tenemos 4 clases, entre ellas debemos de completar el 100%. Es importante mencionar que para las clases con menos registros, se les da más peso.
y_train.value_counts()
# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
descripcion_grupo_enfermedad_num 1 438 0 138 Name: count, dtype: int64
y_test.value_counts()
# 0: Factores influyendo en el estado de salud. 1: Trastorno mental y del comportamiento.
descripcion_grupo_enfermedad_num 1 109 0 35 Name: count, dtype: int64
Vamos a contruir el modelo manual de la siguiente manera:
- Para la clase 1, que es de Trastorno mental y del comportamiento, le brindaremos un peso de un 45% ya que del mismo hay mucha cantidad de registros.
- Para la clase 0, que es de Factores influyendo en el estado de salud, le brindaremos un peso de 55% ya que de este hay menore cantidad de registros.
Aunque la diferencia no es abismal y podría ser peor, vale la pena tomarla en consideración.
model3 = LogisticRegression(class_weight = {0:0.55, 1:0.45}, random_state = 0) # Llegar hasta 100.
# Entrenamos el modelo.
model3.fit(X_train, y_train)
LogisticRegression(class_weight={0: 0.55, 1: 0.45}, 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 | 'l2' | |
| dual | False | |
| tol | 0.0001 | |
| C | 1.0 | |
| fit_intercept | True | |
| intercept_scaling | 1 | |
| class_weight | {0: 0.55, 1: 0.45} | |
| random_state | 0 | |
| solver | 'lbfgs' | |
| max_iter | 100 | |
| multi_class | 'deprecated' | |
| verbose | 0 | |
| warm_start | False | |
| n_jobs | None | |
| l1_ratio | None |
Checamos cómo pronostica este modelo construido:
# Validar si el modelo pronostica adecuadamente.
y_pred_test = model3.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
accuracy_train = model3.score(X_train, y_train)
print('Accuracy train = {:.2f}'.format(accuracy_train))
accuracy_test = model3.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.86 Accuracy test = 0.85 Diferencia = 0.8681%
Este modelo con la técnica de balanceo establecida de forma manual a consideración propia, nos muestra que tiene un leve sobreajuste de 2.4%, sin embargo, esto no indica el no saber generalizar entre los positivos reales y negativos reales (1 y 0 respectivamente).
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, y_pred_test) # Siempre datos de validación.
cm
array([[29, 6],
[15, 94]])
Matriz de confusión:
104 + 100 = 204 aciertos.
Porcentaje de aciertos → 204 / 219 = 0.93 → 93%.
9 + 6 = 15 errores.
219 en total.
scores = cross_val_score(model3, X, y, cv = 5)
print(scores.mean())
0.8569444444444445
La validación cruzada nos indica que el desempeño del modelo bajo la separación de los datos en entrenamiento y prueba es de 85%.
OR = np.exp(model3.coef_[0])
coef_df = pd.DataFrame({
"Variables": X.columns,
"Coeficientes": model3.coef_[0],
"Odds Ratio": OR
})
print(coef_df)
Variables Coeficientes Odds Ratio 0 edad -0.008226 0.991808 1 sexo_num 0.536212 1.709519 2 institucion_unidad_medica_num -1.729901 0.177302 3 descripcion_enfermedad_num 0.128327 1.136925
Bajo la explicación brindada anteriormente de los Odd Ratio, podemos concluir lo siguiente:
sexo_num y descripcion_enfermedad_num generán 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.
Vale la pena mencionar que dichos valores son casi iguales a los resultados del modelo desbalanceado, por ende se clasifican de esa manera las variables.
# Reporte de clasificación.
from sklearn.metrics import classification_report
from imblearn.metrics import geometric_mean_score
names = ['Factores influyendo en el estado de salud.', 'Trastorno mental y del comportamiento.']
print(classification_report(y_test, y_pred_test, target_names = names))
print('G-mean =', geometric_mean_score(y_test, y_pred_test))
precision recall f1-score support
Factores influyendo en el estado de salud. 0.66 0.83 0.73 35
Trastorno mental y del comportamiento. 0.94 0.86 0.90 109
accuracy 0.85 144
macro avg 0.80 0.85 0.82 144
weighted avg 0.87 0.85 0.86 144
G-mean = 0.8453093146793175
219 datos en prueba.
110 datos de prueba de Factores influyendo en el estado de salud.
- 95% de positivos encontrados.
- 92% fueron clasificados correctamente.
109 datos de prubea de Trastorno mental y del comportamiento.
- 92% de positivos encontrados.
- 94% fueron clasificados correctamente.
f1-score: promedio ponderado de las clases.
- Factores influyendo en el estado de salud: 93%.
- Trastorno mental y del comportamiento: 93%.
Porcentaje de proporción a encontrar todas las clasificaciones de este estudio: 93%.
Este porcentaje de proporción a encontrar todas las clasificaciones (G-mean), obtuvimos en este caso un 93% para este modelo bajo el balanceo manual, sin embargo, se tratará la técnica de SMOTE como un extra para más análisis.
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(model3, 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)
AUC: 0.9046929653073515
La curva ROC de este balanceo arroja que el clasificador del mismo (tomando también en consideración el área bajo la curva de 0.90) que este es el mejor de todos hasta el momento. Se tomará este valor para valorar en comparación con el de SMOTE para más análisis como se mencionó anteriormente.
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.
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, 4) Tamaño de X después de SMOTE: (1094, 4) Balance de clases con SMOTE: descripcion_grupo_enfermedad_num 1 547 0 547 Name: count, dtype: int64 Nuestras clases están balanceadas.
Como ahora estamos imputando datos, hay que volver a separar en entrenamiento y prueba este nuevo conjunto para después construir el modelo.
# 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
((875, 4), (219, 4), (875,), (219,))
# Definimos el modelo.
model4 = LogisticRegression(random_state = 0)
# Entrenamos el modelo.
model4.fit(X_train, y_train)
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 | 'l2' | |
| dual | False | |
| tol | 0.0001 | |
| C | 1.0 | |
| fit_intercept | True | |
| intercept_scaling | 1 | |
| class_weight | None | |
| random_state | 0 | |
| solver | 'lbfgs' | |
| max_iter | 100 | |
| multi_class | 'deprecated' | |
| verbose | 0 | |
| warm_start | False | |
| n_jobs | None | |
| l1_ratio | None |
Checamos cómo pronostica este modelo construido:
# Validar si el modelo pronostica adecuadamente.
y_pred_test = model4.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
accuracy_train = model4.score(X_train, y_train)
print('Accuracy train = {:.2f}'.format(accuracy_train))
accuracy_test = model4.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.90 Accuracy test = 0.92 Diferencia = 2.6374%
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).
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, y_pred_test) # Siempre datos de validación.
cm
array([[102, 8],
[ 9, 100]])
Matriz de confusión:
102 + 100 = 202 aciertos.
Porcentaje de aciertos → 202 / 219 = 0.92 → 92%.
9 + 8 = 17 errores.
219 en total.
scores = cross_val_score(model4, X, y, cv = 5)
print(scores.mean())
0.8722222222222221
La validación cruzada nos indica que el desempeño del modelo bajo la separación de los datos en entrenamiento y prueba es de 87%.
OR = np.exp(model4.coef_[0])
coef_df = pd.DataFrame({
"Variables": X.columns,
"Coeficientes": model4.coef_[0],
"Odds Ratio": OR
})
print(coef_df)
Variables Coeficientes Odds Ratio 0 edad -0.001302 0.998699 1 sexo_num 1.051918 2.863137 2 institucion_unidad_medica_num -1.215389 0.296595 3 descripcion_enfermedad_num 0.151412 1.163476
Bajo la explicación brindada anteriormente de los Odd Ratio, podemos concluir lo siguiente:
sexo_num y descripcion_enfermedad_num generán 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.
Vale la pena mencionar que dichos valores son casi iguales a los resultados del modelo desbalanceado, por ende se clasifican de esa manera las variables.
# Reporte de clasificación.
from sklearn.metrics import classification_report
from imblearn.metrics import geometric_mean_score
names = ['Factores influyendo en el estado de salud.', 'Trastorno mental y del comportamiento.']
print(classification_report(y_test, y_pred_test, target_names = names))
print('G-mean =', geometric_mean_score(y_test, y_pred_test))
precision recall f1-score support
Factores influyendo en el estado de salud. 0.92 0.93 0.92 110
Trastorno mental y del comportamiento. 0.93 0.92 0.92 109
accuracy 0.92 219
macro avg 0.92 0.92 0.92 219
weighted avg 0.92 0.92 0.92 219
G-mean = 0.9223388336741652
219 datos en prueba.
110 datos de prueba de Factores influyendo en el estado de salud.
- 93% de positivos encontrados.
- 92% fueron clasificados correctamente.
109 datos de prubea de Trastorno mental y del comportamiento.
- 92% de positivos encontrados.
- 93% fueron clasificados correctamente.
f1-score: promedio ponderado de las clases.
- Factores influyendo en el estado de salud: 92%.
- Trastorno mental y del comportamiento: 92%.
Porcentaje de proporción a encontrar todas las clasificaciones de este estudio: 92%.
Este porcentaje de proporción a encontrar todas las clasificaciones (G-mean), obtuvimos en este caso un 92% para este modelo bajo la técnica de SMOTE, donde se imputaron datos sintéticos.
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(model4, 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)
AUC: 0.9028014075725713
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.
X.head()
| edad | sexo_num | institucion_unidad_medica_num | descripcion_enfermedad_num | |
|---|---|---|---|---|
| 2625 | 33 | 1 | 0 | 44 |
| 2626 | 53 | 0 | 0 | 38 |
| 2627 | 60 | 0 | 0 | 6 |
| 2628 | 46 | 0 | 0 | 38 |
| 2629 | 47 | 0 | 0 | 38 |
X_new = pd.DataFrame({
'edad': [33, 53, 60],
'sexo_num': [1, 0, 0],
'institucion_unidad_medica_num': [0, 0, 0],
'descripcion_enfermedad_num': [44, 38, 6]
})
y_pred_new = model4.predict(X_new)
y_pred_new
array([1, 1, 0])
1 → recae en Trastorno mental y del comportamiento.
0 → recae en Factores influyendo en el estado de salud.
5.1 Resultados.¶
Los resultados que obtuvimos de cada modelo son los siguientes:
by Default:
86% de aciertos.
0.87 de validación cruzada.
G-mean de 0.85.
f1-score:
0.75 para 0.
0.91 para 1.
AUC: 0.90.
Balanced:
86% de aciertos.
0.87 de validación cruzada.
G-mean de 0.87.
f1-score:
0.76 para 0.
0.90 para 1.
AUC: 0.89.
Manual-Balanced:
93% de aciertos.
0.85 de validación cruzada.
G-mean de 0.85.
f1-score:
0.73 para 0.
0.90 para 1.
AUC: 0.90.
SMOTE:
92% de aciertos.
0.87 de validación cruzada.
G-mean de 0.92.
f1-score:
0.92 para 0.
0.92 para 1.
AUC: 0.90.
Una vez explicando los resultados de cada modelo que se realizó en el estudio, se puede concluir que el mejor es el de SMOTE.
La técnica de SMOTE lo que hace es insertar datos sintéticos dentro de los registros que específicamente están desbalanceados, donde según Crear Datos Sintéticos - una Guía Exhaustiva (s. f.), esto aumenta el conjunto de datos para crear el balance entre las clases.
De igual forma, con este SMOTE podemos observar que el f1-score entre las clases de 0 y 1 es exacta por este balance, donde desde un inicio este promedio era de 0.75 para la clase 0 y 0.91 para la clases 1, mostrando en efecto un desbalance muy grande donde la prioridad iba para la clase 1.
Por esto, es que consideramos que este es el mejor modelo, estableciendo que sus resultados provienen de una imputación de datos sintéticos, sin embargo, debido a que también trabajamos con una nuestra pequeña, no resulta contraproducente en insertar datos nuevos.
6.1 Discusiones.¶
Lo único que se pudo considerar como tedioso más no complicado, fue el implementar las técnicas de balanceo, ya que las mismas requieren de sus etapas de desarrollo para que las mismas sean correctas, sin embargo, las mismas valen la pena porque mejoran el modelo, sin dejar de lado el tomar las consideraciones de cada técnica.
Para mencionar como un extra, se pudo haber implementado una técnica de balanceo extra que hubiera sido la de SMOTEENN, donde esta elimina datos para el balanceo, pero en este caso, la misma hubiera resultado contraproducente al tener pocos datos de los cuáles tomar para el entrenamiento y prueba del modelo; si se tuviera una base de datos más grande o si hubieramos trabajado con más municipios, probablemente hubiera valido la pena implementar también esta técnica ya que la cantidad de datos aumenta.
7.1 Conclusiones.¶
Para concluir, este estudio aportó en determinar si dicho paciente recae en el grupo 0 donde solo necesita una ayuda psicológica muy breve, a diferencia del grupo 1 que podría necesitar de seguimiento más establecido y controlado, debido a que se tiene establecido que padece de algún trastorno mental o del comportamiento de los individuos que residen dentro del municipio de San Pedro Garza García, dentro del rango de edades de 18 a 65 años, siendo las edades más activas.
Una vez teniendo este planteamiento que se mencionó desde un inicio, al momento de desarrollar el modelo es que se tiene que buscar que sus clases estén balanceadas, para que la construcción de la Regresión Logística (en este caso) sea adecuada y se pueda adaptar para buscar las probabilidades de 0 a 1 correctas y proporcionadas - dicho problema se trató y se resolvió, concluyendo en la sección de Resultados que el mejor modelo es el de SMOTE, bajo la técnica de imputar datos sintéticos al dataset que tenía relativamente pocos datos al estar filtrado por ciertas condiciones que se establecieron y se justificaron en el proyecto realizado para el primer parcial.
7.2 Aprendizajes.¶
Los aprendizajes que sigo reforzando al momento de realizar este estudio bajo la Regresión Logística es seguir aplicando y desarrollando las técnicas para generar un buen modelo, mientras que se implementan nuevas validaciones para el modelo que fueron aprendidas recientemente en la clase de Inteligencia Artificial, como lo es los Odd Ratio y la Curva ROC en conjunto con el AUC, los cuáles abren mucho más el panorama como Científico de Datos/Ingeniero de Apendizaje de Datos para poder recopilar más información del modelo e interpretarla, para tener mucho más alcance en el storytelling.
7.3 Implicaciones.¶
Las implicaciones de este estudio sería ahora aplicar dicha Regresión Logística para clasificar a estos grupos registrados pero ahora en un mayor alcance, pudiendo ser con niños, solo adultos mayores o de diversos municipios en conjunto o de uno en específico, por ejemplo, se podría hacer un análisis con adultos mayores en municipios fuera del área metropolitana de Monterrey al dichas zonas fuera de la metropoli siendo habitadas por personas con mayor edad debido a los desplazamientos de sus familias - pudiendo alcanzar hasta un enfoque de la Antropología gracias a estudios como este.
7.4 Posibles líneas futuras.¶
Como se mencionó anteriormente en las implicaciones, el dataset tiene mucho potencial para modelos de Regresión Logística, ya que aún hay muchos aspectos que no se abordaron al estar trabajando con la limitación del municipio y del rango de edad, solo por mencionar dichas condiciones que se justificaron en el proyecto.
8.1 Referencias.¶
3.1. Cross-validation: evaluating estimator performance. (s. f.). Scikit-learn. https://scikit-learn.org/stable/modules/cross_validation.html
Crear datos sintéticos - Una guía exhaustiva. (s. f.). https://www.q2bstudio.com/nuestro-blog/104211/generacion-de-datos-sinteticos-con-python-y-sql-una-guia-exhaustiva-para-el-diseo-y-creacion-de-conjuntos-de-datos-simulados#:~:text=Qu%C3%A9%20es%20datos%20sint%C3%A9ticos%20Los%20datos%20sint%C3%A9ticos,son%20escasos%2C%20sensibles%20o%20costosos%20de%20obtener.
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/
roc_curve. (s. f.). Scikit-learn. https://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_curve.html
Torres, L. (2024). Curva ROC y AUC en Python. The Machine Learners. https://www.themachinelearners.com/curva-roc-vs-prec-recall/
9.1 Código de Honor de la Universidad de Monterrey.¶
Doy mi palabra que he realizado esta actividad con integridad académica.