Módulo 3: Visualización de datos interactivos#
En este módulo, exploraremos la visualización de datos, una herramienta esencial para transformar grandes volúmenes de información en representaciones visuales claras y útiles para la toma de decisiones. A través de gráficos y visualizaciones, podremos identificar patrones, tendencias y relaciones en los datos, facilitando la comunicación y el análisis.
Comenzaremos con un enfoque fundamental: el tratamiento de datos faltantes y atípicos. Aprenderemos a identificar y manejar estos problemas comunes que pueden afectar la calidad y precisión de nuestras visualizaciones. Un manejo adecuado de estos datos es crucial para asegurar que nuestras representaciones visuales sean confiables y significativas.
A continuación, avanzaremos hacia la visualización interactiva, que nos permitirá desarrollar gráficos dinámicos y personalizables, mejorando la interacción y el análisis de datos.
Seguiremos con la visualización de datos a través de capas, una biblioteca que ofrece una forma intuitiva de crear gráficos complejos mediante una sintaxis declarativa. Este enfoque facilita la combinación y superposición de diferentes tipos de visualizaciones para obtener una comprensión más profunda de los datos.
También profundizaremos en la visualización interactiva de datos geográficos, una habilidad clave para realizar análisis espaciales y desarrollar soluciones que consideren la ubicación geográfica.
Finalmente, introduciremos Dash, una herramienta potente y de fácil uso para crear aplicaciones web interactivas centradas en la visualización de datos. Con Dash, aprenderás a transformar scripts de Python en aplicaciones web completamente funcionales, permitiéndote combinar todas las técnicas de visualización vistas en el módulo en una interfaz interactiva y accesible para el usuario final.
Al completar este módulo, contarás con un conjunto sólido de habilidades para crear visualizaciones de datos desde cero, manejar datos atípicos y faltantes, y desarrollar aplicaciones visuales interactivas que sean impactantes y útiles.
Tratamiento de datos faltantes y atípicos#
Introducción#
En esta sección se enumeran y explican los posibles errores que pueden surgir durante diversas etapas del proceso de visualización de datos, tales como la representación de elementos no correlacionados para sugerir una relación o la incorporación de características interactivas inadecuadas.
Aunque el proceso de visualización de datos puede parecer sencillo tomar algunos datos, trazar gráficos y añadir funciones interactivas en realidad es fácil cometer errores en diferentes fases del mismo. Estos errores pueden resultar en visualizaciones defectuosas que no logran comunicar de manera clara y eficaz la información contenida en los datos, lo que las convierte en herramientas inútiles para el público al que van dirigidas.
Tratamiento de valores faltantes#
Los datos perdidos son, como su nombre indica, valores que están en blanco (NaN, -, 0 cuando no deberían ser 0, etc.). Al igual que los valores atípicos, los valores perdidos pueden ser problemáticos tanto en el caso de las visualizaciones como en el de los modelos predictivos.
Los valores faltantes en las visualizaciones pueden mostrar una tendencia que en realidad no existe o no representa una relación entre dos variables que, en realidad, es significativa. Aunque es posible crear visualizaciones con un conjunto de datos que contenga valores perdidos, no se recomienda hacerlo. Al hacerlo, se ignoran los casos en los que se encuentran esos valores perdidos, creando así una visualización basada en algunos de los datos pero no en todos.
Por lo tanto, el tratamiento de los valores perdidos es de suma importancia. Existen dos enfoques principales para tratar los valores perdidos: la supresión y la imputación.
Este conjunto de datos contiene información relacionada con los empleos de profesionales de datos, recopilada durante varios años. Cada fila representa un empleo único y proporciona detalles sobre el puesto, nivel de experiencia, modalidad de trabajo, ubicación del empleado, y otros factores relevantes para el contexto laboral y el salario recibido. Este conjunto de datos es útil para analizar tendencias en el mercado laboral de la ciencia de datos, permitiendo comparaciones entre diferentes tipos de empleos, niveles de experiencia, y ubicaciones geográficas.
Descripción de las columnas
Columna |
Descripción |
Tipo de dato |
|---|---|---|
Working_Year |
Año en que se obtuvieron los datos sobre el empleo. Esto puede ser útil para observar tendencias a lo largo del tiempo. |
Float |
Designation |
Título del puesto del profesional de datos. Ejemplos comunes incluyen “Data Scientist”, “Data Engineer”, etc. |
Cadena |
Experience |
Nivel de experiencia del empleado en el puesto, categorizado como “Medio”, “Senior”, etc. Esto ayuda a segmentar los trabajos según la experiencia necesaria. |
Cadena |
Employment_Status |
Tipo de contrato laboral bajo el cual se emplea a la persona. Puede ser a tiempo completo (“FT”), medio tiempo (“PT”), entre otros. Es un indicador de la estabilidad y dedicación del empleo. |
Cadena |
Employee_Location |
País donde se encuentra ubicado el empleado. Esto puede influir en el salario y las condiciones laborales, dada la variación entre mercados internacionales. |
Cadena |
Company_Size |
Tamaño de la empresa en la que trabaja el profesional de datos. Las empresas se clasifican en “S” (Pequeña), “M” (Mediana), o “L” (Grande), y esto puede influir en las oportunidades de crecimiento, ambiente de trabajo, y remuneración. |
Cadena |
Remote_Working_Ratio |
Porcentaje de tiempo que el empleado trabaja de manera remota. Un valor del 100% indicaría que el empleo es completamente remoto, mientras que un valor del 0% indicaría un empleo presencial. |
Entero |
Salary_USD |
Salario anual del empleado expresado en dólares estadounidenses (USD). Este valor es útil para hacer comparaciones salariales entre empleos de diferentes ubicaciones y niveles de experiencia. |
Float |
import pandas as pd
import numpy as np
# Cargar el archivo CSV proporcionado
file_path = '_data/dataviz/ds_salaries.csv'
df = pd.read_csv(file_path)
# Informacion del dataset
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 607 entries, 0 to 606
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Working_Year 595 non-null float64
1 Designation 600 non-null object
2 Experience 607 non-null object
3 Employment_Status 607 non-null object
4 Employee_Location 607 non-null object
5 Company_Size 607 non-null object
6 Remote_Working_Ratio 607 non-null int64
7 Salary_USD 597 non-null float64
dtypes: float64(2), int64(1), object(5)
memory usage: 38.1+ KB
# Copia del dataset
df_va = df.copy()
# Revisar los datos faltantes en el conjunto de datos
missing_values = df_va .isnull().sum()
# Calcular el porcentaje de valores faltantes
missing_percentage = (df_va .isnull().mean() * 100).round(2)
# Crear un DataFrame con el análisis de datos faltantes
missing_data = pd.DataFrame({
'Valores Faltantes': missing_values,
'Porcentaje Faltante (%)': missing_percentage
})
missing_data
| Valores Faltantes | Porcentaje Faltante (%) | |
|---|---|---|
| Working_Year | 12 | 1.98 |
| Designation | 7 | 1.15 |
| Experience | 0 | 0.00 |
| Employment_Status | 0 | 0.00 |
| Employee_Location | 0 | 0.00 |
| Company_Size | 0 | 0.00 |
| Remote_Working_Ratio | 0 | 0.00 |
| Salary_USD | 10 | 1.65 |
Hay varias formas de tratar los datos faltantes. Como modo de ejemplo, se explicaran los procesos de cada uno.
Eliminación de datos faltantes#
La estrategia eliminar valores faltantes implica quitar aquellas filas de tu conjunto de datos que contienen valores faltantes (NaN). Sin embargo, para evitar eliminar demasiados datos y reducir significativamente el tamaño del conjunto de datos, esta estrategia se aplica solo cuando los valores faltantes constituyen un pequeño porcentaje del total, en este caso, el 5% o menos.
5% o menos del total de valores: Si la cantidad de valores faltantes en una columna o fila representa el 5% o menos del total de registros, es una pérdida aceptable de información. Por lo tanto, puedes eliminar esas filas o columnas sin comprometer demasiado la integridad del análisis.
# Coloquemos un umbral
umbral = len(df_va) * 0.05
# Selecciona columnas con valores faltantes <= umbral
cols_to_drop = df_va .columns[df_va .isna().sum() <= umbral]
# Elimina filas con valores faltantes en las columnas seleccionadas
df_va.dropna(subset=cols_to_drop, inplace=True)
# informacion del dataset ajustado
df_va .info()
<class 'pandas.core.frame.DataFrame'>
Index: 578 entries, 0 to 606
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Working_Year 578 non-null float64
1 Designation 578 non-null object
2 Experience 578 non-null object
3 Employment_Status 578 non-null object
4 Employee_Location 578 non-null object
5 Company_Size 578 non-null object
6 Remote_Working_Ratio 578 non-null int64
7 Salary_USD 578 non-null float64
dtypes: float64(2), int64(1), object(5)
memory usage: 40.6+ KB
# Revisar los datos faltantes en el conjunto de datos
missing_values = df_va.isnull().sum()
# Calcular el porcentaje de valores faltantes
missing_percentage = (df_va.isnull().mean() * 100).round(2)
# Crear un DataFrame con el análisis de datos faltantes
missing_data = pd.DataFrame({
'Valores Faltantes': missing_values,
'Porcentaje Faltante (%)': missing_percentage
})
missing_data
| Valores Faltantes | Porcentaje Faltante (%) | |
|---|---|---|
| Working_Year | 0 | 0.0 |
| Designation | 0 | 0.0 |
| Experience | 0 | 0.0 |
| Employment_Status | 0 | 0.0 |
| Employee_Location | 0 | 0.0 |
| Company_Size | 0 | 0.0 |
| Remote_Working_Ratio | 0 | 0.0 |
| Salary_USD | 0 | 0.0 |
Otra forma para eliminar los datos faltantes es:
# copia del dataset
df_2 = df.copy()
# eliminar las filas con Nans
df_clean = df_2.dropna(axis=0)
df_clean.info()
<class 'pandas.core.frame.DataFrame'>
Index: 578 entries, 0 to 606
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Working_Year 578 non-null float64
1 Designation 578 non-null object
2 Experience 578 non-null object
3 Employment_Status 578 non-null object
4 Employee_Location 578 non-null object
5 Company_Size 578 non-null object
6 Remote_Working_Ratio 578 non-null int64
7 Salary_USD 578 non-null float64
dtypes: float64(2), int64(1), object(5)
memory usage: 40.6+ KB
# Revisar los datos faltantes en el conjunto de datos
missing_values = df_clean.isnull().sum()
# Calcular el porcentaje de valores faltantes
missing_percentage = (df_clean.isnull().mean() * 100).round(2)
# Crear un DataFrame con el análisis de datos faltantes
missing_data = pd.DataFrame({
'Valores Faltantes': missing_values,
'Porcentaje Faltante (%)': missing_percentage
})
missing_data
| Valores Faltantes | Porcentaje Faltante (%) | |
|---|---|---|
| Working_Year | 0 | 0.0 |
| Designation | 0 | 0.0 |
| Experience | 0 | 0.0 |
| Employment_Status | 0 | 0.0 |
| Employee_Location | 0 | 0.0 |
| Company_Size | 0 | 0.0 |
| Remote_Working_Ratio | 0 | 0.0 |
| Salary_USD | 0 | 0.0 |
Hay una diferencia muy importante entre ambos método; el primer enfoque es más flexible y te permite manejar datos faltantes de manera más controlada, mientras que el segundo es más directo y elimina todas las filas que tengan valores faltantes.
Comparación de variables con la original y la data limpia#
Si nosotros realizamos la eliminación de datos o la imputación de datos debemos tener en cuenta que las distribuciones no cambien.
Datos categóricos#
import pandas as pd
import matplotlib.pyplot as plt
# Contar frecuencias en cada DataFrame
original_counts = df['Experience'].value_counts()
clean_counts = df_clean['Experience'].value_counts()
# Unir ambos conteos en un solo DataFrame
comparison = pd.concat(
[original_counts.rename("Original"), clean_counts.rename("Limpio")],
axis=1
).fillna(0)
# Crear gráfico de barras agrupado
comparison.plot(kind='bar', figsize=(10, 6))
plt.title("Comparación de valores en 'Experience' antes y después de eliminar NaN")
plt.xlabel("Nivel de experiencia")
plt.ylabel("Frecuencia")
plt.xticks(rotation=45)
plt.legend(title="Dataset")
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.show()
Visualmente vemos que no hay diferencias entre las proporciones, pero para estar más seguros realizaremos una prueba estadística \(\chi^2\)
from scipy.stats import chi2_contingency
# Alinear categorías para evitar errores
comparison_table = pd.concat([original_counts, clean_counts], axis=1, keys=['Original', 'Limpio']).fillna(0)
# Prueba chi-cuadrado
chi2, p, dof, expected = chi2_contingency(comparison_table.T)
# Mostrar resultados
print("Prueba Chi-cuadrado de homogeneidad")
print("Estadístico chi²:", chi2)
print("Grados de libertad:", dof)
print("Valor p:", p)
if p < 0.05:
print("✅ Existen diferencias significativas en las proporciones entre los dos conjuntos.")
else:
print("❌ No se encontraron diferencias significativas en las proporciones entre los dos conjuntos.")
Prueba Chi-cuadrado de homogeneidad
Estadístico chi²: 0.07153479107112373
Grados de libertad: 3
Valor p: 0.9950192620159025
❌ No se encontraron diferencias significativas en las proporciones entre los dos conjuntos.
Datos numérico#
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
# Ignorar warnings
warnings.filterwarnings("ignore", category=FutureWarning)
# Crear gráfico de densidad
plt.figure(figsize=(10, 6))
sns.kdeplot(df['Remote_Working_Ratio'], label='Original', linewidth=2)
sns.kdeplot(df_clean['Remote_Working_Ratio'], label='Limpio', linewidth=2, linestyle='--')
plt.title("Distribución de 'Remote_Working_Ratio' antes y después de eliminar NaN")
plt.xlabel("Remote Working Ratio")
plt.ylabel("Densidad")
plt.legend(title="Dataset")
plt.grid(alpha=0.3)
plt.show()
Al ver las gráficas de densidad no se ven cambios en las curvas. Pero hagamos una prueba de Kolmogorov para ver si hay diferencia o no en las distribuciones
# PRUEBA KS
from scipy.stats import ks_2samp
stat, p_value = ks_2samp(df['Remote_Working_Ratio'], df_clean['Remote_Working_Ratio'])
print("Prueba Kolmogorov-Smirnov (KS) para 'Remote_Working_Ratio' ")
print("Estadístico KS:", stat)
print("Valor p:", p_value)
if p_value < 0.05:
print("✅ Hay diferencias significativas entre las dos distribuciones.")
else:
print("❌ No hay evidencia significativa de que las distribuciones sean diferentes.")
Prueba Kolmogorov-Smirnov (KS) para 'Remote_Working_Ratio'
Estadístico KS: 0.0018469641951169458
Valor p: 1.0
❌ No hay evidencia significativa de que las distribuciones sean diferentes.
Imputación de datos faltantes#
Datos categóricos#
Para los datos categóricos (como “País”, “Categoría”, “Género”, etc.), los métodos comunes de imputación incluyen:
Moda: Imputar los valores faltantes con la categoría que más se repite. Este enfoque es simple y suele ser efectivo cuando los datos tienen una categoría predominante.
Asignación aleatoria: Reemplazar los valores faltantes con una categoría seleccionada al azar entre los valores no faltantes de la misma variable, preservando la distribución original de las categorías.
Imputación basada en subgrupos: Imputar los valores faltantes basándose en subgrupos definidos por otras variables, asegurando que la categoría imputada sea coherente con los datos restantes.
Usando la moda:
# Copia del dataset
df_3 = df.copy()
# Iterar sobre cada columna del DataFrame
for col in df_3.columns:
# Comprobar si la columna es de tipo objeto (categórica)
if df_3[col].dtypes == 'object':
# Imputar los valores faltantes con la moda (categoría más frecuente)
df_3[col].fillna(df_3[col].value_counts().index[0], inplace=True)
Usando Asignación aleatoria:
# Copia del dataset
df_4 = df.copy()
# Iterar sobre cada columna de df_4
for col in df_4.columns:
# Comprobar si la columna es de tipo objeto (categórica)
if df_4[col].dtypes == 'object':
# Obtener las categorías no faltantes de la columna
categorias_no_nan = df_4[col].dropna().unique()
# Reemplazar valores faltantes con una categoría seleccionada aleatoriamente
df_4[col] = df_4[col].apply(lambda x: np.random.choice(categorias_no_nan) if pd.isna(x) else x)
Usando imputación basada en grupos
# Copia del dataset
df_5 = df.copy()
# Definir la columna por la cual agrupar (por ejemplo, "Experience")
grupo_col = 'Experience'
# Iterar sobre cada columna de df_4
for col in df_5.columns:
# Comprobar si la columna es de tipo objeto (categórica) y no es la columna por la cual agrupamos
if df_5[col].dtypes == 'object' and col != grupo_col:
# Agrupar por la columna definida (en este caso 'Experience') e imputar valores faltantes
df_5[col] = df_5.groupby(grupo_col)[col].transform(
lambda x: x.fillna(np.random.choice(x.dropna().unique())) if x.isna().sum() > 0 else x
)
Datos numéricos#
Para los datos numéricos (como “Edad”, “Salario”, “Ingreso”, etc.), los métodos comunes de imputación incluyen:
Media: Reemplazar los valores faltantes con la media de la columna. Este método es útil cuando los datos tienen una distribución simétrica y no hay valores atípicos.
Mediana: Utilizar la mediana para imputar, lo cual es más robusto frente a valores atípicos o distribuciones asimétricas.
Imputación por métodos de machine learning: Estimar el valor faltante utilizando una regresión basada en otras variables del conjunto de datos. Este método es más avanzado y puede ser más preciso, pero requiere un modelo de regresión adecuado.
Para poder usar la imputación de datos con la media o la mediana, se debe conocer la distribución de los datos sin los vacios. Con lo anterior podemos ver si tiene comportamiento normal o no. De la distribución depende si imputar con el promedio o la mediana.
Usando la media o el promedio
# Copia del dataset
df_6 = df.copy()
# Imputar los valores perdidos con imputación de medias
df_6.fillna(df_6.select_dtypes(include=np.number).mean(), inplace=True)
Usando la mediana
# Copia del dataset
df_7 = df.copy()
# Imputar los valores perdidos con imputación de medias
df_7.fillna(df_7.select_dtypes(include=np.number).median(), inplace=True)
Otra manera adecuada de imputar valores ausentes es mediante la clase SimpleImputer de scikit-learn
# Libreria
from sklearn.impute import SimpleImputer
# Copia del dataset
df_8 = df.copy()
# Crear la instancia para imputar con la media
imputer = SimpleImputer(strategy='mean')
# Imputar los valores faltantes
df_8[['Working_Year','Salary_USD']] = imputer.fit_transform(df_8[['Working_Year','Salary_USD']])
df_8.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 607 entries, 0 to 606
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Working_Year 607 non-null float64
1 Designation 600 non-null object
2 Experience 607 non-null object
3 Employment_Status 607 non-null object
4 Employee_Location 607 non-null object
5 Company_Size 607 non-null object
6 Remote_Working_Ratio 607 non-null int64
7 Salary_USD 607 non-null float64
dtypes: float64(2), int64(1), object(5)
memory usage: 38.1+ KB
Tratamiento de datos atípicos#
Ell tratamiento de datos atípicos es fundamental para asegurar que las decisiones basadas en datos sean precisas y confiables, ya que los valores atípicos pueden distorsionar tanto las visualizaciones como los modelos predictivos utilizados en áreas clave, como la evaluación de riesgos, el análisis crediticio, y la detección de fraude.
La primera fase del tratamiento es entender los datos frente a ti: entender lo que es, lo que significa y lo que transmite. Solo cuando entiendas los datos serás capaz de diseñar una visualización que ayude a otros a entenderlos.
Además, es importante asegurarse de que los datos tienen sentido y contienen suficiente información, ya sea categórica, numérica o una mezcla de ambas, para ser visualizados. Por lo tanto, si tratas con datos erróneos o sucios, es probable que la visualización sea defectuosa.
¿Cómo evitar los errores más comunes al tratar con datos sucios?
Garbage In, Garbage Out: Este es un dicho popular en el campo de la ciencia de datos, especialmente con respecto a la visualización de datos. Básicamente, significa que si utilizas datos desordenados y ruidosos, vas a obtener una visualización defectuosa y poco informativa. Los datos desordenados, ruidosos y sucios corresponden a una serie de problemas que se encuentran en los datos.
Los valores atípicos en un conjunto de datos bancarios pueden surgir de varias fuentes. Algunos ejemplos incluyen:
Errores en la entrada de datos: Por ejemplo, si en la columna de saldo de una cuenta bancaria se registra \(1,000,000 en lugar de \)10,000, debido a un error tipográfico, esto es un valor atípico por error humano.
Auténticos valores atípicos: En contraste, un cliente VIP con un saldo bancario de $10,000,000 sería un valor atípico verdadero. Aunque este saldo es extremadamente alto en comparación con el promedio de los clientes, sigue siendo un valor realista para ciertas personas o instituciones bancarias.
Veamos un ejemplo de un error cometido al recoger o almacenar los datos. La siguiente tabla muestra la edad, el peso y el sexo de los clientes que acuden a un determinado gimnasio. La columna de sexo consta de tres valores discretos 0, 1 y 2 que corresponden todos a una clase: hombre, mujer y otro, respectivamente. La columna de la edad se expresa en años y la columna de peso está en kilogramos

Todo parece correcto hasta que llegamos a la cuarta instancia (índice 3), donde el peso es de 790 kg. Esto parece extraño porque nadie puede pesar realmente 790 kg, especialmente alguien cuya estatura es de 1,5 metros y 7 pulgadas. Quien haya almacenado estos datos debe haber querido decir 79 kg y haber añadido un 0 por error.
Este es un caso de un valor atípico en el conjunto de datos. Esto puede parecer trivial en este momento, sin embargo, esto puede resultar en visualizaciones y predicciones o patrones de modelos de aprendizaje automático defectuosos, especialmente si hay múltiples repeticiones de esos datos. Ahora, veamos un ejemplo de un auténtico valor atípico en la siguiente tabla.

El peso en el cuarto caso (índice 3) es de 167 kilogramos, lo que parece extrañamente alto. Sin embargo, sigue siendo un valor verosímil, ya que es posible que alguien tenga un peso de 167 kilogramos a los 37 años. Por lo tanto, se trata de un verdadero valor atípico. Mientras que en los ejemplos anteriores es fácil detectar el valor atípico, ya que solo hay 5 casos, en realidad, nuestros conjuntos de datos son masivos, por lo que comprobar cada caso es una tarea tediosa y poco práctica.
En consecuencia, en la vida real, podemos utilizar visualizaciones estáticas básicas, como los gráficos de caja, para observar la existencia de valores atípicos. Los gráficos de caja son visualizaciones de datos sencillas, pero informativas que pueden decirnos mucho sobre la forma en que se distribuyen nuestros datos. Muestran el rango de nuestros datos basándose en cinco valores clave:
El valor mínimo de la columna
El primer cuartil
La mediana
El tercer cuartil
El valor máximo de la columna
Esto es lo que hace que sean excelentes para mostrar los valores atípicos, además de describir la simetría de los datos, el grado de agrupación de los mismos (si todos los valores están repartidos en un amplio rango), y si están o no sesgados.
Vamos a crear un gráfico de caja para comprobar si nuestro conjunto de datos contiene valores atípicos. Vamos a utilizar el conjunto de datos gym.csv, que contiene información sobre los clientes de un determinado gimnasio.
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import plotly.express as px
Guarda el archivo gym.csv en un DataFrame llamado gym, e imprime las cinco primeras filas del mismo para ver cómo son los datos
gym = pd.read_csv('_data/dataviz/gym.csv')
gym.head()
| age | weight | sex | |
|---|---|---|---|
| 0 | 29 | 88 | 2 |
| 1 | 45 | 96 | 1 |
| 2 | 35 | 91 | 0 |
| 3 | 37 | 790 | 1 |
| 4 | 27 | 62 | 0 |
Como puede ver, nuestros datos tienen tres columnas: age, weight, sex. La columna sexo consta de tres valores discretos que corresponden a tres clases discretas: 0 es hombre, 1 es mujer y 2 es otro.
Cree un gráfico de caja con el eje \(x\) como columna de sexo y el eje \(y\) como peso:
# Importar plotly
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
# Configurar el renderizador para notebook o Jupyter Book
pio.renderers.default = 'notebook'
fig = px.box(gym, x = 'sex', y = 'weight', notched = False)
fig.show()
La escala del eje \(y\) es extrañamente grande, ya que todos los gráficos de caja están comprimidos en la octava parte inferior del gráfico, por lo que no representa una visualización clara de los datos. Esto se debe al valor atípico en la cuarta instancia de nuestro DataFrame 790 kg
Todos los valores parecen estar bien, excepto ese valor atípico en la parte superior del gráfico con max=790. Ahora, veremos las formas de tratar los valores atípicos.
Detección de valores atípicos#
Si nuestro conjunto de datos es pequeño, podemos detectar el valor atípico simplemente mirando el conjunto de datos. Pero si tenemos un conjunto de datos enorme, ¿cómo podemos identificar los valores atípicos? Tenemos que utilizar técnicas de visualización y matemáticas. A continuación se presentan algunas de las técnicas de detección de valores atípicos
Puntuación \(Z\)
Rango intercuantil (IQR)
Detección de valores atípicos mediante las puntuaciones \(Z\)#
Datos cuyo valor absoluto de \(Z\)-score supere 3 son considerados valores atípicos.

Recorrer todos los datos y calcular la puntuación \(Z\) mediante la fórmula \((X_i-\mu)/\sigma\). Definir un valor de umbral de 3 y marcar los puntos de datos cuyo valor absoluto de puntuación \(Z\) sea mayor que el umbral como valores atípicos. Recuerde que por el Teorema de Chebyshev un 99.7% de los datos están contenidos en el intervalo \(\mu\pm 3\sigma\).
# Lista para almacenar los valores atípicos detectados
outliers = []
# Función para detectar valores atípicos usando la puntuación Z (Z-score)
def detect_outliers_zscore(data):
thres = 3 # Umbral de Z-score para considerar un valor como atípico
mean = np.mean(data) # Calcular la media del conjunto de datos
std = np.std(data) # Calcular la desviación estándar del conjunto de datos
# Iterar a través de los valores del conjunto de datos
for x in data:
z_score = (x - mean) / std # Calcular la puntuación Z de cada valor
# Si el valor absoluto del Z-score es mayor que el umbral, es un valor atípico
if np.abs(z_score) > thres:
outliers.append(x) # Añadir el valor atípico a la lista de outliers
return outliers # Retornar la lista de valores atípicos
# Aplicar la función a la columna 'weight' del DataFrame gym
sample_outliers = detect_outliers_zscore(gym.weight)
# Imprimir los valores atípicos detectados usando el método de Z-score en español
print("Valores atípicos detectados por el método de Z-score:", sample_outliers)
Valores atípicos detectados por el método de Z-score: [790]
Detección de valores atípicos mediante el rango intercuantil (IQR)#
Valores fuera de 1.5 veces el rango intercuartil son atípicos.
# Lista para almacenar los valores atípicos detectados
outliers = []
# Función para detectar valores atípicos utilizando el rango intercuartil (IQR)
def detect_outliers_iqr(data):
data = sorted(data) # Ordenar los datos de menor a mayor
q1 = np.percentile(data, 25) # Calcular el primer cuartil (Q1)
q3 = np.percentile(data, 75) # Calcular el tercer cuartil (Q3)
IQR = q3 - q1 # Calcular el rango intercuartil (IQR)
# Definir los límites inferior y superior para identificar atípicos
lwr_bound = q1 - 1.5 * IQR # Límite inferior
upr_bound = q3 + 1.5 * IQR # Límite superior
# Iterar a través de los datos para detectar valores fuera de los límites
for x in data:
if x < lwr_bound or x > upr_bound: # Si el valor está fuera de los límites, es un valor atípico
outliers.append(x) # Añadir el valor atípico a la lista
return outliers # Retornar la lista de valores atípicos
# Aplicar la función a la columna 'weight' del DataFrame gym
sample_outliers = detect_outliers_iqr(gym.weight)
# Imprimir los valores atípicos detectados utilizando el método del rango intercuartil (IQR)
print("Valores atípicos detectados por el método del IQR:", sample_outliers)
Valores atípicos detectados por el método del IQR: [790]
Despues de haber detectado el valor atípico, tenemos dos procesos para el tratamiento: eliminación e imputación
Tratamiento#
Eliminación de datos atípicos#
Eliminaremos el dato que contiene el valor atípico del conjunto de datos utilizado previamente y, a continuación, visualizaremos el nuevo conjunto de datos mediante un gráfico de caja actualizado.
# Cargamos la data
gym = pd.read_csv('_data/dataviz/gym.csv')
gym.head()
| age | weight | sex | |
|---|---|---|---|
| 0 | 29 | 88 | 2 |
| 1 | 45 | 96 | 1 |
| 2 | 35 | 91 | 0 |
| 3 | 37 | 790 | 1 |
| 4 | 27 | 62 | 0 |
gym.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 543 entries, 0 to 542
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 age 543 non-null int64
1 weight 543 non-null int64
2 sex 543 non-null int64
dtypes: int64(3)
memory usage: 12.9 KB
Modificar el DataFrame del gimnasio para que solo esté formado por los casos en los que el peso sea inferior a 104 e imprimir las cinco primeras filas:
gym_clean = gym[gym.weight < 104]
gym_clean.head()
| age | weight | sex | |
|---|---|---|---|
| 0 | 29 | 88 | 2 |
| 1 | 45 | 96 | 1 |
| 2 | 35 | 91 | 0 |
| 4 | 27 | 62 | 0 |
| 5 | 58 | 55 | 0 |
gym_clean.info()
<class 'pandas.core.frame.DataFrame'>
Index: 542 entries, 0 to 542
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 age 542 non-null int64
1 weight 542 non-null int64
2 sex 542 non-null int64
dtypes: int64(3)
memory usage: 16.9 KB
Vamos a crear un boxplot para ver el aspecto de los datos
# Crear el diagrama de violín
fig = px.violin(gym_clean, x='sex', y='weight', box=True, points=None)
# Configurar fondo blanco y rejilla gris
fig.update_layout(
paper_bgcolor='white', # Fondo del gráfico
plot_bgcolor='white', # Fondo de la parte de trazado
# xaxis=dict(showgrid=True, gridcolor='lightgrey'), # Rejilla gris en el eje x
yaxis=dict(showgrid=True, gridcolor='lightgrey') # Rejilla gris en el eje y
)
# Actualizar el color de la caja a rojo y agregar línea del promedio más gruesa
fig.update_traces(
box=dict(line_color='red'), # Color de la caja (rojo)
meanline_visible=True, # Mostrar línea del promedio
meanline=dict(color='darkgreen', width=3) # Línea del promedio más gruesa y en rojo
)
# Mostrar el gráfico
fig.show()
Otra forma de eliminar los datos atípicos es:
gym.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 543 entries, 0 to 542
Data columns (total 3 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 age 543 non-null int64
1 weight 543 non-null int64
2 sex 543 non-null int64
dtypes: int64(3)
memory usage: 12.9 KB
# Función para eliminar outliers usando el rango intercuartílico (IQR)
def remove_outliers_iqr(df):
Q1 = df.quantile(0.25)
Q3 = df.quantile(0.75)
IQR = Q3 - Q1
# Filtrar outliers
df_filtered = df[~((df < (Q1 - 1.5 * IQR)) | (df > (Q3 + 1.5 * IQR))).any(axis=1)]
return df_filtered
# Se aplica la función de filtro de outliers sin incluir la variable objetivo
gym_cleaned = remove_outliers_iqr(gym.drop(columns=['sex']))
# Se vuelve a añadir la variable objetivo
gym_cleaned['sex'] = gym['sex'].loc[gym_cleaned.index]
print("Forma de datos originales: ", gym.shape)
print("Forma de datos sin outliers: ",gym_cleaned.shape)
Forma de datos originales: (543, 3)
Forma de datos sin outliers: (542, 3)
Imputación de datos atípicos#
Veamos un diagrama de cajas y bigotes
# , Diagrama de cajas y bigotes del weight
fig = px.box(gym, y="weight", title="Weight")
# Configurar fondo blanco y rejilla gris
fig.update_layout(
paper_bgcolor='white', # Fondo del gráfico
plot_bgcolor='white', # Fondo de la parte de trazado
# xaxis=dict(showgrid=True, gridcolor='lightgrey'), # Rejilla gris en el eje x
yaxis=dict(showgrid=True, gridcolor='lightgrey') # Rejilla gris en el eje y
)
fig.show()
Al eliminar el dato atpico pudimos ver que los datos no tienen comportamiento normal, por lo que el valor medio es sensible a valores atípicos, se aconseja sustituirlos por la mediana.
# Calcular la mediana de la columna 'weight'
mediana_weight = gym['weight'].median()
# Encontrar y reemplazar los valores atípicos
Q1 = gym['weight'].quantile(0.25)
Q3 = gym['weight'].quantile(0.75)
IQR = Q3 - Q1
limite_inferior = Q1 - 1.5 * IQR
limite_superior = Q3 + 1.5 * IQR
# Copia de datos
gym_cleaned_2 = gym.copy()
# Reemplazar los valores atípicos con la mediana
gym_cleaned_2['weight'] = gym['weight'].apply(lambda x: mediana_weight if x < limite_inferior or x > limite_superior else x)
Veamos el diagrama de cajas y bigotes de weight según el sexo
fig2 = px.box(gym_cleaned_2, x = 'sex', y = 'weight', notched = True)
# Configurar fondo blanco y rejilla gris
fig2.update_layout(
paper_bgcolor='white', # Fondo del gráfico
plot_bgcolor='white', # Fondo de la parte de trazado
# xaxis=dict(showgrid=True, gridcolor='lightgrey'), # Rejilla gris en el eje x
yaxis=dict(showgrid=True, gridcolor='lightgrey') # Rejilla gris en el eje y
)
fig2.show()
Imputación múltiple iterativa (MICE) e IterativeImputer#
Ahora contamos con una visualización sin valores perdidos, lo que significa que representa todas las instancias presentes en el conjunto de datos. Con esto resuelto, pasemos al tercer problema que puede generar una visualización defectuosa: la imputación.
Además de las técnicas vistas en R, existen otras alternativas de imputación que pueden integrarse en flujos de trabajo con Python. Por ejemplo, en Python puede aplicarse un enfoque tipo MICE (Multiple Imputation by Chained Equations) mediante implementaciones disponibles en sklearn.impute, particularmente con IterativeImputer. Este método es muy usado y conveniente para experimentación y análisis, porque permite imputar valores faltantes utilizando modelos predictivos y la información contenida en las demás variables.
¿Cómo funciona la imputación múltiple iterativa? (idea general)#
El principio de MICE (y de la imputación iterativa en general) es rellenar los valores faltantes de una variable usando al resto como predictores, repitiendo este proceso varias veces hasta estabilizar las imputaciones.
Imputación inicial
Se completa el conjunto de datos con una estrategia básica (por ejemplo, media o mediana) para obtener una primera versión sin vacíos.Ecuaciones encadenadas (un modelo por variable)
Para cada variable con valores faltantes \(X_j\):Se usan las filas donde \(X_j\) sí está observada para entrenar un modelo.
Se emplean las demás variables \(X_{-j}\) como predictores.
Con el modelo entrenado, se predicen los valores faltantes de \(X_j\))$ y se reemplazan.
Iteraciones (refinamiento progresivo)
El proceso se repite recorriendo todas las variables con faltantes durante varias rondas (iteraciones) porque las imputaciones de una variable influyen en la calidad de imputación de las otras.Resultado final
Se obtiene un conjunto de datos completo con imputaciones que tienden a ser más coherentes con los patrones del dataset que métodos simples (como imputar con un valor fijo).
Nota
En Python, IterativeImputer implementa esta lógica de ecuaciones encadenadas, actualizando los valores faltantes de forma iterativa.
¿Qué significa “múltiple” en imputación múltiple?#
En MICE clásico, la palabra múltiple significa que se generan varios conjuntos imputados (por ejemplo, 5 o 10), incorporando la incertidumbre del proceso de imputación. Luego, los análisis se realizan sobre cada conjunto y se combinan los resultados.
En IterativeImputer, es posible aproximar este enfoque cuando se introduce aleatoriedad (por ejemplo, con sample_posterior=True y distintas semillas), permitiendo generar múltiples versiones imputadas del mismo dataset.

La figura de imputación múltiple ilustra el flujo general:
Se parte de un conjunto con valores faltantes.
Se hace una imputación inicial.
Se imputan variables una por una, cada una usando a las demás como soporte.
El ciclo se repite en varias iteraciones hasta estabilizar los valores imputados.
Esto resume exactamente el enfoque de ecuaciones encadenadas: modelo por variable + actualización iterativa.
Emparejamiento predictivo medio (PMM)#

La figura de PMM (Predictive Mean Matching) representa una estrategia muy común dentro de MICE (especialmente en R). A diferencia de imputar el valor predicho directamente, PMM busca imputaciones más realistas:
Se ajusta un modelo para la variable con faltantes.
Para cada valor faltante, se obtiene su predicción.
Se identifican casos observados con predicciones similares (vecinos).
Se imputa el valor faltante usando un valor real observado de esos vecinos.
Ventajas típicas de PMM:
Imputa valores “posibles” (observados): al tomar valores reales de donantes, evita resultados imposibles (p. ej., edades negativas) y respeta la naturaleza discreta de algunas variables.
Robustez ante no normalidad y asimetrías: suele funcionar mejor que imputar con la predicción directa cuando la variable está sesgada o tiene colas largas.
Menos sensibilidad a mala especificación del modelo: si el modelo lineal no captura perfectamente la relación, PMM puede ser más estable porque no usa el valor predicho tal cual, sino un donante cercano.
Preserva mejor relaciones bivariadas: tiende a mantener asociaciones entre variables (correlaciones/gradientes) sin “aplanarlas” tanto como la imputación por media.
Conserva la variabilidad (no “sobre-suaviza”): la imputación por predicción directa puede reducir la varianza; PMM suele mantenerla mejor al usar valores reales distintos.
Respeta límites y reglas implícitas: si los datos observados ya cumplen restricciones (mínimos/máximos, valores enteros, etc.), PMM hereda esas restricciones.
Útil con variables semi-continuas: por ejemplo, variables con muchos ceros y valores positivos (gasto, visitas), donde imputar con una predicción continua puede crear valores raros.
Mejor comportamiento con outliers: al depender de “vecinos” en el espacio de predicción, puede evitar imputaciones extremadamente grandes/pequeñas que un modelo podría extrapolar.
Interpretabilidad: es fácil explicar el criterio: “se imputa con un caso real parecido”, lo que ayuda en reportes y auditorías.
Compatible con imputación múltiple: al repetir el proceso con aleatoriedad (elección de donantes/vecinos), facilita reflejar incertidumbre entre datasets imputados.
Nota
IterativeImputer no implementa PMM de forma idéntica a mice en R, pero sí comparte la estructura MICE de ecuaciones encadenadas. PMM sería una “regla de imputación” específica dentro de ese marco.
Puente hacia la implementación en R#
Con esto, aunque IterativeImputer ofrece una implementación práctica en Python para imputación iterativa, el análisis continuará implementando este enfoque en R, donde MICE y estrategias como PMM suelen utilizarse para obtener imputaciones más plausibles y robustas.
Aplicación de IterativeImputer#
En este ejemplo trabajaremos con el conjunto de datos Horse Colic, que recoge características médicas de caballos con cólico y el desenlace del caso. El archivo contiene 300 filas y 26 variables de entrada, además de una variable de salida. En términos prácticos, es una tarea de clasificación, porque buscamos predecir el resultado del caso.
Aunque existen varias variables que podríamos usar como objetivo, aquí nos enfocaremos en predecir si el caballo requirió cirugía o no (columna 23). Por lo tanto, el problema se formula como una clasificación binaria.
La variable objetivo es:
23: outcome¿Qué ocurrió finalmente con el caballo?
Valores posibles:
1= lived (sobrevivió)2= died (murió)3= was euthanized (fue sacrificado)
Un punto importante de este conjunto de datos es que presenta muchos valores faltantes en varias columnas. En el archivo original, estos faltantes aparecen marcados con un signo de interrogación: ?.
Para cargar los datos de forma correcta, podemos usar
readr::read_csv()e indicar que?debe interpretarse como dato faltante mediante el argumentona_values. Así, los faltantes quedarán registrados comoNAdentro de R.
import pandas as pd
import numpy as np
url = "https://raw.githubusercontent.com/cdeoroaguado/Datos/refs/heads/main/dataviz/horse-colic.csv"
df = pd.read_csv(url, header=None, na_values='?')
Muestrame las 5 primeras filas del dataset
df.head()
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2.0 | 1 | 530101 | 38.5 | 66.0 | 28.0 | 3.0 | 3.0 | NaN | 2.0 | ... | 45.0 | 8.4 | NaN | NaN | 2.0 | 2 | 11300 | 0 | 0 | 2 |
| 1 | 1.0 | 1 | 534817 | 39.2 | 88.0 | 20.0 | NaN | NaN | 4.0 | 1.0 | ... | 50.0 | 85.0 | 2.0 | 2.0 | 3.0 | 2 | 2208 | 0 | 0 | 2 |
| 2 | 2.0 | 1 | 530334 | 38.3 | 40.0 | 24.0 | 1.0 | 1.0 | 3.0 | 1.0 | ... | 33.0 | 6.7 | NaN | NaN | 1.0 | 2 | 0 | 0 | 0 | 1 |
| 3 | 1.0 | 9 | 5290409 | 39.1 | 164.0 | 84.0 | 4.0 | 1.0 | 6.0 | 2.0 | ... | 48.0 | 7.2 | 3.0 | 5.3 | 2.0 | 1 | 2208 | 0 | 0 | 1 |
| 4 | 2.0 | 1 | 530255 | 37.3 | 104.0 | 35.0 | NaN | NaN | 6.0 | 2.0 | ... | 74.0 | 7.4 | NaN | NaN | 2.0 | 2 | 4300 | 0 | 0 | 2 |
5 rows × 28 columns
Dame la información del dataset
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 300 entries, 0 to 299
Data columns (total 28 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 0 299 non-null float64
1 1 300 non-null int64
2 2 300 non-null int64
3 3 240 non-null float64
4 4 276 non-null float64
5 5 242 non-null float64
6 6 244 non-null float64
7 7 231 non-null float64
8 8 253 non-null float64
9 9 268 non-null float64
10 10 245 non-null float64
11 11 256 non-null float64
12 12 244 non-null float64
13 13 196 non-null float64
14 14 194 non-null float64
15 15 53 non-null float64
16 16 198 non-null float64
17 17 182 non-null float64
18 18 271 non-null float64
19 19 267 non-null float64
20 20 135 non-null float64
21 21 102 non-null float64
22 22 299 non-null float64
23 23 300 non-null int64
24 24 300 non-null int64
25 25 300 non-null int64
26 26 300 non-null int64
27 27 300 non-null int64
dtypes: float64(21), int64(7)
memory usage: 65.8 KB
Realiza un descriptivo de las variables
df.describe()
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 299.000000 | 300.000000 | 3.000000e+02 | 240.000000 | 276.000000 | 242.000000 | 244.000000 | 231.000000 | 253.000000 | 268.000000 | ... | 271.000000 | 267.000000 | 135.000000 | 102.000000 | 299.000000 | 300.000000 | 300.000000 | 300.000000 | 300.000000 | 300.000000 |
| mean | 1.397993 | 1.640000 | 1.085889e+06 | 38.167917 | 71.913043 | 30.417355 | 2.348361 | 2.017316 | 2.853755 | 1.305970 | ... | 46.295203 | 24.456929 | 2.037037 | 3.019608 | 1.551839 | 1.363333 | 3657.880000 | 90.226667 | 7.363333 | 1.670000 |
| std | 0.490305 | 2.173972 | 1.529801e+06 | 0.732289 | 28.630557 | 17.642231 | 1.045054 | 1.042428 | 1.620294 | 0.477629 | ... | 10.419335 | 27.475009 | 0.804905 | 1.968567 | 0.737187 | 0.481763 | 5399.513513 | 649.569234 | 127.536674 | 0.470998 |
| min | 1.000000 | 1.000000 | 5.184760e+05 | 35.400000 | 30.000000 | 8.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | ... | 23.000000 | 3.300000 | 1.000000 | 0.100000 | 1.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 1.000000 |
| 25% | 1.000000 | 1.000000 | 5.289040e+05 | 37.800000 | 48.000000 | 18.500000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | ... | 38.000000 | 6.500000 | 1.000000 | 2.000000 | 1.000000 | 1.000000 | 2111.750000 | 0.000000 | 0.000000 | 1.000000 |
| 50% | 1.000000 | 1.000000 | 5.303055e+05 | 38.200000 | 64.000000 | 24.500000 | 3.000000 | 2.000000 | 3.000000 | 1.000000 | ... | 45.000000 | 7.500000 | 2.000000 | 2.250000 | 1.000000 | 1.000000 | 2673.500000 | 0.000000 | 0.000000 | 2.000000 |
| 75% | 2.000000 | 1.000000 | 5.347275e+05 | 38.500000 | 88.000000 | 36.000000 | 3.000000 | 3.000000 | 4.000000 | 2.000000 | ... | 52.000000 | 57.000000 | 3.000000 | 3.900000 | 2.000000 | 2.000000 | 3209.000000 | 0.000000 | 0.000000 | 2.000000 |
| max | 2.000000 | 9.000000 | 5.305629e+06 | 40.800000 | 184.000000 | 96.000000 | 4.000000 | 4.000000 | 6.000000 | 3.000000 | ... | 75.000000 | 89.000000 | 3.000000 | 10.100000 | 3.000000 | 2.000000 | 41110.000000 | 7111.000000 | 2209.000000 | 2.000000 |
8 rows × 28 columns
Podemos enumerar cada columna y reportar el número de filas con valores faltantes para esa columna.
# Revisar los datos faltantes en el conjunto de datos
missing_values = df.isnull().sum()
# Calcular el porcentaje de valores faltantes
missing_percentage = (df.isnull().mean() * 100).round(2)
# Crear un DataFrame con el análisis de datos faltantes
missing_data = pd.DataFrame({
'Valores Faltantes': missing_values,
'Porcentaje Faltante (%)': missing_percentage
})
missing_data
| Valores Faltantes | Porcentaje Faltante (%) | |
|---|---|---|
| 0 | 1 | 0.33 |
| 1 | 0 | 0.00 |
| 2 | 0 | 0.00 |
| 3 | 60 | 20.00 |
| 4 | 24 | 8.00 |
| 5 | 58 | 19.33 |
| 6 | 56 | 18.67 |
| 7 | 69 | 23.00 |
| 8 | 47 | 15.67 |
| 9 | 32 | 10.67 |
| 10 | 55 | 18.33 |
| 11 | 44 | 14.67 |
| 12 | 56 | 18.67 |
| 13 | 104 | 34.67 |
| 14 | 106 | 35.33 |
| 15 | 247 | 82.33 |
| 16 | 102 | 34.00 |
| 17 | 118 | 39.33 |
| 18 | 29 | 9.67 |
| 19 | 33 | 11.00 |
| 20 | 165 | 55.00 |
| 21 | 198 | 66.00 |
| 22 | 1 | 0.33 |
| 23 | 0 | 0.00 |
| 24 | 0 | 0.00 |
| 25 | 0 | 0.00 |
| 26 | 0 | 0.00 |
| 27 | 0 | 0.00 |
Podemos observar que algunas columnas (por ejemplo, los índices de columna 1 y 2) no tienen valores faltantes, mientras que otras columnas (por ejemplo, los índices de columna 15 y 21) tienen muchos o incluso la mayoría de los valores faltantes.
Transformación de datos con IterativeImputer#
IterativeImputer realiza una imputación iterativa: en lugar de rellenar los faltantes de una sola vez, ajusta modelos sucesivos para estimar cada variable con valores perdidos a partir del resto de variables disponibles.
Por defecto, este método suele apoyarse en un modelo tipo BayesianRidge(), que aprende relaciones entre variables y usa esa información para proponer valores plausibles donde hay NA.
Un detalle clave del procedimiento es el orden de imputación: las variables se completan progresivamente, empezando por aquellas con menos valores faltantes y avanzando hacia las que tienen más. Esto ayuda a que, cuando se imputen las columnas más incompletas, ya exista información adicional imputada en las demás variables que puede mejorar la estimación.
from numpy import isnan
from pandas import read_csv
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imputer = IterativeImputer(n_nearest_features=None, imputation_order='ascending')
En IterativeImputer, el parámetro n_nearest_features = None se utiliza para definir cuántas variables “vecinas” se tendrán en cuenta al imputar un valor faltante.
Si se asigna un número (por ejemplo,
5), el método intentará estimar el faltante usando solo esas variables más relevantes.En cambio, cuando se deja en
None, se indica que el algoritmo utilizará todas las variables disponibles para calcular la imputación.
En la práctica, mantenerlo en None puede aprovechar más información del conjunto de datos, aunque también puede incrementar el costo computacional si hay muchas variables.
Tomemos
Xyy
data = df.values
ix = [i for i in range(data.shape[1]) if i != 23]
X, y = data[:, ix], data[:, 23]
print('Missing: %d' % sum(isnan(X).flatten()))
Missing: 1605
El proceso suele hacerse en dos pasos:
Ajuste (fit): el imputador se entrena con el conjunto de datos para aprender las relaciones entre variables.
Transformación (transform): luego, el imputador ya ajustado se aplica al mismo conjunto (u otro compatible) para generar una copia del dataset donde los valores faltantes de cada columna se reemplazan por valores estimados.
Nota
La clase IterativeImputer no siempre puede usarse “directamente” sin habilitarla, ya que se considera experimental en algunas versiones y requiere importarla desde el módulo correspondiente.
imputer = IterativeImputer()
imputer.fit(X)
Xtrans = imputer.transform(X)
print('Missing: %d' % sum(isnan(Xtrans).flatten()))
Missing: 0
IterativeImputer y evaluación de modelos#
Para aplicar correctamente la imputación iterativa y evitar la fuga de datos (data leakage), lo más recomendable es construir un pipeline donde el flujo sea siempre:
Primero: imputar los valores faltantes (dentro del proceso de entrenamiento).
Después: ajustar el modelo de predicción.
De esta forma, la imputación se aprende solo con los datos del entrenamiento en cada partición y luego se aplica al subconjunto de validación, manteniendo una evaluación más realista.
En Python, esto se implementa fácilmente con la clase Pipeline. Por ejemplo, se puede crear un pipeline que incluya IterativeImputer (con su configuración por defecto o una estrategia ajustada) y, a continuación, un modelo como un bosque aleatorio (Random Forest).
Finalmente, el rendimiento del modelo puede evaluarse comparando el pipeline sobre el conjunto de datos mediante validación cruzada repetida de 10 pliegues, lo cual ayuda a obtener estimaciones más estables del desempeño.
from numpy import mean
from numpy import std
from pandas import read_csv
from sklearn.ensemble import RandomForestClassifier
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.pipeline import Pipeline
model = RandomForestClassifier()
imputer = IterativeImputer()
pipeline = Pipeline(steps=[('i', imputer), ('m', model)])
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
scores = cross_val_score(pipeline, X, y, scoring='accuracy', cv=cv, n_jobs=-1, error_score='raise')
print('Mean Accuracy: %.3f (%.3f)' % (mean(scores), std(scores)))
Mean Accuracy: 0.868 (0.061)
Recuerda que
StratifiedKFoldayuda a que cada partición de entrenamiento y prueba conserve una distribución de clases similar.
EnRepeatedStratifiedKFold, el parámetron_repeats = 3indica que la validación cruzada estratificada se repite tres veces; en cada repetición se generan 10 particiones distintas (con diferente aleatoriedad), lo que da una evaluación más estable.En este caso,
error_score = "raise"significa que, si ocurre algún problema durante el ajuste o la evaluación en uno de los pliegues, el proceso no seguirá en silencio: se lanzará una excepción (Exception) y la validación se detendrá. Esto es útil para detectar fallas en los datos o en el pipeline (por ejemplo, columnas mal codificadas, valores fuera de rango o incompatibilidades del modelo).Ten en cuenta que los resultados pueden variar porque tanto el algoritmo como el procedimiento de evaluación tienen un componente aleatorio (y, a veces, también influyen detalles de precisión numérica). Por eso, es buena práctica ejecutar el experimento varias veces y reportar un promedio (o incluso un intervalo de variación).
Con esta configuración (10 pliegues × 3 repeticiones), el pipeline reporta una precisión promedio cercana al 86.3%, lo cual es un desempeño bastante competitivo.
Aun así, queda una pregunta importante: ¿la estrategia iterativa por defecto es realmente la mejor para este conjunto de datos? Para responderlo, conviene comparar distintas configuraciones de imputación (parámetros o modelos internos) y validar cuál ofrece el mejor equilibrio entre desempeño y estabilidad.
IterativeImputer y diferentes órdenes de imputación#
Por defecto, IterativeImputer completa los valores faltantes siguiendo un orden ascendente: comienza con la variable que tiene menos valores perdidos y avanza hasta la que tiene más.
Sin embargo, este orden no es el único posible. También podemos experimentar con otras estrategias para ver si el rendimiento del modelo mejora, por ejemplo:
Descendente: de la variable con más faltantes a la de menos.
De derecha a izquierda (árabe).
De izquierda a derecha (romano).
Aleatorio: el orden cambia según la semilla o el muestreo.
La idea es sencilla: el orden en que se imputan las columnas puede influir en la calidad de las estimaciones y, en consecuencia, en el desempeño final del modelo.
from numpy import mean
from numpy import std
from pandas import read_csv
from sklearn.ensemble import RandomForestClassifier
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.pipeline import Pipeline
from matplotlib import pyplot
results = list()
strategies = ['ascending', 'descending', 'roman', 'arabic', 'random']
for s in strategies:
pipeline = Pipeline(steps=[('i', IterativeImputer(imputation_order=s)), ('m', RandomForestClassifier())])
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
scores = cross_val_score(pipeline, X, y, scoring='accuracy', cv=cv, n_jobs=-1)
results.append(scores)
print('>%s %.3f (%.3f)' % (s, mean(scores), std(scores)))
>ascending 0.870 (0.053)
>descending 0.877 (0.051)
>roman 0.872 (0.056)
>arabic 0.874 (0.053)
>random 0.869 (0.049)
pyplot.boxplot(results, labels=strategies, showmeans=True)
pyplot.xticks(rotation=45)
pyplot.show()
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
# results: lista de arrays/listas (scores por estrategia)
# strategies: lista de nombres (mismo orden que results)
df_plot = pd.DataFrame({
"strategy": [s for s, scores in zip(strategies, results) for _ in scores],
"accuracy": [score for scores in results for score in scores]
})
# Boxplot (con puntos)
fig = px.box(
df_plot,
x="strategy",
y="accuracy",
points="all"
)
# Media por estrategia como MARCADOR (no línea)
means = df_plot.groupby("strategy", as_index=False)["accuracy"].mean()
fig.add_trace(
go.Scatter(
x=means["strategy"],
y=means["accuracy"],
mode="markers",
name="Media",
marker=dict(size=10, symbol="diamond")
)
)
fig.update_layout(
xaxis_title="Estrategia de imputación",
yaxis_title="Accuracy",
xaxis_tickangle=45
)
fig.show()
A partir del diagrama de cajas se observa que las diferencias entre estrategias son pequeñas: las medianas y los rangos intercuartílicos son muy similares en casi todos los casos, lo que sugiere un rendimiento bastante estable sin importar el orden de imputación.
Aun así, el orden descendente destaca ligeramente, ya que muestra la media (diamante rojo) más alta entre las opciones comparadas. Muy cerca aparecen los órdenes romano y árabe, que también mantienen una precisión promedio alrededor de 0.87 (≈ 87%).
En resumen, aunque no hay cambios drásticos, la gráfica sugiere que usar un orden distinto al predeterminado (ascendente), especialmente descendente, puede dar una mejora marginal en este conjunto de datos.
IterativeImputer y el número de iteraciones#
Por defecto, IterativeImputer realiza 10 iteraciones para completar los valores faltantes. En cada iteración, el método vuelve a estimar las columnas con datos perdidos usando la información del resto de variables.
Sin embargo, más iteraciones no siempre significan mejores resultados. En algunos casos, un número muy alto puede introducir sesgo, aumentar el ruido en las estimaciones o simplemente aportar mejoras mínimas a cambio de más tiempo de cómputo. Por eso, a veces conviene trabajar con pocas iteraciones.
El número de iteraciones se controla con el parámetro max_iter. En el siguiente ejemplo compararemos distintos valores de max_iter (por ejemplo, de 1 a 20) para observar cómo cambia el desempeño del modelo.
from numpy import mean
from numpy import std
from pandas import read_csv
from sklearn.ensemble import RandomForestClassifier
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.pipeline import Pipeline
from matplotlib import pyplot
results = list()
strategies = [str(i) for i in range(1, 21)]
for s in strategies:
pipeline = Pipeline(steps=[('i', IterativeImputer(max_iter=int(s))), ('m', RandomForestClassifier())])
cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
scores = cross_val_score(pipeline, X, y, scoring='accuracy', cv=cv, n_jobs=-1)
results.append(scores)
print('>%s %.3f (%.3f)' % (s, mean(scores), std(scores)))
>1 0.867 (0.050)
>2 0.866 (0.051)
>3 0.867 (0.054)
>4 0.870 (0.053)
>5 0.876 (0.052)
>6 0.873 (0.052)
>7 0.872 (0.050)
>8 0.871 (0.052)
>9 0.869 (0.055)
>10 0.877 (0.048)
>11 0.873 (0.053)
>12 0.869 (0.051)
>13 0.869 (0.056)
>14 0.871 (0.054)
>15 0.866 (0.056)
>16 0.864 (0.054)
>17 0.874 (0.050)
>18 0.864 (0.045)
>19 0.871 (0.055)
>20 0.872 (0.053)
pyplot.boxplot(results, labels=strategies, showmeans=True)
pyplot.xticks(rotation=45)
pyplot.show()
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
# results: lista de arrays/listas con los scores por cada max_iter
# strategies: ['1','2',...,'20']
df_plot = pd.DataFrame({
"max_iter": [s for s, scores in zip(strategies, results) for _ in scores],
"accuracy": [score for scores in results for score in scores]
})
# Boxplot + puntos
fig = px.box(
df_plot,
x="max_iter",
y="accuracy",
points="all"
)
# Media por max_iter como MARCADOR (no línea)
means = df_plot.groupby("max_iter", as_index=False)["accuracy"].mean()
fig.add_trace(
go.Scatter(
x=means["max_iter"],
y=means["accuracy"],
mode="markers",
name="Media",
marker=dict(size=9, symbol="diamond")
)
)
fig.update_layout(
xaxis_title="Número de iteraciones (max_iter)",
yaxis_title="Accuracy",
xaxis_tickangle=0
)
fig.show()
A partir del diagrama de cajas se aprecia que el desempeño cambia muy poco cuando aumentamos max_iter de 1 a 20: las medianas son similares y la dispersión de los puntajes se mantiene en rangos comparables.
De hecho, las pocas iteraciones (≈ 3–5) ya alcanzan una precisión promedio muy cercana al mejor resultado observado (marcado por los triángulos verdes), y en algunos casos incluso lo igualan o lo superan levemente. En contraste, usar valores más altos (por ejemplo, 9–12 iteraciones) no muestra una mejora clara y consistente en la precisión.
En resumen, para este conjunto de datos parece razonable preferir un número bajo de iteraciones, ya que ofrece un rendimiento similar con menor costo computacional.
IterativeImputer al hacer una predicción#
Una vez elegida la configuración final, podemos construir un pipeline que combine la imputación iterativa (IterativeImputer) con el modelo de bosque aleatorio (Random Forest). Esto permite que la preparación de datos y el entrenamiento queden integrados en un solo flujo.
El procedimiento típico es:
Definir el pipeline (imputador + modelo).
Ajustarlo con todos los datos disponibles (
fit).Usarlo para predecir nuevos casos con
predict.
Un detalle importante: cuando ingreses una nueva observación, cualquier valor desconocido o ausente debe indicarse como NaN. Así, el imputador podrá completar esos faltantes antes de que el modelo genere la predicción.
from numpy import nan
df = read_csv(url, header=None, na_values='?')
data = df.values
X, y = data[:, :-1], data[:, -1]
pipeline = Pipeline(steps=[('i', IterativeImputer()), ('m', RandomForestClassifier())])
pipeline.fit(X, y)
Pipeline(steps=[('i', IterativeImputer()), ('m', RandomForestClassifier())])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.
Pipeline(steps=[('i', IterativeImputer()), ('m', RandomForestClassifier())])IterativeImputer()
RandomForestClassifier()
row = [2, 1, 530101, 38.50, 66, 28, 3, 3, nan, 2, 5, 4, 4, nan, nan, nan, 3, 5, 45.00, 8.40, nan, nan, 2, 11300, 00000, 00000, 2]
yhat = pipeline.predict([row])
print('Predicted Class: %d' % yhat[0])
Predicted Class: 2
En tu caso (según el diccionario del dataset horse-colic):
1 = lived (sobrevivió)
2 = died (murió)
3 = was euthanized (fue sacrificado)
Así que 2 se interpreta como: el modelo predice que el caballo murió.
Instancias y/o características duplicadas#
Un tercer problema común al analizar datos es la presencia de instancias (filas) y/o características (columnas) duplicadas. En general, estos duplicados no aportan información nueva y, si no se detectan y eliminan, pueden distorsionar los patrones del conjunto de datos y llevar a conclusiones equivocadas en una visualización.
Por ejemplo, imagina que construyes una gráfica para explorar la relación entre el género de un adolescente y si toca el piano. Si el conjunto de datos incluye valores atípicos, errores, registros repetidos o faltantes, la visualización puede terminar mostrando una tendencia engañosa.
A partir de una gráfica así, podrías llegar a concluir que hay más mujeres que tocan el piano que hombres. Sin embargo, antes de aceptar esa interpretación, conviene revisar si la visualización proviene de un conjunto de datos con duplicados o problemas de calidad, como el que se muestra a continuación.

En este ejemplo aparecen dos registros para Nita Thadaka y tres registros para Pooja Rajesh. En total, esto implica tres instancias duplicadas (registros repetidos que no deberían contarse más de una vez).
Cuando hay duplicados, la visualización puede sobre-representar ciertos casos y terminar mostrando una “tendencia” que en realidad es producto de datos repetidos. Por eso, la lectura de la gráfica podría ser inexacta.
La forma más directa de manejar este problema suele ser eliminar los duplicados. En pandas, esto se puede hacer fácilmente con la función drop_duplicates().
Mala selección de características#
En un conjunto de datos, una característica es una columna (variable), mientras que una instancia es una fila (registro).
Por ejemplo, en una tabla donde aparecen nombre, sexo, toca_piano y edad, esas serían características; en cambio, un registro específico (por ejemplo, Pooja Rajesh, F, 15, yes) corresponde a una instancia.
Como el objetivo de una visualización es mostrar tendencias, patrones o relaciones entre variables, la selección de características debe hacerse con cuidado. Elegir variables poco relevantes puede producir gráficos “bonitos”, pero sin un mensaje claro o, peor aún, con interpretaciones engañosas.
Si lo que buscas es demostrar que existe una relación fuerte entre dos variables, conviene verificar primero que realmente estén asociadas (por ejemplo, mediante una medida de correlación o un análisis exploratorio). Si no hay relación, la visualización probablemente no aporte información útil.
Por ejemplo, en el conjunto de datos co2.csv, que incluye información de emisiones de CO₂ y PIB por país, tiene sentido revisar la relación entre ambas variables antes de graficarlas. Confirmar esa asociación antes de visualizar ayuda a asegurarnos de que estamos construyendo una gráfica que realmente valga la pena.
Visualización interactiva#
Las visualizaciones de datos interactivas superan a las estáticas al permitir una mayor participación del usuario. El término interactivo se refiere a la comunicación y colaboración entre dos o más entidades. En este contexto, las visualizaciones interactivas son representaciones gráficas de datos, ya sean estáticos o dinámicos, que permiten a los usuarios explorar, modificar y analizar la información en tiempo real, respondiendo a sus acciones de manera inmediata. Esta capacidad para adaptarse a las interacciones ofrece una comprensión más profunda y una toma de decisiones más ágil y efectiva.
Observación
Las visualizaciones de datos interactivas se destacan por las siguientes cualidades:
Facilidad de exploración: Permiten interactuar con los datos mediante ajustes como cambio de colores, parámetros y tipos de gráficos, lo que hace que la exploración de la información sea más intuitiva.
Manipulación instantánea: Los gráficos pueden modificarse en tiempo real frente al usuario. Por ejemplo, un deslizador interactivo permite ajustar un valor y observar cómo el gráfico cambia instantáneamente. También se pueden usar casillas de verificación para seleccionar y visualizar diferentes parámetros según sea necesario.
Acceso a datos en tiempo real: Las visualizaciones interactivas ofrecen información actualizada de inmediato, facilitando el análisis ágil de tendencias y patrones emergentes.
Mayor comprensión: Su facilidad de uso e interactividad permiten una interpretación más clara y rápida de los datos, ayudando a las organizaciones a tomar decisiones mejor fundamentadas.
Reducción de gráficos redundantes: Un solo gráfico interactivo puede reemplazar múltiples gráficos estáticos al proporcionar la capacidad de visualizar la misma información de diversas formas en un solo lugar.
Visualización de relaciones complejas: Facilitan la identificación de patrones y relaciones, como causas y efectos, lo que mejora el análisis de datos multidimensionales.
Estas características hacen que las visualizaciones interactivas sean herramientas poderosas para la toma de decisiones y el análisis de datos.
Aspecto claves de las visualizaciones interactivas#
El aspecto clave de las visualizaciones de datos interactivas es su capacidad de responder y adaptarse a las entradas del usuario en tiempo real o en intervalos muy cortos. A continuación, se exploran algunas de las principales entradas humanas, cómo se integran en las visualizaciones y el impacto que tienen en la comprensión de los datos:
Deslizador (
Slider): Permite al usuario ajustar un rango de valores y visualizar cómo el gráfico cambia dinámicamente en respuesta a esta acción. Esto es ideal para analizar datos en diferentes intervalos de tiempo o comparar categorías en tiempo real.
Hover: Al pasar el cursor sobre un elemento del gráfico, se muestra información adicional que no es visible en la visualización principal. Esto es útil para detallar valores precisos o proporcionar descripciones sin saturar el gráfico con demasiada información.
Zoom: La capacidad de acercar o alejar un gráfico interactivo es una característica integrada en muchas bibliotecas de visualización. Facilita el análisis detallado de puntos de interés en los datos y permite observar más de cerca las áreas que requieren mayor atención.
Parámetros clicables (
Clickable Parameters): Elementos como casillas de verificación o menús desplegables permiten al usuario seleccionar qué aspectos de los datos desea visualizar. Estas opciones brindan flexibilidad al usuario para personalizar su análisis de manera intuitiva.
Existen varias bibliotecas en Python que soportan estas funcionalidades interactivas, permitiendo que las visualizaciones de datos sean más dinámicas y receptivas a las entradas humanas. En capítulos anteriores, se introdujeron dos bibliotecas populares para visualizaciones estáticas:
MatplotlibSeaborn
Ambas son ampliamente usadas en la comunidad de visualización de datos para gráficos estáticos, como un gráfico de dispersión que muestra la relación entre dos variables.
Aunque matplotlib y seaborn son excelentes para visualizaciones estáticas, otras bibliotecas se especializan en características interactivas. Entre las más destacadas están:
BokehPlotly
Estas bibliotecas permiten la creación de visualizaciones interactivas que reaccionan a las entradas del usuario, enriqueciendo el análisis de datos. En los próximos ejercicios, exploraremos cómo utilizar tanto bokeh como plotly para diseñar visualizaciones interactivas de datos.
Preparación de nuestro dataset#
Descargaremos y prepararemos nuestro conjunto de datos utilizando las bibliotecas de Python pandas y numpy. Al final de este ejercicio, tendremos un DataFrame listo para la creación de nuestras visualizaciones de datos interactivas. Utilizaremos dos archivos de datos: co2.csv, que contiene información sobre las emisiones de dióxido de carbono por persona, año y país, y gapminder.csv, que registra el PIB por año y país.
Importar las bibliotecas necesarias:
import pandas as pd
import numpy as np
Utilizaremos el path único para cargar los archivos de datos directamente en DataFrames:
url_co2 = 'C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/co2.csv'
co2 = pd.read_csv(url_co2)
co2.head()
| country | 1800 | 1801 | 1802 | 1803 | 1804 | 1805 | 1806 | 1807 | 1808 | ... | 2005 | 2006 | 2007 | 2008 | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Afghanistan | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 0.0529 | 0.0637 | 0.0854 | 0.154 | 0.242 | 0.294 | 0.412 | 0.35 | 0.316 | 0.299 |
| 1 | Albania | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 1.3800 | 1.2800 | 1.3000 | 1.460 | 1.480 | 1.560 | 1.790 | 1.68 | 1.730 | 1.960 |
| 2 | Algeria | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 3.2200 | 2.9900 | 3.1900 | 3.160 | 3.420 | 3.300 | 3.290 | 3.46 | 3.510 | 3.720 |
| 3 | Andorra | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 7.3000 | 6.7500 | 6.5200 | 6.430 | 6.120 | 6.120 | 5.870 | 5.92 | 5.900 | 5.830 |
| 4 | Angola | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 0.9800 | 1.1000 | 1.2000 | 1.180 | 1.230 | 1.240 | 1.250 | 1.33 | 1.250 | 1.290 |
5 rows × 216 columns
url_gm = 'C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/gapminder.csv'
gm = pd.read_csv(url_gm)
gm.head()
| Country | Year | fertility | life | population | child_mortality | gdp | region | |
|---|---|---|---|---|---|---|---|---|
| 0 | Afghanistan | 1964 | 7.671 | 33.639 | 10474903.0 | 339.7 | 1182.0 | South Asia |
| 1 | Afghanistan | 1965 | 7.671 | 34.152 | 10697983.0 | 334.1 | 1182.0 | South Asia |
| 2 | Afghanistan | 1966 | 7.671 | 34.662 | 10927724.0 | 328.7 | 1168.0 | South Asia |
| 3 | Afghanistan | 1967 | 7.671 | 35.170 | 11163656.0 | 323.3 | 1173.0 | South Asia |
| 4 | Afghanistan | 1968 | 7.671 | 35.674 | 11411022.0 | 318.1 | 1187.0 | South Asia |
Creamos un nuevo
DataFramedf_gmque contenga solo las columnas Country y region, y eliminamos duplicados:
df_gm = gm[['Country', 'region']].drop_duplicates()
df_gm.head()
| Country | region | |
|---|---|---|
| 0 | Afghanistan | South Asia |
| 50 | Albania | Europe & Central Asia |
| 100 | Algeria | Middle East & North Africa |
| 150 | Angola | Sub-Saharan Africa |
| 200 | Antigua and Barbuda | America |
Utilice
.merge()para combinar el DataFrameco2con el DataFramedf_gm. Esta función función merge básicamente realiza una unión interna en los dos DataFrames (lo mismo como el inner join cuando se utiliza en bases de datos). Esta fusión es necesaria para garantizar que tanto el DataFrameco2como el DataFramegmestén formados por los mismos países, garantizando así que los valores de las emisiones de CO2 correspondan a sus respectivos países. La opciónhow ='inner'devuelve un marco de datos con sólo las filas que tienen características comunes. Una unión interna requiere que cada fila de los dos dataframes unidos tenga valores de columna que coincidan. Esto es similar a la intersección de dos conjuntos (ver Diferentes tipos de joins en pandas).Outer Join or Full outer join: Mantiene todas las filas de ambos marcos de datos, especifique:how='outer'Left Join or Left outer join: Para incluir todas las filas de su marco de datos \(X\) y sólo las de \(Y\) que coincidan, especifique:how='left'.Right Join or Right outer join: Para incluir todas las filas de su marco de datos \(Y\) y sólo las de \(X\) que coincidan, especifique:how='right'.
# Fusionamos el DataFrame 'co2' con el DataFrame 'df_gm' utilizando 'country' y 'Country' como claves
# La fusión es interna ('inner join') para mantener solo las filas comunes a ambos DataFrames
df_w_regions = pd.merge(co2, df_gm, left_on='country', right_on='Country', how='inner')
# Eliminamos la columna duplicada 'Country' ya que 'country' contiene la misma información
df_w_regions = df_w_regions.drop('Country', axis='columns')
# Mostramos las primeras 5 filas del DataFrame resultante para verificar la fusión
df_w_regions.head()
| country | 1800 | 1801 | 1802 | 1803 | 1804 | 1805 | 1806 | 1807 | 1808 | ... | 2006 | 2007 | 2008 | 2009 | 2010 | 2011 | 2012 | 2013 | 2014 | region | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Afghanistan | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 0.0637 | 0.0854 | 0.154 | 0.242 | 0.294 | 0.412 | 0.35 | 0.316 | 0.299 | South Asia |
| 1 | Albania | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 1.2800 | 1.3000 | 1.460 | 1.480 | 1.560 | 1.790 | 1.68 | 1.730 | 1.960 | Europe & Central Asia |
| 2 | Algeria | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 2.9900 | 3.1900 | 3.160 | 3.420 | 3.300 | 3.290 | 3.46 | 3.510 | 3.720 | Middle East & North Africa |
| 3 | Angola | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 1.1000 | 1.2000 | 1.180 | 1.230 | 1.240 | 1.250 | 1.33 | 1.250 | 1.290 | Sub-Saharan Africa |
| 4 | Antigua and Barbuda | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | 4.9100 | 5.1400 | 5.190 | 5.450 | 5.540 | 5.360 | 5.42 | 5.360 | 5.380 | America |
5 rows × 217 columns
A continuación, aplicaremos la función
.melt()al DataFrame y almacenaremos el resultado en un nuevo DataFrame llamadonew_co2. Esta función transforma el formato del DataFrame, reorganizando las columnas en variables identificadoras según nuestras necesidades. En este caso, utilizaremos las columnas de país y región como variables identificadoras, mientras que las columnas de años se convertirán en valores dentro de una nueva columna denominada year. Se reorganiza la estructura para facilitar su análisis.
# Aplicamos la función pd.melt() al DataFrame 'df_w_regions'
# 'id_vars' indica las columnas que queremos mantener como identificadoras (en este caso, 'country' y 'region')
# El resto de las columnas (años) se transforman en una sola columna 'variable' con sus respectivos valores en otra columna 'value'
new_co2 = pd.melt(df_w_regions, id_vars=['country', 'region'])
# Renombramos las columnas del DataFrame resultante para mayor claridad
# 'variable' ahora se llamará 'year' para representar los años, y 'value' se renombra a 'co2' para las emisiones de CO2
new_co2.columns = ['country', 'region', 'year', 'co2']
# muestra las 5 primeras
new_co2.tail(5)
| country | region | year | co2 | |
|---|---|---|---|---|
| 37190 | Vanuatu | East Asia & Pacific | 2014 | 0.595 |
| 37191 | Venezuela | America | 2014 | 6.030 |
| 37192 | Vietnam | East Asia & Pacific | 2014 | 1.800 |
| 37193 | Zambia | Sub-Saharan Africa | 2014 | 0.288 |
| 37194 | Zimbabwe | Sub-Saharan Africa | 2014 | 0.780 |
Estableceremos un límite inferior de 1964 en la columna year del DataFrame
new_co2, filtrando los datos para mantener únicamente los años posteriores a 1963, y convertiremos los valores de esta columna al tipoint64utilizando.astype('int64'). El resultado se almacenará en un nuevo DataFrame llamadodf_co2, que luego se ordenará por las columnas['country', 'year']utilizando.sort_values()para garantizar que los datos estén organizados por país y año de manera coherente y lista para análisis posteriores.
# Filtramos el DataFrame 'new_co2' para mantener solo los registros donde el valor de 'year' es mayor a 1963
# Convertimos los valores de la columna 'year' al tipo int64 para asegurar que los años sean tratados como enteros
# Luego, ordenamos el DataFrame resultante por las columnas 'country' y 'year'
df_co2 = new_co2[new_co2['year'].astype('int64') > 1963].sort_values(by=['country', 'year'])
# Convertimos nuevamente la columna 'year' al tipo int64 para asegurarnos de que los valores tengan el formato adecuado
df_co2['year'] = df_co2['year'].astype('int64')
# Mostramos las primeras 5 filas del DataFrame 'df_co2' para verificar que todo se ha realizado correctamente
df_co2.head()
| country | region | year | co2 | |
|---|---|---|---|---|
| 28372 | Afghanistan | South Asia | 1964 | 0.0863 |
| 28545 | Afghanistan | South Asia | 1965 | 0.1010 |
| 28718 | Afghanistan | South Asia | 1966 | 0.1080 |
| 28891 | Afghanistan | South Asia | 1967 | 0.1240 |
| 29064 | Afghanistan | South Asia | 1968 | 0.1160 |
Creamos un nuevo DataFrame
df_gdpque contenga las columnas country, year y gdp del DataFramegm:
# Seleccionamos las columnas 'Country', 'Year' y 'gdp' del DataFrame 'gm'
# Estas columnas contienen la información del país, año y PIB (gdp)
df_gdp = gm[['Country', 'Year', 'gdp']]
# Renombramos las columnas para mantener consistencia en el formato
# Cambiamos 'Country' a 'country' y 'Year' a 'year' para alinearlas con el formato del DataFrame 'df_co2'
df_gdp.columns = ['country', 'year', 'gdp']
# Mostramos las primeras 5 filas del DataFrame 'df_gdp' para verificar que los nombres de columnas y los datos son correctos
df_gdp.head()
| country | year | gdp | |
|---|---|---|---|
| 0 | Afghanistan | 1964 | 1182.0 |
| 1 | Afghanistan | 1965 | 1182.0 |
| 2 | Afghanistan | 1966 | 1168.0 |
| 3 | Afghanistan | 1967 | 1173.0 |
| 4 | Afghanistan | 1968 | 1187.0 |
Identificamos las columnas con valores faltantes en ambos DataFrames:
df_co2.isna().sum()
country 0
region 0
year 0
co2 448
dtype: int64
df_gdp.isna().sum()
country 0
year 0
gdp 1111
dtype: int64
Fusionamos los DataFrames
df_co2ydf_gdpen las columnas country y year, y eliminamos los valores faltantes:
# Combinamos los DataFrames 'df_co2' y 'df_gdp' utilizando un 'merge' en las columnas 'country' y 'year'
# Se utiliza 'how=left' para hacer un left join, lo que asegura que se mantengan todos los registros de 'df_co2'
# y solo se añadan los datos de 'gdp' donde haya coincidencias en país y año
data = pd.merge(df_co2, df_gdp, on=['country', 'year'], how='left')
# Eliminamos las filas que contienen valores NaN en cualquier columna utilizando 'dropna()'
# Esto es necesario para asegurarnos de que trabajamos solo con datos completos
data = data.dropna()
# Mostramos las primeras 5 filas del DataFrame 'data' para verificar que la combinación y la limpieza se realizaron correctamente
data.head()
| country | region | year | co2 | gdp | |
|---|---|---|---|---|---|
| 0 | Afghanistan | South Asia | 1964 | 0.0863 | 1182.0 |
| 1 | Afghanistan | South Asia | 1965 | 0.1010 | 1182.0 |
| 2 | Afghanistan | South Asia | 1966 | 0.1080 | 1168.0 |
| 3 | Afghanistan | South Asia | 1967 | 0.1240 | 1173.0 |
| 4 | Afghanistan | South Asia | 1968 | 0.1160 | 1187.0 |
Por último, comprobemos la correlación entre las emisiones de dióxido de carbono y el PIB para asegurarnos de que estamos analizando datos que merecen ser visualizados. Crea un array numpy con las columnas
co2ygdp:
# Convertimos la columna 'co2' del DataFrame 'data' en un array de NumPy
# Esto facilita la manipulación y el cálculo de correlaciones entre los valores
np_co2 = np.array(data['co2'])
# Convertimos la columna 'gdp' del DataFrame 'data' en un array de NumPy
# Este paso es similar al anterior para preparar los datos para el análisis
np_gdp = np.array(data['gdp'])
# Calculamos el coeficiente de correlación entre los arrays 'np_co2' y 'np_gdp'
# Utilizamos la función 'corrcoef()' de NumPy, que devuelve una matriz de correlación
# El valor en la posición [0, 1] o [1, 0] será el coeficiente de correlación entre 'co2' y 'gdp'
np.corrcoef(np_co2, np_gdp)
array([[1. , 0.78219731],
[0.78219731, 1. ]])
El resultado muestra una correlación significativa (0.78) entre las emisiones de CO2 y el PIB, lo que indica que estos datos son adecuados para un análisis más profundo y visualización interactiva.
Creación del gráfico estático con visualización interactiva#
En este ejercicio, crearemos un gráfico estático utilizando Bokeh para visualizar nuestro conjunto de datos, al cual añadiremos glifos circulares. A continuación, se detallan los pasos para lograrlo:
Se debe instalar la biblioteca
bokehyipywidgets.
Vamos a importar las bibliotecas de Bokeh que nos permitirán crear visualizaciones interactivas, mapear datos de pandas a Bokeh, y aplicar herramientas útiles como el
HoverTooly deslizadores.
Importemos las bibliotecas
curdoc()devuelve el documento actual en Bokeh para que podamos manipularlo.HoverTool,Slider, yColumnDataSourcepermiten la interactividad en el gráfico.Spectral6proporciona una paleta de colores para los gráficos.widgetboxyrowson útiles para organizar el diseño de los elementos visuales.
# Importamos la función 'curdoc' que maneja el documento gráfico actual en Bokeh
from bokeh.io import curdoc, output_notebook
# Importamos las herramientas y métodos de Bokeh para crear interactividad
from bokeh.models import HoverTool, ColumnDataSource, CategoricalColorMapper, Slider, CustomJS
# Importamos la paleta de colores 'Spectral6' que usaremos para colorear las regiones
from bokeh.palettes import Spectral6
# Importamos 'widgetbox' para agrupar herramientas, y 'row' para organizar objetos visuales en filas
from bokeh.layouts import row, column
# Importamos la clase 'Figure' para crear gráficos en Bokeh
from bokeh.plotting import figure, show, output_notebook, output_file
# Importamos 'interact' para permitir la interactividad en el Jupyter notebook
from ipywidgets import interact
Ejecutamos la función
output_notebook()para cargar BokehJS, lo que permite que las visualizaciones se muestren dentro del Jupyter notebook.
Vamos a codificar los puntos de datos (países) por color, dependiendo de la región a la que pertenezcan. Primero, crearemos una lista única de regiones a partir del DataFrame
data, y luego utilizaremosCategoricalColorMapperpara asignar un color a cada región.
# Creamos una lista de regiones únicas a partir de la columna 'region' del DataFrame 'data'
regions_list = data['region'].unique().tolist()
# Usamos 'CategoricalColorMapper' para asignar un color de la paleta 'Spectral6' a cada región
color_mapper = CategoricalColorMapper(factors=regions_list, palette=Spectral6)
Utilizaremos la función
ColumnDataSourcepara construir un diccionario que contendrá las variables utilizadas en la gráfica, al que llamaremos source_init. Además, crearemos otro diccionario llamado source_last, que nos permitirá actualizar la gráfica de manera dinámica a medida que se visualicen los datos por año. Este enfoque nos ayudará a gestionar eficientemente los datos y actualizar la visualización de manera interactiva.
source_init = ColumnDataSource(data={'x': data.gdp[data.year == min(data.year)],
'y': data.co2[data.year == min(data.year)],
'country': data.country[data.year == min(data.year)],
'region': data.region[data.year == min(data.year)],
'year': data.year[data.year == min(data.year)]})
source_last = ColumnDataSource(data={'x': data.gdp, 'y': data.co2, 'year': data.year})
Creamos una figura inicial vacía con las siguientes configuraciones:
Establecemos el título como
Emisiones de CO2 versus PIB en 1964.Definimos el rango del eje
xdesdexminhastaxmax.Configuramos el rango del eje
ydesdeyminhastaymax.Ajustamos el tipo de escala del eje
ya logarítmica para reflejar mejor la distribución de los datos.
plot = figure(title='CO2 Emissions vs GDP in 1964', y_axis_type='log')
Añadimos glifos circulares a la gráfica:
plot.scatter(x='x', y='y',
fill_alpha=0.8,
source=source_init,
legend_label='region',
color=dict(field='region', transform=color_mapper),
size=7)
Establezca la ubicación de la leyenda en la esquina inferior derecha del gráfico, y agregue títulos a los ejes
plot.legend.location = 'bottom_right'
plot.xaxis.axis_label = 'Income Per Person'
plot.yaxis.axis_label = 'CO2 Emissions (tons per person)'
show(plot)
Añadiendo la herramienta Hover#
Vamos a permitir que el usuario pase por encima de un punto de datos en nuestro gráfico para ver el nombre del país, las emisiones de dióxido de carbono y el PIB. Los siguientes pasos te ayudarán con la solución:
Crear una herramienta de rastreo llamada hover. Añade la herramienta hover a la gráfica
hover = HoverTool(tooltips=[('Country', '@country'), ('GDP', '@x'), ('CO2 Emission', '@y')])
plot.add_tools(hover)
show(plot)
Añadiendo un slicer al gráfico estático#
Nombramos el archivo HTML de salida como co2_emissions
output_file('iframe_figures/co2_emissions.html')
Creamos la función
callbackusandoCustomJSde la libreria bokeh la cual permitira actualizar nuestra figura. Usamos algunas pocas ordenes en lenguajeJavaScript. En este casocb_obj.valuecontiene el modelo queactiva el callback
callback = CustomJS(args=dict(source_last=source_last, source_init=source_init), code="""
var data_init = source_init.data;
var yr = cb_obj.value;
var year = source_last.data['year'];
var x_new = [];
var y_new = [];
for(var i = 0; i < year.length; i++) {
if(year[i] == yr) {
x_new.push(source_last.data['x'][i]);
y_new.push(source_last.data['y'][i]);
}
}
data_init['x'] = x_new;
data_init['y'] = y_new;
source_init.change.emit();
""")
Construimos el slider que utilizaremos para actualizar las figuras, y realizamos el llamado de la función callback que recibirá como input cada año que proviene del slider. La siguiente figura podrá ser visualizada desde su editor VS Code. Para visualizarla en su JBook deberá actualizar el html desde su navegador, para que la función show() pueda activarse y mostrarse en su página.
# Crear deslizador para seleccionar el año
slider = Slider(
start=min(data.year), # Año inicial (mínimo del dataset)
end=max(data.year), # Año final (máximo del dataset)
step=1, # Avance de 1 en 1 año
value=min(data.year), # Valor inicial del deslizador
title='Year' # Título que aparece en el control
)
# Conectar el deslizador con la función de actualización (callback)
slider.js_on_change('value', callback)
# Organizar el deslizador y el gráfico en una misma columna
layout = column(slider, plot)
show(layout)
Visualización de datos a través de capas#
Presentaremos Altair, una potente biblioteca de Python diseñada específicamente para la creación de gráficos interactivos. En esta sesión, demostraremos cómo utilizar Altair para generar visualizaciones interactivas en datos estratificados según diferentes variables categóricas. Para ilustrar sus capacidades, emplearemos un conjunto de datos real y crearemos gráficos de dispersión y de barras, destacando las principales características de los datos. Además, añadiremos una amplia gama de elementos interactivos para mejorar la experiencia visual y permitir una exploración dinámica de los datos.
Funcionalidad Hover y Tooltip#
Importa el módulo
altaircomoalt. Antes debe instalar altair usandopip install altair vega_datasets.
import altair as alt
Cargar el conjunto de datos
hpiy leer desde el conjunto de datos usando pandas:
import pandas as pd
filename = "C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/hpi_data_countries.tsv"
hpi_df = pd.read_csv(filename, sep='\t')
hpi_df.head()
| HPI Rank | Country | Region | Life Expectancy (years) | Wellbeing (0-10) | Inequality of outcomes | Ecological Footprint (gha/capita) | Happy Planet Index | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | Costa Rica | Americas | 79.1 | 7.3 | 15% | 2.8 | 44.7 |
| 1 | 2 | Mexico | Americas | 76.4 | 7.3 | 19% | 2.9 | 40.7 |
| 2 | 3 | Colombia | Americas | 73.7 | 6.4 | 24% | 1.9 | 40.7 |
| 3 | 4 | Vanuatu | Asia Pacific | 71.3 | 6.5 | 22% | 1.9 | 40.6 |
| 4 | 5 | Vietnam | Asia Pacific | 75.5 | 5.5 | 19% | 1.7 | 40.3 |
Proporcione el DataFrame seleccionado, en este caso
hpi_df, a la funciónalt.Chartde Altair. Utilice la funciónmark_circle()para representar los puntos de datos en el gráfico de dispersión con círculos rellenos. Emplee la funciónencode()para asignar las variables correspondientes a los ejes \(x\) e \(y\). Aunque también utilizamos el parámetrocolordentro deencode()para diferenciar los puntos de datos por la variable región mediante un código de colores, este paso es opcional.
alt.Chart(hpi_df).mark_circle().encode(
x = 'Wellbeing (0-10):Q', # Asigna la variable cuantitativa 'Wellbeing (0-10)' al eje x.
y = 'Happy Planet Index:Q', # Asigna la variable cuantitativa 'Happy Planet Index' al eje y.
color = 'Region:N', # Colorea los puntos de datos según la variable categórica 'Region'.
tooltip = ['Country', 'Region', 'Wellbeing (0-10)', 'Happy Planet Index', 'Life Expectancy (years)'] # Muestra información adicional al pasar el cursor sobre los puntos.
)
En el gráfico anterior, observará que las características especificadas en el parámetro
tooltipde la funciónencode()se muestran cuando el cursor se coloca sobre cualquier punto de datos. Sin embargo, la funcionalidad de zoom se ha perdido. ¿Cómo recuperarla? Es muy sencillo: ¡simplemente añada la funcióninteractive()!. Incorporeinteractive()para habilitar nuevamente la función de zoom en el gráfico, como se muestra a continuación:
alt.Chart(hpi_df).mark_circle().encode(
x = 'Wellbeing (0-10):Q', # Asigna la variable cuantitativa 'Wellbeing (0-10)' al eje x.
y = 'Happy Planet Index:Q', # Asigna la variable cuantitativa 'Happy Planet Index' al eje y.
color = 'Region:N', # Colorea los puntos según la variable categórica 'Region'.
tooltip = ['Country', 'Region', 'Wellbeing (0-10)', 'Happy Planet Index', 'Life Expectancy (years)'] # Muestra un tooltip con información adicional al pasar el cursor.
).interactive() # Habilita la interactividad, incluyendo la capacidad de hacer zoom.
Añade
alt_valuecomolightgraypara que todos los puntos fuera de la selección sean grises.
selected_area = alt.selection_interval()
alt.Chart(hpi_df).mark_circle().encode(
x = 'Wellbeing (0-10):Q',
y = 'Happy Planet Index:Q',
color = alt.condition(selected_area, 'Region:N', alt.value('lightgray'))
).add_selection(
selected_area
)
Selección a través de múltiples parcelas#
La función de selección se vuelve mucho más poderosa cuando se aplica a múltiples gráficos de manera conjunta. Tomemos como ejemplo dos gráficos de dispersión:
wellbeingvshappy planet indexlife expectancyvshappy planet index
En este ejercicio, avanzaremos paso a paso para crear un gráfico interactivo. Para nuestro primer gráfico de dispersión, dado que queremos que el eje y sea común en ambos gráficos, solo especificaremos el eje x en la función encode() de Altair, y luego añadiremos las características del eje y por separado en el objeto Chart. Además, para disponer los dos gráficos de manera vertical y habilitar la selección entre ellos, utilizaremos la función vconcat de Altair.
Gráfico de dispersión de manera vertical
chart = alt.Chart(hpi_df).mark_circle().encode(
y='Happy Planet Index', # Asigna 'Happy Planet Index' al eje y y codifica el color por 'Region'.
color='Region:N' # Colorea los puntos de datos según la variable categórica 'Region'.
)
chart1 = chart.encode(x = 'Wellbeing (0-10)') # Define el eje x como 'Wellbeing (0-10)' para el primer gráfico.
chart2 = chart.encode(x = 'Life Expectancy (years)') # Define el eje x como 'Life Expectancy (years)' para el segundo gráfico.
alt.vconcat(chart1, chart2) # Combina ambos gráficos verticalmente.
Gráfico de dispersión de manera horizontal
# Selección por hover y tooltip en múltiples gráficos
selected_area = alt.selection_interval() # Define una selección de intervalo para la interacción.
chart = alt.Chart(hpi_df).mark_circle().encode(
y = 'Happy Planet Index', # Asigna 'Happy Planet Index' al eje y.
color = alt.condition(selected_area, 'Region', alt.value('lightgray')) # Colorea los puntos según la selección; los puntos no seleccionados se muestran en gris.
).add_selection(
selected_area # Añade la selección al gráfico.
)
chart1 = chart.encode(x = 'Wellbeing (0-10)') # Primer gráfico con 'Wellbeing (0-10)' en el eje x.
chart2 = chart.encode(x = 'Life Expectancy (years)') # Segundo gráfico con 'Life Expectancy (years)' en el eje x.
chart1 | chart2 # Combina los gráficos en un diseño horizontal.
Selección de los valores de una variable categórica#
Ahora, crearemos un gráfico interactivo que permitirá visualizar los puntos de datos correspondientes a una región específica. Utilizaremos la función selection_single() para seleccionar un conjunto determinado de puntos de datos. Al analizar el código, notará que los parámetros de esta función son bastante intuitivos. Para obtener más detalles o aclaraciones sobre su funcionamiento, puede consultar la documentación oficial en: Altair Selection Single.
Cree una variable input_dropdown utilizando la función binding_select() y configure el parámetro options con la lista de regiones disponibles en nuestro conjunto de datos. A continuación, utilice la función selection_single() para permitir la selección de un conjunto específico de puntos de datos. Almacene la condición de selección en una variable llamada color, la cual determinará bajo qué condiciones se resaltarán los puntos seleccionados en el gráfico.
input_dropdown = alt.binding_select(options = list(set(hpi_df.Region))) # Crea un menú desplegable con las regiones únicas del DataFrame.
selected_points = alt.selection_single(fields = ['Region'], bind = input_dropdown, name = 'Select') # Define una selección simple vinculada al menú desplegable por la variable 'Region'.
color = alt.condition(selected_points, # Establece la condición para el color basado en la selección.
alt.Color('Region:N'), # Si está seleccionada, se usa el color de 'Region'.
alt.value('lightgray')) # Si no está seleccionada, los puntos se muestran en gris.
alt.Chart(hpi_df).mark_circle().encode(
x = 'Wellbeing (0-10):Q', # Asigna 'Wellbeing (0-10)' al eje x.
y = 'Happy Planet Index:Q', # Asigna 'Happy Planet Index' al eje y.
color = color, # Aplica la condición de color basada en la selección.
tooltip = 'Region:N' # Muestra la región en el tooltip al pasar el cursor sobre un punto.
).add_selection(
selected_points # Añade la selección al gráfico.
)
En el siguiente código se crea un gráfico de barras interactivo utilizando Altair, donde se muestra el promedio del Happy Planet Index por región. Se implementa una selección interactiva que permite resaltar las barras correspondientes a cada región al seleccionar desde la leyenda. Además, se agregan barras de error que representan el intervalo de confianza (CI) para cada promedio, con las barras de error mostradas en color negro. Ambos gráficos, las barras y las barras de error, se combinan y permiten la interactividad, como hacer zoom o resaltar regiones específicas.
# Definir selección para interactividad
highlight = alt.selection_single(fields=['Region'], bind='legend')
# Gráfico de barras con el promedio del Happy Planet Index y la selección interactiva
bar_chart = alt.Chart(hpi_df).mark_bar().encode(
x='Region:N',
y='mean(Happy Planet Index):Q',
color=alt.condition(highlight, 'Region:N', alt.value('lightgray')) # Colorea las barras seleccionadas
).add_selection(
highlight # Añadir selección interactiva
).properties(width=400) # Ajusta el ancho del gráfico
# Añadir barras de error con intervalo de confianza (ci) en color negro
error_bars = alt.Chart(hpi_df).mark_errorbar(extent='ci', color='black').encode(
x='Region:N',
y=alt.Y('mean(Happy Planet Index):Q')
).properties(width=400) # Ajusta el ancho de las barras de error
# Combinar ambos gráficos
(bar_chart + error_bars).interactive()
Función de zoom en un mapa de calor estático#
A continuación, añadiremos la función de zoom al mapa de calor de las correlaciones.
alt.Chart(hpi_df).mark_rect().encode(
alt.X('Happy Planet Index:Q', bin=True), # Eje X con 'Happy Planet Index', agrupado en intervalos (binning)
alt.Y('Wellbeing (0-10):Q', bin=True), # Eje Y con 'Wellbeing', también agrupado en intervalos
alt.Color('count()', scale=alt.Scale(scheme='greenblue'), legend=alt.Legend(title='Total Countries')) # Colorea los rectángulos según el recuento de países, con un esquema de color verde-azul
).interactive() # Habilita la interactividad (zoom y desplazamiento)
Dinámica de un gráfico de barras y un mapa de calor#
Vamos a vincular dinámicamente un gráfico de barras con un mapa de calor. Imagina un escenario en el que, al hacer clic en una barra del gráfico, el mapa de calor se actualiza automáticamente para mostrar los datos correspondientes a la región seleccionada. Por ejemplo, podrías hacer clic en una barra que representa una región y, a partir de eso, el mapa de calor de Life Expectancy frente a Wellbeing se ajustaría para mostrar únicamente los datos de los países de esa región específica. Esto permite una exploración más detallada y enfocada en los datos.
Seleccione la región mediante el método de
selectiony crea elheatmap
selected_region = alt.selection(type="single", encodings=['x']) # Definir una selección interactiva en el eje x (región)
heatmap = alt.Chart(hpi_df).mark_rect().encode(
alt.X('Wellbeing (0-10):Q', bin=True), # Eje X con 'Wellbeing' agrupado en intervalos (binned)
alt.Y('Life Expectancy (years):Q', bin=True), # Eje Y con 'Life Expectancy' también agrupado en intervalos
alt.Color('count()', scale=alt.Scale(scheme='greenblue'), legend=alt.Legend(title='Total Countries')) # Colorear según el recuento de países, usando un esquema de color verde-azul
).properties(
width=350 # Ajustar el ancho del gráfico a 350 píxeles
)
Colocar los círculos en un mapa de calor
circles = heatmap.mark_point().encode(
alt.ColorValue('grey'), # Los puntos (círculos) tendrán color gris
alt.Size('count()', legend=alt.Legend(title='Records in Selection')) # El tamaño de los puntos representa el número de registros en la selección
).transform_filter(
selected_region # Filtra los puntos en función de la región seleccionada en el gráfico de barras
)
Utilice la función
heatmap+circles | barspara vincular dinámicamente el gráfico de barras y el mapa de calor
bars = alt.Chart(hpi_df).mark_bar().encode(
x='Region:N', # Eje X con las regiones (categórico)
y='count()', # Eje Y con el recuento de países por región
color=alt.condition(selected_region, alt.ColorValue("steelblue"), alt.ColorValue("grey")) # Colorea las barras en azul para la región seleccionada y en gris para las no seleccionadas
).properties(
width=350 # Ajusta el ancho del gráfico de barras
).add_selection(selected_region) # Añade la selección interactiva en las barras
# Combina el heatmap con los círculos, seguido por el gráfico de barras en un diseño horizontal
heatmap + circles | bars
Visualización interactiva de mapas con datos geográficos#
En la actualidad, muchos conjuntos de datos contienen características relacionadas con aspectos espaciales o geográficos, como la ubicación de los usuarios de redes sociales, las métricas de desarrollo mundial calculadas para diferentes países o rutas de transporte que conectan diversos lugares del planeta. Es fundamental aprender a comprender y presentar esta información de manera clara y perspicaz.
Esta sección te proporcionará las herramientas necesarias para generar gráficos que representen datos geográficos de manera efectiva. Aunque bibliotecas como Altair y Geopandas ofrecen posibilidades interesantes para visualizar estos datos, Plotly destaca por su capacidad de crear gráficos geográficos variados que son fáciles de construir, personalizar y depurar. Además, utilizaremos Plotly para mostrar cómo generar distintos tipos de visualizaciones geográficas utilizando conjuntos de datos públicos. Un ejemplo clave es el mapa coroplético, que colorea diferentes divisiones geográficas, como países o regiones, para mostrar el valor de una característica específica en cada una. Por ejemplo, podrías visualizar el acceso a internet en diferentes países del mundo utilizando un mapa coroplético.
Creación de un mapa coroplético#
Visualización del uso de internet a Nivel Mundial#
En la primera visualización, utilizaremos las estadísticas de uso de internet publicadas por Our World in Data para mostrar el porcentaje de la población que utiliza internet en cada país, desde 1990 hasta 2017.
¿Te has dado cuenta de la columna llamada Code en el conjunto de datos? Este campo contiene un código asignado a cada país bajo el estándar ISO 3166-1, el cual es utilizado globalmente por desarrolladores para referirse a los nombres de los países de manera uniforme en los datos.
Dado que el DataFrame incluye registros de múltiples años, primero filtraremos los datos para un año específico, como 2016. Luego, utilizaremos este subconjunto de datos para generar un mapa mundial, donde se visualizará el uso de internet por país.
Para lograrlo, seguiremos los siguientes pasos:
Cargaremos el conjunto de datos de uso de internet.
import pandas as pd
import numpy as np
# Cargar el archivo CSV proporcionado
file_path = 'C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/share-of-individuals-using-the-internet.csv'
df_owd = pd.read_csv(file_path)
df_owd.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6056 entries, 0 to 6055
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Country 6056 non-null object
1 Code 4912 non-null object
2 Year 6056 non-null int64
3 Individuals using the Internet (% of population) 6056 non-null float64
dtypes: float64(1), int64(1), object(2)
memory usage: 189.4+ KB
Filtraremos los datos por el año 2016.
df_owd_2016 = df_owd.query("Year==2016")
df_owd_2016.head()
| Country | Code | Year | Individuals using the Internet (% of population) | |
|---|---|---|---|---|
| 16 | Afghanistan | AFG | 2016 | 10.595726 |
| 39 | Albania | ALB | 2016 | 66.363445 |
| 63 | Algeria | DZA | 2016 | 42.945527 |
| 85 | Andorra | AND | 2016 | 97.930637 |
| 107 | Angola | AGO | 2016 | 13.000000 |
En los siguientes pasos, utilizaremos el módulo
expressdePlotly(debido a su simplicidad) y la funciónchoroplethpara crear un mapa coroplético interactivo. A continuación, se describen los parámetros clave que pasaremos a esta función:
locations: Este parámetro se configura con el nombre de la columna del DataFrame que contiene los códigos de país ISO 3166.color: Aquí se establece el nombre de la columna que contiene la característica numérica que se utilizará para colorear el mapa.hover_name: Se define el nombre de la columna que contiene la característica que se mostrará al pasar el cursor sobre el mapa.color_continuous_scale: Define el esquema de color utilizado para la representación visual. Algunos ejemplos son:Blues,Reds,Greens, o el esquema dePlotly:px.colors.sequential.Plasma.
import plotly.express as px # Importa Plotly Express para generar gráficos interactivos de forma sencilla
from IPython.display import IFrame, display, HTML # Importa funciones para visualizar gráficos en Jupyter Notebooks
import chart_studio.plotly as py # Importa Chart Studio para subir gráficos a la plataforma de Plotly (opcional)
config = {'showLink': False, 'displayModeBar': False} # Configuración para desactivar enlaces y barra de herramientas
# Crear el mapa coroplético usando el DataFrame filtrado para 2016
fig = px.choropleth(
df_owd_2016, # DataFrame con los datos del uso de Internet en 2016
locations="Code", # Columna con los códigos de país ISO 3166
color="Individuals using the Internet (% of population)", # Columna para colorear el mapa según el porcentaje de usuarios de Internet
hover_name="Country", # Columna para mostrar el nombre del país al pasar el cursor
color_continuous_scale=px.colors.sequential.Plasma # Esquema de color continuo 'Plasma'
)
# Actualizar el título del gráfico
fig.update_layout(title_text='Internet usage across the world (% population)')
# Mostrar el gráfico
fig.show() # Visualiza el gráfico de forma interactiva
Ajuste de un mapa coroplético mundial#
Supongamos que solo nos interesa visualizar el uso de internet en el continente de Sudamérica. Para lograr esto, podemos utilizar el parámetro geo_scope en la función update_layout() para hacer un zoom en esa región geográfica.
El siguiente código muestra cómo hacerlo de manera rápida y sencilla:
fig = px.choropleth(
df_owd_2016, # DataFrame con los datos de uso de Internet en 2016
locations="Code", # Columna con los códigos ISO 3166 de los países
color="Individuals using the Internet (% of population)", # Columna para colorear el mapa según el porcentaje de usuarios de Internet
hover_name="Country", # Columna para mostrar el nombre del país al pasar el cursor
color_continuous_scale=px.colors.sequential.Plasma # Esquema de color continuo 'Plasma'
)
fig.update_layout(
# Añadir un título para el gráfico
title_text='Uso de Internet en todo Sur América (% de población) - 2016',
geo_scope='south america' # Limitar el alcance del mapa a Sudamérica (opciones: north america, africa, asia, etc.)
)
# Mostrar el gráfico
fig.show()
Otro continente
fig = px.choropleth(df_owd_2016,
locations="Code",
color="Individuals using the Internet (% of population)", # column by which to color-code
hover_name="Country", # column to display in hover information
color_continuous_scale=px.colors.sequential.Plasma)
fig.update_layout(
# add a title text for the plot
title_text = 'Internet usage across the Europe Continent (% population) - 2016',
geo_scope = 'europe' # can be set to north america | south america | africa | asia | europe | usa
)
fig.show()
Para darle al mapa una proyección más realista y permitir que se vea como un globo terráqueo, podemos configurar el tipo de proyección a natural earth utilizando el parámetro projection_type en la función update_layout(). Esto nos permitirá visualizar el mapa con una proyección más natural (ver layout-geo-projection-type), lo que hace que el gráfico pueda rotarse de manera similar a un globo real.
El siguiente código demuestra cómo establecer la proyección del mapa en Natural Earth:
fig = px.choropleth(
df_owd_2016, # DataFrame con los datos de uso de Internet en 2016
locations="Code", # Columna con los códigos ISO 3166 de los países
color="Individuals using the Internet (% of population)", # Columna para colorear el mapa según el porcentaje de usuarios de Internet
hover_name="Country", # Columna para mostrar el nombre del país al pasar el cursor
color_continuous_scale=px.colors.sequential.Plasma # Esquema de color continuo 'Plasma'
)
fig.update_layout(
# Añadir un título para el gráfico
title_text='Internet usage across the world (% population)',
# Establecer el estilo de proyección del mapa
geo=dict(projection={'type':'natural earth'}) # Establecer proyección tipo 'natural earth' (por defecto es 'equirectangular')
)
# Mostrar el gráfico
fig.show()
Añadiendo animación a un mapa coroplético#
A continuación, añadiremos un componente deslizante al mapa para ver los registros en diferentes puntos temporales
# Ordenar las fechas
df_owd.sort_values(by=["Year"],inplace=True)
fig = px.choropleth(
df_owd, # DataFrame con los datos de uso de Internet
locations="Code", # Columna con los códigos ISO 3166 de los países
color="Individuals using the Internet (% of population)", # Columna que define la coloración según el porcentaje de usuarios de Internet
hover_name="Country", # Columna para mostrar el nombre del país al pasar el cursor
animation_frame="Year", # Columna para animar el gráfico a lo largo del tiempo (por año)
color_continuous_scale=px.colors.sequential.Plasma # Esquema de color continuo 'Plasma'
)
fig.update_layout(
# Añadir un título al gráfico
title_text='Internet usage across the world (% population)',
# Establecer el estilo de proyección del mapa
geo=dict(projection={'type':'natural earth'}) # Establecer proyección 'natural earth' (por defecto es 'equirectangular')
)
# Mostrar el gráfico animado
fig.show()
Mapas coropléticos con graph_objects#
graph_objects es una parte avanzada de la biblioteca Plotly que permite un mayor control y personalización sobre los gráficos. Al crear mapas coropléticos con graph_objects, puedes definir manualmente cada aspecto del gráfico, desde la estructura de los datos hasta las características visuales, como los colores, las etiquetas y la proyección del mapa.
Esto ofrece más flexibilidad que px.choropleth, lo que resulta útil cuando necesitas una personalización detallada, como ajustar propiedades específicas de los datos, personalizar interacciones o modificar configuraciones visuales avanzadas. Sin embargo, requiere más código que las opciones más sencillas de plotly.express. Hagamos el siguiente ejemplo:
Vamos a utilizar un conjunto de datos sobre la población de los Estados Unidos para crear un mapa coroplético que muestre la distribución de la población en los distintos estados. Primero, modificaremos el conjunto de datos para asegurarnos de que se ajuste a nuestras necesidades, y luego trazaremos un mapa coroplético que represente visualmente la población en todo el país. Finalmente, ajustaremos el diseño del mapa para resaltar de manera clara y efectiva la población de los Estados Unidos por estado.
# Importemos los datos
filename = 'C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/us_state_population.tsv'
df_pob = pd.read_csv(filename, sep='\t')
df_pob.head()
| State | Code | 2010 | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Alabama | AL | 4785448 | 4798834 | 4815564 | 4830460 | 4842481 | 4853160 | 4864745 | 4875120 | 4887871 |
| 1 | Alaska | AK | 713906 | 722038 | 730399 | 737045 | 736307 | 737547 | 741504 | 739786 | 737438 |
| 2 | Arizona | AZ | 6407774 | 6473497 | 6556629 | 6634999 | 6733840 | 6833596 | 6945452 | 7048876 | 7171646 |
| 3 | Arkansas | AR | 2921978 | 2940407 | 2952109 | 2959549 | 2967726 | 2978407 | 2990410 | 3002997 | 3013825 |
| 4 | California | CA | 37320903 | 37641823 | 37960782 | 38280824 | 38625139 | 38953142 | 39209127 | 39399349 | 39557045 |
Utilice la función
meltpara convertir los datos al formato deseado
df_pob = pd.melt(df_pob, id_vars=['State', 'Code'], var_name="Year", value_name="Population")
df_pob.head()
| State | Code | Year | Population | |
|---|---|---|---|---|
| 0 | Alabama | AL | 2010 | 4785448 |
| 1 | Alaska | AK | 2010 | 713906 |
| 2 | Arizona | AZ | 2010 | 6407774 |
| 3 | Arkansas | AR | 2010 | 2921978 |
| 4 | California | CA | 2010 | 37320903 |
Importar el módulo
graph_objects
import plotly.graph_objects as go
Inicializaremos la figura utilizando la función Figure de graph_objects. En particular, el argumento data debe contener una instancia de la clase Choropleth, que nos permitirá definir los parámetros clave para generar el mapa coroplético.
Los parámetros principales son:
locations: Define la columna del DataFrame que contiene los códigos o nombres de los estados que se representarán en el mapa.z: Se establece en la columna que contiene la variable numérica que queremos representar en el mapa, la cual será codificada por colores.locationmode: Este parámetro se ajusta aUSA-statespara indicar que el mapa debe mostrar los estados de EE. UU.colorscale: Se selecciona un esquema de colores predefinido, comoBlues,RedsoGreens, para representar las variaciones en los valores de la característica numérica. Puedes consultar más opciones en la documentación oficial de Plotly.colorbar_title: Define el título de la barra de colores que aparecerá a la derecha del gráfico, indicando cómo los colores se corresponden con los valores numéricos.
# Inicializar la figura
fig = go.Figure(
data=go.Choropleth(
locations=df_pob['Code'], # Columna con los códigos de los estados de EE. UU.
z=df_pob['Population'].astype(int), # Columna con los datos de población para codificar por colores
locationmode='USA-states', # Definir que las ubicaciones corresponden a los estados de EE. UU.
colorscale='Blues', # Esquema de colores para representar los valores
colorbar_title="Población" # Título de la barra de colores (población)
)
)
# Actualizar el diseño
fig.update_layout(
title_text='Población de los EE. UU. por estados', # Título del gráfico
geo_scope='usa' # Limitar el mapa al territorio de los EE. UU.
)
# Mostrar la figura
fig.show()
Ahora, podemos visualizar un gráfico que represente la cantidad de hectáreas por departamento, para el caso de Colombia. Para sus proyectos de investigación es necesario que obtengan el archivo Colombia.geo.json con la información que se requiere.
Carguemos los paquetes
import plotly.graph_objects as go
import json
Carguemos el GeoJSON
# Cargar el archivo GeoJSON desde la ruta local
geojson_path = r'C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/Colombia.geo.json'
with open(geojson_path, 'r', encoding='utf-8') as response: # Abrir el archivo local
counties = json.load(response)
Almacenemos en una lista los deptos y las hectarias
locs = [] # Lista para almacenar los nombres de los departamentos
z_id = [] # Lista para almacenar los valores de hectáreas
# Recorrer los features del GeoJSON
for loc in counties['features']:
loc['id'] = loc['properties']['NOMBRE_DPT'] # Asignar el nombre del departamento como ID
locs.append(loc['properties']['NOMBRE_DPT']) # Agregar el nombre del departamento a locs
z_id.append(loc['properties']['HECTARES']) # Agregar el valor de hectáreas a z_id
Creamos el mapa coroplético de Colombia
# Crear el mapa coroplético
fig = go.Figure(go.Choroplethmapbox(
geojson=counties,
locations=locs,
z=z_id,
colorscale='Viridis', # Esquema de colores Viridis
colorbar_title="Hectáreas" # Título de la barra de color
))
# Actualizar el layout del mapa
fig.update_layout(
mapbox_style="open-street-map", # Usar el estilo de mapa de Open Street Map
mapbox_zoom=3.78, # Zoom en el mapa
mapbox_center={"lat": 4.570868, "lon": -74.2973328} # Centrar el mapa en Colombia
)
# Mostrar el gráfico
fig.show()
Ahora, solo Colombia
fig = go.Figure(go.Choroplethmapbox(
geojson=counties, # GeoJSON con la geometría de los departamentos
locations=locs, # Nombres de los departamentos
z=z_id, # Valores (hectáreas) para colorear el mapa
colorscale='Viridis', # Esquema de colores 'Viridis'
colorbar_title="Hectáreas" # Título de la barra de color
))
fig.update_layout(
mapbox_style="white-bg", # Estilo del mapa de fondo (blanco)
mapbox_zoom=3.78, # Nivel de zoom del mapa
mapbox_center={"lat": 4.570868, "lon": -74.2973328} # Centrar el mapa en la latitud/longitud de Colombia
)
fig.show() # Mostrar el gráfico
Construcción de un gráfico de dispersión en un mapa geográfico#
Vamos a trazar las ubicaciones de las tiendas Walmart en un mapa de los Estados Unidos, utilizando un conjunto de datos disponible públicamente sobre las aperturas de tiendas Walmart entre 1962 y 2006. Este ejemplo nos permitirá ver la ubicación exacta de las tiendas Walmart, y utilizaremos las coordenadas de latitud y longitud para representarlas en un gráfico de dispersión.
import plotly.graph_objects as go
import pandas as pd
# Leer los datos de ubicaciones de Walmart desde la URL
filename = "C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/1962_2006_walmart_store_openings.csv"
df_walm = pd.read_csv(filename)
# Crear el gráfico de dispersión
fig = go.Figure(data=go.Scattergeo(
lon=df_walm['LON'], # Columna de longitudes
lat=df_walm['LAT'], # Columna de latitudes
text=df_walm['STREETADDR'], # Información que aparece al pasar el cursor (dirección de las tiendas)
mode='markers' # Usar marcadores para las ubicaciones
))
# Configurar el diseño del mapa
fig.update_layout(
title='Tiendas Walmart en los EE.UU.',
geo_scope='usa' # Limitar el mapa a los EE.UU.
)
# Mostrar el gráfico
fig.show()
Este código genera un gráfico de dispersión que muestra las ubicaciones de las tiendas Walmart en los Estados Unidos. Las ubicaciones se representan con marcadores, y la información de la dirección aparece al pasar el cursor sobre los puntos.
Construcción de un gráfico de burbujas en un mapa geográfico#
Dado que hay muchas tiendas Walmart en el este de EE. UU., puede ser útil visualizar el número de tiendas Walmart por estado utilizando un gráfico de burbujas. En este tipo de gráfico, el tamaño de cada burbuja representa el número de tiendas en cada estado.
import plotly.express as px
# Calcular el número de tiendas por estado
walmart_stores_by_state = df_walm.groupby('STRSTATE').count()['storenum'].reset_index().rename(columns={'storenum': 'NUM_STORES'})
# Crear el gráfico de burbujas
fig = px.scatter_geo(walmart_stores_by_state,
locations="STRSTATE", # Columna con los códigos de estado
size="NUM_STORES", # Tamaño de las burbujas según el número de tiendas
locationmode='USA-states',
hover_name="STRSTATE", # Mostrar el código del estado al pasar el cursor
size_max=45)
# Configurar el diseño del mapa
fig.update_layout(
title_text='Tiendas Walmart en los EE.UU. por estado',
geo_scope='usa' # Limitar el mapa a los EE.UU.
)
# Mostrar el gráfico
fig.show()
Este código genera un gráfico de burbujas donde el tamaño de cada burbuja representa el número de tiendas Walmart en cada estado de los EE.UU. Los estados con más tiendas tendrán burbujas más grandes.
Ahora, realizaremos el gráfico de burbujas para usuarios de internet a nivel mundial. Otra aplicación útil de los gráficos de burbujas es visualizar el número de usuarios de Internet por país. Utilizaremos un conjunto de datos sobre los usuarios de Internet en todo el mundo y generaremos un gráfico de burbujas que muestre el número de usuarios en cada país.
# Leer los datos de usuarios de Internet por país
filename = "C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/number-of-internet-users-by-country.csv"
df_int = pd.read_csv(filename)
# Filtrar los datos para el año 2016
df_int_2016 = df_int.query("Year==2016")
# Crear el gráfico de burbujas
fig = px.scatter_geo(df_int_2016,
locations="Code", # Columna con los códigos de país
size="Number of internet users (users)", # Tamaño de las burbujas según el número de usuarios
hover_name="Country", # Mostrar el nombre del país al pasar el cursor
size_max=80, # Máximo tamaño de las burbujas
color_continuous_scale=px.colors.sequential.Plasma)
# Configurar el diseño del mapa
fig.update_layout(
title_text='Usuarios de Internet en el mundo - 2016',
geo=dict(projection={'type': 'natural earth'}) # Usar la proyección 'natural earth'
)
# Mostrar el gráfico
fig.show()
Este gráfico de burbujas muestra la cantidad de usuarios de Internet por país en 2016. Las burbujas más grandes indican una mayor cantidad de usuarios.
Construcción de un gráfico de burbujas con animación del crecimiento de usuarios de internet#
Podemos añadir animación para visualizar cómo ha crecido el número de usuarios de Internet en diferentes países a lo largo de los años.
# Crear el gráfico de burbujas animado
fig = px.scatter_geo(df_int,
locations="Code", # Columna con los códigos de país
size="Number of internet users (users)", # Tamaño de las burbujas según el número de usuarios
hover_name="Country", # Mostrar el nombre del país al pasar el cursor
size_max=80, # Máximo tamaño de las burbujas
animation_frame="Year", # Añadir animación por año
color_continuous_scale=px.colors.sequential.Plasma)
# Configurar el diseño del mapa
fig.update_layout(
title_text='Usuarios de Internet en el mundo a lo largo de los años',
geo=dict(projection={'type': 'natural earth'}) # Usar la proyección 'natural earth'
)
# Mostrar el gráfico
fig.show()
Este gráfico muestra la evolución del número de usuarios de Internet en el mundo a lo largo del tiempo. Las burbujas crecen a medida que aumenta el número de usuarios en cada país.
Construcción de gráficos de líneas en un mapa geográfico#
En este ejemplo, generaremos un gráfico de dispersión para mostrar la ubicación de los aeropuertos de Estados Unidos y luego dibujaremos líneas que representen vuelos retrasados entre los aeropuertos de origen y destino el 1 de enero de 2015.
Cargar y visualizar los aeropuertos de EE.UU.
Cargamos el conjunto de datos de los aeropuertos y creamos un gráfico de dispersión que muestre la ubicación de los aeropuertos en EE.UU.
import pandas as pd
import plotly.graph_objects as go
# Cargar los datos de los aeropuertos de EE.UU.
filename = "C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/airports.csv"
df_air = pd.read_csv(filename)
# Crear un gráfico de dispersión para mostrar los aeropuertos
fig = go.Figure()
fig.add_trace(go.Scattergeo(
locationmode='USA-states',
lon=df_air['LONGITUDE'], # Longitudes de los aeropuertos
lat=df_air['LATITUDE'], # Latitudes de los aeropuertos
hoverinfo='text', # Información al pasar el cursor
text=df_air['AIRPORT'], # Nombre del aeropuerto
mode='markers', # Modo marcador
marker=dict(size=5, color='black') # Configurar los marcadores
))
# Actualizar el diseño del gráfico
fig.update_layout(
title_text='Aeropuertos en EE.UU.', # Título del gráfico
showlegend=False, # Ocultar leyenda
geo=go.layout.Geo(scope='usa') # Limitar el gráfico a EE.UU.
)
# Mostrar el gráfico
fig.show()
Cargar y fusionar los datos de vuelos
A continuación, cargamos los datos de vuelos retrasados el 1 de enero de 2015, y combinamos estos datos con la información de los aeropuertos para obtener las latitudes y longitudes de los aeropuertos de origen y destino.
# Cargar los datos de vuelos retrasados del 1 de enero de 2015
filename= "C:/Users/cdeor/OneDrive/Documentos/MachineLearningDipSerfinanzas/jbook_ml202430/docs/_data/dataviz/new_year_day_2015_delayed_flights.csv"
new_year_2015_flights_df = pd.read_csv(filename)
# Fusionar los datos de vuelos con los aeropuertos de origen para obtener latitud y longitud
new_year_2015_flights_df = new_year_2015_flights_df.merge(
df_air[['IATA_CODE', 'LATITUDE', 'LONGITUDE']],
left_on='ORIGIN_AIRPORT',
right_on='IATA_CODE',
how='inner'
)
# Renombrar las columnas para reflejar que son del aeropuerto de origen
new_year_2015_flights_df.rename(
columns={'LATITUDE': 'ORIGIN_AIRPORT_LATITUDE', 'LONGITUDE': 'ORIGIN_AIRPORT_LONGITUDE'},
inplace=True
)
# Fusionar los datos de vuelos con los aeropuertos de destino
new_year_2015_flights_df = new_year_2015_flights_df.merge(
df_air[['IATA_CODE', 'LATITUDE', 'LONGITUDE']],
left_on='DESTINATION_AIRPORT',
right_on='IATA_CODE',
how='inner'
)
# Renombrar las columnas para reflejar que son del aeropuerto de destino
new_year_2015_flights_df.rename(
columns={'LATITUDE': 'DESTINATION_AIRPORT_LATITUDE', 'LONGITUDE': 'DESTINATION_AIRPORT_LONGITUDE'},
inplace=True
)
Este código fusiona los datos de vuelos con las latitudes y longitudes de los aeropuertos de origen y destino.
Trazar las líneas que representan los vuelos retrasados
Ahora dibujaremos líneas que representen los vuelos retrasados entre los aeropuertos de origen y destino.
# Dibujar líneas para los vuelos retrasados entre los aeropuertos de origen y destino
for i in range(len(new_year_2015_flights_df)):
fig.add_trace(go.Scattergeo(
locationmode='USA-states',
lon=[new_year_2015_flights_df['ORIGIN_AIRPORT_LONGITUDE'][i], new_year_2015_flights_df['DESTINATION_AIRPORT_LONGITUDE'][i]], # Longitudes de origen y destino
lat=[new_year_2015_flights_df['ORIGIN_AIRPORT_LATITUDE'][i], new_year_2015_flights_df['DESTINATION_AIRPORT_LATITUDE'][i]], # Latitudes de origen y destino
mode='lines', # Dibujar líneas en lugar de marcadores
line=dict(width=1, color='red') # Configurar el estilo de la línea (color y ancho)
))
# Actualizar el diseño del gráfico
fig.update_layout(
title_text='Vuelos retrasados el 1 de enero de 2015 en EE.UU.', # Título del gráfico
showlegend=False, # Ocultar la leyenda
geo=go.layout.Geo(scope='usa') # Limitar el gráfico a EE.UU.
)
# Mostrar el gráfico
fig.show()