# APÉNDICE {#apendice}
Lectura complementaria.
## La magia de los percentiles {#apendice-percentiles}
El percentil es un concepto tan crucial en el análisis de datos que vamos a cubrirlo ampliamente en este libro. Considera cada observación con respecto a las demás. Un número aislado puede no ser significativo, pero cuando se compara con otros, aparece el concepto de distribución.
Los percentiles se utilizan en el análisis numérico, así como en la evaluación del rendimiento de un modelo predictivo.
<br>
{width="100%"}
<br>
**El conjunto de datos, un consejo antes de continuar:**
Esto contiene muchos indicadores sobre el desarrollo mundial. Independientemente del ejemplo del análisis numérico, la idea es proporcionar una tabla lista para usar para sociólogos, investigadores, etc. interesados en analizar este tipo de datos.
La fuente de datos original es: <a href="http://databank.worldbank.org/data/reports.aspx?source=2&Topic=11#" target="blank">http://databank.worldbank.org</a>. Ahí encontrarán un diccionario de datos que explica todas las variables.
En esta sección utilizaremos una tabla que ya está preparada para el análisis. La preparación completa de los datos paso a paso se encuentra en el capítulo [Análisis numérico](#analisis_numerico).
Pueden buscar el significado de cualquier indicador en data.worldbank.org. Por ejemplo, si queremos saber qué significa `EN.POP.SLUM.UR.ZS`, entonces escribimos: http://data.worldbank.org/indicator/EN.POP.SLUM.UR.ZS
<br>
```{python}
#| echo: false
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
from funpymodeling import (
status, freq_tbl, profiling_num, plot_num,
equal_freq, tukey_outlier, hampel_outlier, prep_outliers,
categ_analysis, discretize_get_bins, discretize_df,
cross_plot, var_rank_info, gain_lift,
plotar, range01, convert_df_to_categoric,
)
# Configuración de visualización
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (10, 6)
pd.set_option('display.max_columns', 20)
pd.set_option('display.width', 200)
# --- Cargar heart_disease desde UCI ---
url_hd = ("https://archive.ics.uci.edu/ml/machine-learning-databases/"
"heart-disease/processed.cleveland.data")
column_names = [
'age', 'gender', 'chest_pain', 'resting_blood_pressure',
'serum_cholestoral', 'fasting_blood_sugar', 'resting_electro',
'max_heart_rate', 'exer_angina', 'oldpeak',
'slope', 'num_vessels_flour', 'thal', 'heart_disease_severity'
]
heart_disease = pd.read_csv(url_hd, names=column_names, na_values='?')
heart_disease['has_heart_disease'] = np.where(
heart_disease['heart_disease_severity'] > 0, 'yes', 'no')
# --- Cargar data_country desde CSV ---
data_country = pd.read_csv("images/data_country.csv")
# --- Cargar data_world_wide ---
data_world_wide = pd.read_csv(
"images/data_world_wide.txt", sep="\t")
```
### Cómo calcular percentiles
Existen varios métodos para obtener el percentil. Basado en interpolaciones, la forma más fácil es ordenar la variable de forma ascendente, seleccionando el percentil que deseamos (por ejemplo, 75%), y luego observando _cuál es el valor máximo si queremos elegir el 75% de la población ordenada_.
Ahora vamos a usar la técnica de mantener la muestra pequeña para que podamos tener el máximo control sobre _lo que está sucediendo_ detrás del cálculo.
Conservamos los 10 países aleatorios y visualizamos el vector de `rural_poverty_headcount`, que es la variable que vamos a utilizar.
```{python}
data_sample = data_world_wide[
data_world_wide['Country.Name'].isin([
"Kazakhstan", "Zambia", "Mauritania", "Malaysia",
"Sao Tome and Principe", "Colombia", "Haiti",
"Fiji", "Sierra Leone", "Morocco"
])
].sort_values('rural_poverty_headcount')
data_sample[['Country.Name', 'rural_poverty_headcount']]
```
Tengan en cuenta que el vector se ordena sólo con fines didácticos. _Como dijimos en el capítulo de Análisis numérico, a nuestros ojos les gusta el orden._
Ahora aplicamos la función "cuantitativa" a otra variable (el porcentaje de la población rural que vive por debajo de las líneas de pobreza):
```{python}
data_sample['rural_poverty_headcount'].quantile([0, 0.25, 0.5, 0.75, 1])
```
**Análisis**
* **Percentil 50%**: el 50% de los países (cinco de ellos) tienen una `rural_poverty_headcount` debajo de `51.7`. Podemos comprobar esto en la última tabla: estos países son: Fiji, Colombia, Marruecos, Kazajistán, y Malasia.
* **Percentil 25%**: el 25% de los países están por debajo de 20.87. Aquí podemos ver una interpolación porque el 25% representa ~2.5 países. Si utilizamos este valor para filtrar los países, entonces tendremos tres países: Marruecos, Kazajistán, y Malasia.
Más información sobre los diferentes tipos de cuantiles y sus interpolaciones: consultar la documentación de `numpy.quantile` y `pandas.Series.quantile`.
#### Obtener las descripciones semánticas
Del último ejemplo podemos afirmar que:
* _"La mitad de los países tienen hasta un 51.7% de pobreza rural"_
* _"Tres cuartas partes de los países tienen un máximo de 64.4% en cuanto a su pobreza rural"_ (basado en los países ordenados ascendentemente).
También podemos pensar en **usar lo contrario**:
* _"Una cuarta parte de los países que presentan los valores más altos de pobreza rural tienen un porcentaje de por lo menos 64.4%"_
### Calcular cuantiles personalizados
Típicamente, queremos calcular ciertos cuantiles. La variable de ejemplo será el `gini_index`.
**¿Qué es el Índice de Gini?**
Es una medida de la desigualdad de ingresos o de riqueza.
* Un coeficiente de Gini de **cero** expresa **igualdad perfecta** donde todos los valores son iguales (por ejemplo, donde todos tienen los mismos ingresos).
* Un coeficiente de Gini de **1** (o 100%) expresa **desigualdad máxima** entre los valores (por ejemplo, en un gran número de personas, donde sólo una persona tiene todos los ingresos o el consumo mientras que todas las demás no tienen ninguno, el coeficiente de Gini será muy cercano a uno).
Fuente: https://en.wikipedia.org/wiki/Gini_coefficient
**Ejemplo en Python**:
Si queremos obtener los cuantiles de 20, 40, 60, y 80 de la variable del índice de Gini, usamos el método `quantile` de pandas.
El método descarta automáticamente los `NaN`, pero podemos ser explícitos con `dropna()`:
```{python}
# También podemos obtener múltiples cuantiles en simultáneo
p_custom = data_world_wide['gini_index'].dropna().quantile(
[0.2, 0.4, 0.6, 0.8])
p_custom
```
### Indicar dónde están la mayoría de los valores
En estadística descriptiva, queremos describir la población en términos generales. Podemos hablar de rangos usando dos percentiles. Tomemos los percentiles 10 y 90 para describir al 80% de la población.
_La pobreza oscila entre el ~0.07% y el ~54% en el 80% de los países_. (80% porque hicimos percentil 90 - percentil 10, centrándonos en la mitad de la población.)
Si consideramos al 80% como la mayoría de la población, entonces podríamos decir: _"Normalmente (o en términos generales), la pobreza pasa de ~0.07% a ~54%"_. Esta es una descripción semántica.
Observamos al 80% de la población, que parece ser un buen número para describir dónde están la mayoría de los casos. También podríamos haber usado el rango del 90% (percentil 95 - percentil 5).
#### ¿El percentil se relaciona con el cuartil?
**Cuartil** es el nombre formal para los percentiles 25, 50 y 75 (cuartos o 'Q'). Si queremos observar el 50% de la población, debemos restar el 3er cuartil (o percentil 75) del 1er cuartil (percentil 25) para saber dónde está concentrado el 50% de los datos, también conocido como el **rango intercuartil** o IQR.
Percentil vs. cuantil vs. cuartil
```
0 cuartil = 0 cuantil = 0 percentil
1 cuartil = 0.25 cuantil = 25 percentil
2 cuartil = .5 cuantil = 50 percentil (mediana)
3 cuartil = .75 cuantil = 75 percentil
4 cuartil = 1 cuantil = 100 percentil
```
Créditos: [@perc_quan_quar].
### Visualizar cuantiles
Graficar un histograma junto a los lugares donde se encuentra cada percentil puede ayudarnos a entender el concepto:
```{python}
#| fig-cap: "Visualizar cuantiles"
quantiles_var = data_world_wide['poverty_headcount_1.9'].dropna().quantile(
[0.25, 0.5, 0.75])
df_p = pd.DataFrame({
'value': quantiles_var.values,
'quantile': ['25th', '50th', '75th']
})
fig, ax = plt.subplots(figsize=(7, 4))
sns.histplot(data_world_wide['poverty_headcount_1.9'].dropna(),
bins=20, color='gray', alpha=0.7, ax=ax)
colors = ['#E41A1C', '#377EB8', '#4DAF4A']
for i, row in df_p.iterrows():
ax.axvline(x=row['value'], color=colors[i],
linestyle='--', linewidth=2, label=row['quantile'])
ax.legend(title='Quantile')
ax.set(xlabel='poverty_headcount_1.9', ylabel='Frecuencia')
sns.despine()
plt.tight_layout()
plt.show()
```
Si sumamos todas las barras grises antes del percentil 25, tendrán aproximadamente la misma altura acumulada que la suma de las barras grises después del percentil 75.
En el último gráfico, el IQR aparece entre la primera y la última línea punteada y contiene el 50% de la población.
### Conceptos de clasificación y top/bottom '_X%_'
El concepto de clasificación es el mismo que el de las competiciones. Nos permite responder _¿cuál es el país con la tasa más alta en la variable pop_living_slums?_
Usaremos el método `rank` de pandas con `method='dense'`. Asigna la posición (puesto) a cada país, pero las necesitamos en orden inverso; es decir, asignamos el `rank = 1` al valor más alto.
Ahora la variable será: _La población que vive en hogares marginales es la proporción de la población urbana que vive en hogares marginales. Un hogar marginal se define como un grupo de individuos que viven bajo el mismo techo y que carecen de una o más de las siguientes condiciones: acceso a agua potable, acceso a saneamiento mejorado, suficiente área habitable y durabilidad de la vivienda._
La pregunta a responder: _¿Cuáles son los seis países con las tasas más altas de personas que viven en hogares marginales?_
```{python}
# Crear la variable de clasificación
data_world_wide['rank_pop_living_slums'] = (
data_world_wide['pop_living_slums']
.rank(method='dense', ascending=False)
.astype('Int64')
)
```
```{python}
# Ordenar los datos según la clasificación
data_world_wide = data_world_wide.sort_values(
'rank_pop_living_slums')
# Visualizar los primeros seis resultados
data_world_wide[['Country.Name',
'rank_pop_living_slums']].head(6)
```
También podemos preguntar: _¿En qué posición está Ecuador?_
```{python}
data_world_wide.loc[
data_world_wide['Country.Name'] == 'Ecuador',
['rank_pop_living_slums']
]
```
##### Concepto de top/bottom '_X%_'
Otra pregunta que podría interesarnos responder: _¿Cuál es el valor con el que obtengo el 10% superior de los valores más bajos?_
El percentil 10 es la respuesta:
```{python}
data_world_wide['pop_living_slums'].quantile(0.1)
```
Trabajando en lo opuesto: _¿Cuál es el valor con el que obtengo el 10% inferior de los valores más altos?_
El percentil 90 es la respuesta, podemos filtrar todos los casos por encima de este valor:
```{python}
data_world_wide['pop_living_slums'].quantile(0.9)
```
### Uso de percentiles en el scoring de datos
Hay dos capítulos que usan este concepto:
* [Scoring de datos](#scoring_de_datos)
* [Análisis de Ganancia y Lift](#ganancia_y_lift)
La idea básica es desarrollar un modelo predictivo que prediga una variable binaria (`yes`/`no`). Supongamos que necesitamos puntuar nuevos casos, por ejemplo, para utilizarlos en una campaña de marketing. La pregunta a responder es:
_¿Cuál es el valor del puntaje que deberíamos sugerirle a los ejecutivos de ventas para capturar el 50% de las nuevas ventas potenciales?_ La respuesta proviene de una combinación de análisis de percentil sobre el valor del puntaje más el análisis acumulativo de la variable objetivo actual.
{width="80%"}
<br>
#### Estudio de caso: Distribución de la riqueza
La distribución de la riqueza es similar a la del índice de Gini y se centra en la desigualdad. Mide los activos de los propietarios (que son diferentes de los ingresos), haciendo que la comparación entre países sea más uniforme con lo que las personas pueden adquirir según el lugar en el que viven. Para una mejor definición, consulten el artículo de _Wikipedia_ y el _Informe sobre el patrimonio mundial 2013_. Referencias: [@distr_wealth] y [@global_wealth], respectivamente.
Citando a _Wikipedia_ (Ref. [@distr_wealth]):
> la mitad de la riqueza mundial pertenece al 1% de la población;
> el 10% superior de los adultos posee el 85%, mientras que el 90% inferior posee el 15% restante de la riqueza total del mundo; y
> el 30% de los adultos más ricos poseen el 97% de la riqueza total.
Al igual que antes, a partir de la tercera frase podemos afirmarlo: _"El 3% de la riqueza total se distribuye entre el 70% de los adultos"._
"Las métricas `top 10%` y `top 30%` son los quantiles `0.9` y `0.7. La riqueza es la variable numérica."
<br>
---
{width="300px"}
---
<br>
## Quick-start `funpymodeling`
Este paquete contiene un conjunto de funciones relacionadas con el análisis exploratorio de datos, la preparación de datos y el desempeño del modelo. Es utilizado por personas procedentes de los negocios, la investigación y la docencia (profesores y estudiantes).
`funpymodeling` (en Python) están íntimamente relacionados con este libro, en el sentido de que la mayor parte de su funcionalidad se utiliza para explicar los diferentes temas abordados por el libro. Desde la versión 0.2.1, `funpymodeling` contiene todas las funciones migradas de R, incluyendo `plotar`, `range01` y `convert_df_to_categoric`.
### Abriendo la caja negra
Algunas funciones tienen comentarios incluidos en las líneas para que el usuario pueda abrir la caja negra y aprender cómo se desarrolló, o para afinar o mejorar cualquiera de ellas.
Todas las funciones están bien documentadas, explicando todos los parámetros con la ayuda de muchos ejemplos breves. Se puede acceder a la documentación en Python a través de: `help(nombre_de_la_funcion)`.
<br>
#### Sobre este quick-start
Este quick-start se centra sólo en las funciones. Pueden consultar todas las explicaciones en torno a ellas, así como el cómo y cuándo utilizarlas, siguiendo los enlaces "_**Leer más aquí.**_" que se encuentran debajo de cada sección, que los redirigirán al libro.
A continuación está la mayoría de las funciones de `funModeling`/`funpymodeling` divididas por categoría, con su equivalente en Python.
### Análisis exploratorio de datos
#### `status`: Estado de salud de un conjunto de datos
Caso de uso: analiza los ceros, los valores faltantes (`NaN`), el infinito, el tipo de datos y el número de valores únicos para un conjunto de datos determinado.
```{python}
st = status(heart_disease).round(2)
print(st[['variable', 'type', 'q_nan',
'p_nan']].to_string(index=False))
```
[Leer más aquí: [**Estado de salud de un conjunto de datos**](#estado-de-salud-de-un-conjunto-de-datos)]
**Nota importante sobre valores faltantes:** El dataset original de Cleveland tiene 2 valores faltantes en `thal` y 4 en `num_vessels_flour`. A continuación convertiremos algunas variables categóricas a tipo `str` para facilitar su uso con ciertas funciones de `funpymodeling`. Esta conversión transforma los `NaN` al string literal `'nan'`, por lo que ya no serán detectados como valores faltantes en análisis posteriores.
```{python}
#| echo: false
# Convertir variables categóricas a string para uso con funpymodeling
heart_disease['chest_pain'] = heart_disease['chest_pain'].astype(str)
heart_disease['thal'] = heart_disease['thal'].astype(str)
heart_disease['slope'] = heart_disease['slope'].astype(str)
heart_disease['fasting_blood_sugar'] = (
heart_disease['fasting_blood_sugar'].astype(str))
heart_disease['resting_electro'] = (
heart_disease['resting_electro'].astype(str))
heart_disease['gender'] = heart_disease['gender'].astype(str)
```
<br>
#### `plot_num`: Graficar las distribuciones de variables numéricas
Solamente grafica variables numéricas.
```{python}
#| fig-cap: "plot_num: visualizar variables numéricas"
plot_num(heart_disease)
```
Notas:
* `bins`: configura la cantidad de segmentos (10 por defecto).
* Para exportar el gráfico: `plt.savefig("plot_num.png")` antes de `plt.show()`.
[Leer más aquí: [**Graficar variables numéricas**](#graficar-variables-numericas)]
<br>
#### `profiling_num`: Calcular varias estadísticas para variables numéricas
Obtiene varias estadísticas para variables numéricas.
```{python}
df_prof = profiling_num(heart_disease)
print(df_prof[['variable', 'mean', 'std_dev', 'variation_coef']])
```
```{python}
print(df_prof[['variable', 'p_01', 'p_05', 'p_95', 'p_99']])
```
Nota:
* `plot_num` y `profiling_num` automáticamente excluyen variables no numéricas
[Leer más aquí: [**Análisis numérico en Python**](#analisis-numerico-en-python)]
<br>
#### `freq_tbl`: Obtener las distribuciones de frecuencia de variables categóricas
```{python}
#| fig-cap: "freq_tbl: visualizar variables categóricas"
# Seleccionar sólo dos variables para este ejemplo
heart_disease_2 = heart_disease[['chest_pain', 'thal']]
# Distribución de la frecuencia
freq_tbl(heart_disease_2)
```
Notas:
* `freq_tbl` sólo procesa variables categóricas (tipo `object` o `category`), excluyendo variables numéricas.
* Devuelve la tabla de distribución como un DataFrame.
* Si `input` está vacío, entonces se ejecuta para todas las variables categóricas.
* `na_rm` indica si los valores `NaN` deberían ser excluidos (`False` por defecto).
* Para exportar el gráfico: `plt.savefig("freq.png")` antes de `plt.show()`.
[Leer más aquí: [**Análisis de variables categóricas**](#analisis-variables-categoricas)]
<br>
### Correlaciones
#### `var_rank_info`: Correlación basada en Teoría de la Información
Calcula la correlación basándose en distintas métricas de la Teoría de la Información entre todas las variables de un data frame y una variable objetivo.
```{python}
# var_rank_info del paquete funpymodeling
var_rank_info(heart_disease, 'has_heart_disease')
```
Nota: Analiza variables numéricas y categóricas. También se utiliza con el método de discretización numérica como antes, tal como `discretize_df`.
[Leer más aquí: [**Rankear las mejores variables usando la Teoría de la Información**](#seleccionar_factores_var_clas_info)]
<br>
#### `cross_plot`: Gráfico de distribución entre variable de entrada y variable objetivo
Obtiene la distribución relativa y absoluta entre una variable de entrada y una variable objetivo.
Es útil para explicar y reportar si una variable es importante o no.
```{python}
#| fig-cap: "cross_plot: Visualizar variable de entrada vs. variable objetivo"
cross_plot(data=heart_disease, input=['age', 'oldpeak'],
target='has_heart_disease')
```
Notas:
* `auto_binning`: `True` por defecto, muestra la variable numérica como categórica.
* Para exportar el gráfico: `plt.savefig("cross_plot.png")` antes de `plt.show()`.
* `input` puede ser numérica o categórica, y `target` debe ser una variable binaria (dos clases).
* Si `input` está vacío, entonces se ejecutará para todas las variables.
[Leer más aquí: [**Usando cross_plot (dataViz)**](#analisis_objetivo_cross_plot)]
<br>
#### `plotar`: Diagramas de caja e histogramas de densidad entre variables de entrada y objetivo
Es útil para explicar y reportar si una variable es importante o no.
**Diagrama de caja:**
```{python}
#| fig-cap: "plotar (1): visualizar un diagrama de caja"
plotar(data=heart_disease,
input=["age", "oldpeak"],
target="has_heart_disease",
plot_type="boxplot")
```
[Leer más aquí: [**Usando boxplots**](#analisis-numerico-diagramas-caja)]
<br>
**Histogramas de densidad:**
```{python}
#| fig-cap: "plotar (2): visualizar histograma de densidad"
_mtcars = pd.read_csv(
"https://raw.githubusercontent.com/"
"vincentarelbundock/Rdatasets/master/"
"csv/datasets/mtcars.csv")
mtcars = _mtcars[['gear', 'cyl']].copy()
mtcars['cyl'] = mtcars['cyl'].astype(str)
plotar(data=mtcars, input="gear",
target="cyl", plot_type="histdens")
```
[Leer más aquí: [**Usando histogramas de densidad**](#analisis-numerico-histogramas-densidad)]
Notes:
* Para exportar el gráfico: `plt.savefig("plotar.png")` antes de `plt.show()`.
* Si `input` está vacío, entonces se ejecuta para todas las variables numéricas (saltéandose las categóricas).
* `input` debe ser numérico y la variable objetivo debe ser categórica.
* `target` puede ser multi-clase (no sólo binario).
<br>
#### `categ_analysis`: Análisis cuantitativo para un resultado binario
Hace un análisis numérico de una variable binaria objetivo basándose en una variable categórica de entrada, la representatividad (`perc_rows`) y la precisión (`perc_target`) de cada valor de la variable de entrada; por ejemplo, la tasa de infección por gripe por país.
```{python}
df_ca = categ_analysis(data=data_country,
input="country",
target="has_flu")
n_cols = len(df_ca.columns)
mid = (n_cols + 1) // 2
print(df_ca.head(6).iloc[:, :mid].to_string(index=False))
print(df_ca.head(6).iloc[:, mid:].to_string(index=False))
```
Nota:
* La variable `input` debe ser categórica.
* La variable `target` debe ser binaria (dos valores posibles).
Esta función se utiliza para analizar datos cuando necesitamos reducir la cardinalidad de las variables en el modelado predictivo.
[Leer más aquí: [**Variables de alta cardinalidad en modelado predictivo**](#alta_cardinalidad_modelo_predictivo)]
### Preparación de datos
#### Discretización de datos
##### `discretize_get_bins` + `discretize_df`: Convertir variables numéricas a categóricas
Necesitamos dos funciones: `discretize_get_bins`, que devuelve los umbrales para cada variable, y luego `discretize_df`, que toma el resultado de la primera función y convierte las variables deseadas. El criterio de segmentación es el de igual frecuencia.
Ejemplo: convertir sólo dos variables de un conjunto de datos.
```{python}
# Paso 1: Obtener los umbrales de las variables deseadas: "max_heart_rate" y "oldpeak"
d_bins = discretize_get_bins(data=heart_disease,
input=["max_heart_rate", "oldpeak"],
n_bins=5)
```
```{python}
# Paso 2: Aplicar el umbral para llegar al data frame final
heart_disease_discretized = discretize_df(
data=heart_disease, data_bins=d_bins)
```
La siguiente imagen ilustra el resultado. Por favor noten que el nombre de la variable no cambió.
{width="80%"}
Notas:
* Este procedimiento de dos pasos está pensado para ser utilizado en producción con nuevos datos.
* Los valores mín y máx de cada segmento serán `-Inf` e `Inf`, respectivamente.
[Leer más aquí: [**Discretización automática de data frames**](#discretizacion-datos)]
<br>
#### `convert_df_to_categoric`: Convertir cada columna de un data frame a variables carácter
La segmentación, o el criterio de discretización para cualquier variable numérica es igual frecuencia. Las variables factor son convertidas directamente a variables carácter.
```{python}
from sklearn.datasets import load_iris
iris = load_iris(as_frame=True).frame
iris_char = convert_df_to_categoric(data=iris, n_bins=5)
# Verificar las primeras filas (en 2 partes)
print(iris_char.head().iloc[:, :3].to_string(index=False))
print(iris_char.head().iloc[:, 3:].to_string(index=False))
```
#### `equal_freq`: Convertir variable numérica a categórica
Convierte un vector numérico en categoría usando el criterio de igual frecuencia.
```{python}
new_age = equal_freq(heart_disease['age'], n_bins=5)
# Verificar los resultados
new_age.value_counts().sort_index()
```
[Leer más aquí: [**Igual frecuencia**](#segmentacion-igual-frecuencia)]
Notas:
* A diferencia de `discretize_get_bins`, esta función no inserta `-Inf` y `Inf` como los valores mín y máx, respectivamente.
<br>
#### `range01`: Escala la variable en el rango de 0 a 1
Convierte un vector numérico a una escala que va de 0 a 1, donde 0 es el mínimo y 1 es el máximo.
```{python}
oldpeak_scaled = range01(heart_disease['oldpeak'])
# Verificar los resultados
oldpeak_scaled.describe()
```
<br>
### Preparación de datos con valores atípicos
#### `hampel_outlier` y `tukey_outlier`: Obtiene el umbral de los valores atípicos
Ambas funciones obtienen un diccionario con dos valores que indica el umbral en el que los valores son considerados atípicos.
Las funciones `tukey_outlier` y `hampel_outlier` son utilizadas internamente en `prep_outliers`.
**Usando el método de Tukey:**
```{python}
tukey_outlier(heart_disease['resting_blood_pressure'])
```
[Leer más aquí: [**Método de Tukey**](#detectar-valores-atipicos-usando-metodo-Tukey)]
<br>
**Usando el método de Hampel:**
```{python}
hampel_outlier(heart_disease['resting_blood_pressure'])
```
[Leer más aquí: [**Método de Hampel**](#detectar-valores-atipicos-usando-metodo-Hampel)]
<br>
#### `prep_outliers`: Preparar los valores atípicos de un data frame
Toma un data frame y devuelve el mismo data frame más las transformaciones especificadas en el parámetro `input`. También funciona con un sólo vector.
Ejemplo tomando dos variables como datos de entrada:
```{python}
# Obtener el umbral según el método de Hampel
hampel_outlier(heart_disease['max_heart_rate'])
```
```{python}
# Aplicar la función para frenar los valores atípicos en los valores del umbral
data_prep = prep_outliers(data=heart_disease,
input=['max_heart_rate',
'resting_blood_pressure'],
method='hampel', type='stop')
```
Verificar el antes y después de la variable `max_heart_rate`:
```{python}
print(f"Before transformation -> Min: {heart_disease['max_heart_rate'].min()}; "
f"Max: {heart_disease['max_heart_rate'].max()}")
print(f"After transformation -> Min: {data_prep['max_heart_rate'].min()}; "
f"Max: {data_prep['max_heart_rate'].max()}")
```
Notas:
* `method` puede ser: `bottom_top`, `tukey` o `hampel`.
* `type` puede ser: `stop` o `set_na`. Si es `stop` todos los valores marcados como atípicos serán frenados en el umbral. Si es `set_na`, entonces los valores marcados serán tomados como `NaN`.
[Leer más aquí: [**Cómo lidiar con valores atípicos en Python**](#como_lidiar_con_valores_atipicos_en_python)]
<br>
### Desempeño de un modelo predictivo
#### `gain_lift`: Curva de desempeño de ganancia y lift
Después de calcular los scores o probabilidades de la clase que queremos predecir, los pasamos a la función `gain_lift`, que devuelve un DataFrame con métricas de desempeño.
```{python}
#| fig-cap: "ganancia y lift: visualizando el desempeño de un modelo predictivo"
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
# Crear un modelo de machine learning y obtener sus puntajes para casos positivos
df_gl = heart_disease[['age', 'oldpeak', 'has_heart_disease']].dropna().copy()
X_gl = df_gl[['age', 'oldpeak']]
y_gl = (df_gl['has_heart_disease'] == 'yes').astype(int)
# Escalar las variables
scaler = StandardScaler()
X_gl_scaled = scaler.fit_transform(X_gl)
# Ajustar modelo (equivalente a glm binomial en R)
fit_glm = LogisticRegression(random_state=42)
fit_glm.fit(X_gl_scaled, y_gl)
# Obtener los scores
df_gl['score'] = fit_glm.predict_proba(X_gl_scaled)[:, 1]
# Calcular las métricas de desempeño
gain_lift(data=df_gl, score='score', target='has_heart_disease')
```
[Leer más aquí: [**Análisis de Ganancia y Lift**](#ganancia_y_lift)]
<br>
---
{width="300px"}
---