Unidad Práctica 1: Introducción al curso de visualización con Python¶
El objetivo de esta práctica es mostrar el potencial de visualizar datos con estas herramientas y entregar una base para comenzar a experimentar a partir de la biblioteca aves. Los detalles (los por qué y los cómo se hace) los veremos en siguientes prácticas.
Google Colab¶
Google Colab es una plataforma que te permite ejecutar este notebook en la nube de Google: http://colab.google.com
Preámbulo: matplotlib en Python¶
Como base de trabajo en el curso utilizaremos la biblioteca matplotlib. Lo primero que hacemos es importar en nuestro notebook las bibliotecas necesarias (en esta ocasión iremos importando bibliotecas a medida que las necesitemos, más adelante usualmente importaremos todo en un comienzo del notebook).
# matplotlib tiene muchas componentes. la principal es pyplot. la usaremos como plt
import matplotlib as mpl
import matplotlib.pyplot as plt
# esto configura la calidad de la imagen. dependerá de tu resolución. el valor por omisión es 80
mpl.rcParams["figure.dpi"] = 96
# esto depende de las fuentes que tengas instaladas en el sistema.
#mpl.rcParams["font.family"] = "Fira Sans Extra Condensed"
# numpy es una biblioteca de bajo nivel, sobre la que construye pandas.
# la utilizaremos en esta sección para generar datos de prueba.
import numpy as np
import pandas as pd
# Importar la biblioteca seaborn para la visualización de datos
import seaborn as sns
# Establecer el estilo de fondo de las gráficas como "whitegrid" en seaborn.
sns.set_style("whitegrid")
En esta sección explicaremos la base de esta biblioteca: una figura (fig) y sus ejes (axes).
La figura es el equivalente a todo el lienzo sobre el que se trabaja la imagen.
Los ejes son las visualizaciones individuales que están presentes en la figura.
El siguiente código crea una figura (fig) y un eje asociado a ella (ax). Vemos que ax posee funciones que nos permiten configurar su apariencia:
fig, ax = plt.subplots()
ax.set_title("Título")
ax.set_xlabel("Eje x")
ax.set_ylabel("Eje y")
# ajusta el espacio utilizado por todos los elementos en la pantalla
fig.tight_layout()
Como no hacemos nada más, el eje está vacío. Con numpy podemos crear unos datos de prueba para incorporar en el gráfico.
test_x = np.linspace(-5, 5, num=50)
test_y = np.cos(test_x)
Usaremos el método plot de ax para graficar esos datos:
fig, ax = plt.subplots()
ax.plot(test_x, test_y, "-", label="coseno")
ax.set_title("Coseno de x")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.legend()
fig.tight_layout()
Podemos crear datos auxiliares extra y usar más veces el método plot, con distintos parámetros:
test_z = np.sin(test_x)
fig, ax = plt.subplots(1, 1, figsize=(6, 4))
ax.plot(test_x, test_y, "-o", color="orange", label="coseno")
ax.plot(test_x, test_z, "-x", color="purple", label="seno")
ax.set_title("Funciones de x", loc="left")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.legend()
fig.tight_layout()
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(test_x, test_y, "-o", color="orange", label="coseno")
axes[0].set_title("Coseno de x")
axes[1].plot(test_x, test_z, "-x", color="purple", label="seno")
axes[1].set_title("Seno de x")
axes[0].set_xlabel("x")
axes[1].set_xlabel("x")
axes[0].set_ylabel("y")
fig.tight_layout()
Para aprender más sobre las componentes de una visualización en matplotlib podemos leer la siguiente nota: Anatomía de una figura.
Usando Datos con pandas: La Encuesta Origen Destino de Santiago¶
pandas es la biblioteca de Python que se utiliza para trabajar con datos tabulares. La utilizaremos constantemente durante el curso.
El dataset que más analizaremos es la Encuesta Origen-Destino 2012 de Santiago. Le llamaremos EOD. La EOD es el instrumento principal que utilizan las autoridades para tomar decisiones respecto a transporte en la ciudad. Se recolectó por última vez el año 2012. Consistió en entrevistar a los residentes de más de 18000 hogares haciéndoles la siguiente pregunta:
¿Cuáles viajes hiciste ayer?
Las personas encuestadas llenaron un diario de viaje, que incluyó todos los datos pertinentes de la rutina del día anterior: a qué hora iniciaron cada viaje, a qué hora terminaron, los lugares de origen y destino (coordenadas), el propósito del viaje, el/los modo(s) de viaje utilizados, etc. También se incluye información socio-demográfica de cada persona y de sus hogares. Suena bien, ¿no? Es un dataset que contiene datos de todo tipo: cualitativos, cuantitativos y geográficos. Se pueden responder muchas preguntas relevantes para la sociedad.
Ahora bien, antes de seguir con el código en este notebook, tengan en cuenta las siguientes consideraciones:
- Hay que diferenciar análisis de personas encuestadas de análisis de población. La encuesta asigna un factor o peso a cada persona, a cada hogar, y a cada viaje. Estos pesos indican la representatividad de cada una de estas unidades de análisis. Por ejemplo, un viaje que es común (ir a trabajar desde A hasta B en metro) tendrá mayor peso que un viaje poco frecuente (hacer un trabajo para la universidad).
- La encuesta es representativa a nivel comunal. La cantidad de hogares encuestados y el peso de cada observación permite hacer un análisis fidedigno para cada comuna, o bien para la ciudad completa Esto quiere decir que podemos sacar conclusiones sobre como se moviliza la población de Providencia, pero no de un barrio específico de la comuna. Puede ser que exista información de ese barrio específico, pero no podemos sacar conclusiones reales puesto que la cantidad de encuestas en ese barrio no es suficiente.
Por tanto, los análisis presentados en este curso son explorativos e ilustrativos, pero no son representativos. En algunos casos sí consideraremos los factores de expansión de las muestras, pero aún así hay que tener cuidado a la hora de interpretar los resultados.
La encuesta está disponible en el Portal de Datos del Gobierno.
def decode_column(
df,
fname,
col_name,
index_col="Id",
value_col=None,
sep=";",
encoding="utf-8",
index_dtype=np.float64,
):
"""
Agrega una columna extra al DataFrame `df` decodificando valores desde un archivo externo.
:param df: DataFrame al que se agregará la columna extra.
:param fname: Nombre del archivo que contiene los valores a decodificar.
:param col_name: Nombre de la columna en el DataFrame `df` que queremos decodificar.
:param index_col: Nombre de la columna en el archivo `fname` que contiene los índices que codifican la columna `col_name`.
:param value_col: Nombre de la columna en el archivo `fname` que tiene los valores decodificados. Si no se proporciona, se utilizará "value" por defecto.
:param sep: Carácter que separa los valores en el archivo `fname`.
:param encoding: Identificación del conjunto de caracteres (character set) que utiliza el archivo. Usualmente es utf-8, si no funciona, se puede probar con iso-8859-1.
:param index_dtype: Tipo de datos del índice en el archivo `fname`, por defecto np.float64.
:return: Serie con los valores decodificados que se agregarán como columna extra en el DataFrame `df`.
"""
if value_col is None:
value_col = "value"
# Lee el archivo `fname` que contiene los valores decodificados junto con sus índices.
values_df = pd.read_csv(
fname,
sep=sep,
index_col=index_col,
names=[index_col, value_col],
header=0,
dtype={index_col: index_dtype},
encoding=encoding,
)
# Extrae la columna `col_name` del DataFrame `df`.
src_df = df.loc[:, (col_name,)]
# Une el DataFrame original con los valores decodificados usando la columna `col_name` como índice.
# Se obtiene una Serie con los valores decodificados que se agregarán como columna extra en el DataFrame `df`.
return src_df.join(values_df, on=col_name)[value_col]
# leer personas
path = "https://raw.githubusercontent.com/zorzalerrante/aves/master/data/external/EOD_STGO/"
personas = pd.read_csv(
path + "personas.csv",sep=";", decimal=",", encoding="utf-8"
).rename(columns={"Factor": "FactorPersona"})
# agregar atributos
atributos = ["Sexo","TramoIngreso","Relacion","Ocupacion"]
for col_name in atributos:
personas[col_name] = decode_column(personas,path + f"Tablas_parametros/{col_name}.csv", col_name)
personas.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 60054 entries, 0 to 60053 Data columns (total 39 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Hogar 60054 non-null int64 1 Persona 60054 non-null int64 2 AnoNac 60054 non-null int64 3 Sexo 60054 non-null object 4 Relacion 60054 non-null object 5 Viajes 60054 non-null int64 6 LicenciaConducir 60054 non-null object 7 PaseEscolar 60054 non-null int64 8 AdultoMayor 60054 non-null int64 9 Estudios 59932 non-null float64 10 Curso 59932 non-null float64 11 Actividad 57969 non-null object 12 Ocupacion 25586 non-null object 13 ActividadEmpresa 25586 non-null float64 14 JornadaTrabajo 25586 non-null float64 15 DondeEstudia 15643 non-null float64 16 DirActividadCoordX 22706 non-null float64 17 DirActividadCoordY 22706 non-null float64 18 DirEstudiosCoordX 13191 non-null float64 19 DirEstudiosCoordY 13191 non-null float64 20 NoViaja 13660 non-null float64 21 TarjetaBip 39107 non-null float64 22 Tarjeta2Bip 1837 non-null float64 23 MedioViajeRestricion 10634 non-null float64 24 ConoceTransantiago 41715 non-null float64 25 NoUsaTransantiago 41573 non-null object 26 Discapacidad 59774 non-null object 27 TieneIngresos 60054 non-null int64 28 Ingreso 60054 non-null int64 29 TramoIngreso 34384 non-null object 30 IngresoFinal 60054 non-null int64 31 TramoIngresoFinal 60054 non-null int64 32 IngresoImputado 60054 non-null int64 33 Factor_LaboralNormal 37326 non-null float64 34 Factor_SabadoNormal 5467 non-null float64 35 Factor_DomingoNormal 4860 non-null float64 36 Factor_LaboralEstival 8837 non-null float64 37 Factor_FindesemanaEstival 3564 non-null float64 38 FactorPersona 60054 non-null float64 dtypes: float64(20), int64(11), object(8) memory usage: 17.9+ MB
# leer hogares
hogares = pd.read_csv(
path + "Hogares.csv", sep=";", decimal=",", encoding="utf-8"
).rename(columns={"Factor": "FactorHogar"})
# agregar atributos
col_name= "Sector"
hogares[col_name] = decode_column(hogares,path + f"Tablas_parametros/{col_name}.csv", col_name)
hogares.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 18264 entries, 0 to 18263 Data columns (total 21 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Hogar 18264 non-null int64 1 Sector 18264 non-null object 2 Zona 18264 non-null int64 3 Comuna 18264 non-null object 4 DirCoordX 18264 non-null float64 5 DirCoordY 18264 non-null float64 6 Fecha 18264 non-null object 7 DiaAsig 18264 non-null object 8 TipoDia 18264 non-null int64 9 Temporada 18264 non-null int64 10 NumPer 18264 non-null int64 11 NumVeh 18264 non-null int64 12 NumBicAdulto 18264 non-null int64 13 NumBicNino 18264 non-null int64 14 Propiedad 18264 non-null int64 15 MontoDiv 2081 non-null float64 16 ImputadoDiv 18264 non-null int64 17 MontoArr 18264 non-null int64 18 ImputadoArr 18264 non-null int64 19 IngresoHogar 18264 non-null int64 20 FactorHogar 18264 non-null float64 dtypes: float64(4), int64(13), object(4) memory usage: 2.9+ MB
# leer viajes
viajes = (
pd.read_csv(path + "viajes.csv", sep=";", decimal=",")
.join(
pd.read_csv(path + "ViajesDifusion.csv", sep=";", index_col="Viaje"),
on="Viaje",
)
.join(
pd.read_csv(path + "DistanciaViaje.csv", sep=";", index_col="Viaje"),
on="Viaje",
)
)
# agregar atributos
viajes["ModoAgregado"] = decode_column(
viajes,path + "Tablas_parametros/ModoAgregado.csv", index_col="ID",value_col="Modo",col_name = "ModoAgregado")
viajes["ModoDifusion"] = decode_column(
viajes,path + "Tablas_parametros/ModoDifusion.csv", encoding="latin-1",index_col="ID",col_name = "ModoDifusion")
viajes["SectorOrigen"] = decode_column(
viajes,path + "Tablas_parametros/Sector.csv", col_name="SectorOrigen",index_col="Sector",value_col="Nombre",sep=";")
viajes["SectorDestino"] = decode_column(
viajes,path + "Tablas_parametros/Sector.csv", col_name="SectorDestino",index_col="Sector",value_col="Nombre",sep=";")
viajes["Proposito"] = decode_column(
viajes,path + "Tablas_parametros/Proposito.csv", col_name="Proposito")
viajes["ComunaOrigen"] = decode_column(
viajes,path + "Tablas_parametros/Comunas.csv","ComunaOrigen",value_col="Comuna",sep=",")
viajes["ComunaDestino"] = decode_column(
viajes,path + "Tablas_parametros/Comunas.csv", "ComunaDestino",value_col="Comuna",sep=",")
viajes["Periodo"] = decode_column(
viajes,path + "Tablas_parametros/Periodo.csv","Periodo",sep=";",value_col="Periodos")
# correciones
viajes = viajes[pd.notnull(viajes["HoraIni"])]
viajes = viajes[viajes["Imputada"] == 0].copy()
viajes["HoraIni"] = pd.to_timedelta(viajes["HoraIni"] + ":00")
viajes.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 100334 entries, 0 to 113590 Data columns (total 39 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Hogar 100334 non-null int64 1 Persona 100334 non-null int64 2 Viaje 100334 non-null int64 3 Etapas 100334 non-null int64 4 ComunaOrigen 98146 non-null object 5 ComunaDestino 98021 non-null object 6 SectorOrigen 98364 non-null object 7 SectorDestino 98281 non-null object 8 ZonaOrigen 100334 non-null int64 9 ZonaDestino 100334 non-null int64 10 OrigenCoordX 93715 non-null float64 11 OrigenCoordY 93715 non-null float64 12 DestinoCoordX 93714 non-null float64 13 DestinoCoordY 93714 non-null float64 14 Proposito 100333 non-null object 15 PropositoAgregado 100334 non-null float64 16 ActividadDestino 18979 non-null float64 17 MediosUsados 100334 non-null object 18 ModoAgregado 100334 non-null object 19 ModoPriPub 100334 non-null int64 20 ModoMotor 100334 non-null int64 21 HoraIni 100334 non-null timedelta64[ns] 22 HoraFin 100194 non-null object 23 HoraMedia 100319 non-null object 24 TiempoViaje 100192 non-null float64 25 TiempoMedio 100192 non-null float64 26 Periodo 100195 non-null object 27 MinutosDespues 94306 non-null float64 28 CuadrasDespues 94306 non-null float64 29 FactorLaboralNormal 65591 non-null float64 30 FactorSabadoNormal 8618 non-null float64 31 FactorDomingoNormal 6721 non-null float64 32 FactorLaboralEstival 14326 non-null float64 33 FactorFindesemanaEstival 5077 non-null float64 34 CodigoTiempo 65591 non-null float64 35 ModoDifusion 100334 non-null object 36 DistEuclidiana 100334 non-null int64 37 DistManhattan 100334 non-null int64 38 Imputada 100334 non-null int64 dtypes: float64(16), int64(11), object(11), timedelta64[ns](1) memory usage: 30.6+ MB
Nota: Algunas variables cualitativas están codificadas como números. Más adelante en el curso hablaremos de las diferencias entre atributos cuantitativos (
int64,float64) y los cualitativos (int64).
En algunos análisis querremos mezclar estas tablas. pandas nos facilita la vida gracias al método merge:
personas_hogar = personas.merge(hogares)
personas_hogar.shape
(60054, 59)
viajes_persona_hogar = viajes.merge(personas).merge(hogares)
viajes_persona_hogar.shape
(100334, 96)
viajes.head()
| Hogar | Persona | Viaje | Etapas | ComunaOrigen | ComunaDestino | SectorOrigen | SectorDestino | ZonaOrigen | ZonaDestino | ... | FactorLaboralNormal | FactorSabadoNormal | FactorDomingoNormal | FactorLaboralEstival | FactorFindesemanaEstival | CodigoTiempo | ModoDifusion | DistEuclidiana | DistManhattan | Imputada | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 173431 | 17343102 | 1734310202 | 1 | Maipú | Maipú | Poniente | Poniente | 400 | 407 | ... | 1.000000 | NaN | NaN | NaN | NaN | 0.0 | Bip! | 5387 | 7608 | 0 |
| 1 | 173441 | 17344101 | 1734410101 | 2 | Maipú | Las Condes | Poniente | Oriente | 407 | 307 | ... | 1.127220 | NaN | NaN | NaN | NaN | 0.0 | Bip! | 18841 | 26100 | 0 |
| 2 | 173441 | 17344101 | 1734410102 | 2 | Las Condes | Maipú | Oriente | Poniente | 307 | 407 | ... | 1.127220 | NaN | NaN | NaN | NaN | 0.0 | Bip! | 18841 | 26100 | 0 |
| 3 | 173441 | 17344103 | 1734410301 | 2 | Maipú | Ñuñoa | Poniente | Oriente | 407 | 437 | ... | 1.127220 | NaN | NaN | NaN | NaN | 0.0 | Bip! | 13392 | 17589 | 0 |
| 4 | 173441 | 17344103 | 1734410302 | 2 | Ñuñoa | Maipú | Oriente | Poniente | 437 | 407 | ... | 1.052764 | NaN | NaN | NaN | NaN | 0.0 | Bip! | 13392 | 17589 | 0 |
5 rows × 39 columns
Los siguientes pasos serán responder preguntas con estos datos.
¿Dónde están los hogares entrevistados?¶
La primera pregunta que podemos hacer busca entender quiénes han sido entrevistados. Observamos que cada hogar tiene coordenadas x e y, entonces, podemos utilizar una visualización básica para mostrar directamente x e y de cada hogar.
Hacemos el cálculo con una operación groupby:
hogares_x_comuna = hogares.groupby("Comuna").size().sort_values()
hogares_x_comuna
Comuna CALERA DE TANGO 152 PADRE HURTADO 157 TALAGANTE 157 ISLA DE MAIPO 166 EL MONTE 175 PIRQUE 176 BUIN 176 LAMPA 184 LO ESPEJO 196 CERRILLOS 200 SAN RAMON 217 PEÑAFLOR 225 INDEPENDENCIA 225 LO PRADO 228 LA CISTERNA 229 LO BARNECHEA 234 HUECHURABA 235 MELIPILLA 236 SAN JOAQUIN 244 VITACURA 254 COLINA 259 SAN MIGUEL 265 LA REINA 272 QUINTA NORMAL 278 MACUL 279 PEDRO AGUIRRE CERDA 296 LA GRANJA 304 CERRO NAVIA 306 CONCHALI 310 ESTACION CENTRAL 331 RENCA 344 RECOLETA 385 EL BOSQUE 415 PEÑALOLEN 489 PROVIDENCIA 502 LA PINTANA 545 QUILICURA 570 ÑUÑOA 603 PUDAHUEL 630 SAN BERNARDO 762 LAS CONDES 888 SANTIAGO 998 LA FLORIDA 1039 MAIPU 1466 PUENTE ALTO 1662 dtype: int64
¿Cómo haríamos el mismo cálculo considerando los factores de expansión? En vez de contar la cantidad de filas por cada comuna, tenemos que sumar el factor de expansión de cada hogar:
hogares_x_comuna = hogares.groupby("Comuna")["FactorHogar"].sum().sort_values()
hogares_x_comuna
Comuna PIRQUE 6393.000204 CALERA DE TANGO 6477.000217 ISLA DE MAIPO 7335.000203 EL MONTE 8405.999901 PADRE HURTADO 13943.000924 TALAGANTE 15280.999954 BUIN 20928.000414 LAMPA 22053.999653 MELIPILLA 23299.001092 PEÑAFLOR 23655.000068 CERRILLOS 23735.001069 INDEPENDENCIA 24535.999824 HUECHURABA 24960.000212 LO BARNECHEA 25554.000269 SAN RAMON 27026.000375 LA CISTERNA 27269.000017 SAN JOAQUIN 28169.999989 VITACURA 28731.999785 LA REINA 28956.999659 LO ESPEJO 28989.999366 COLINA 29381.001113 LO PRADO 30302.000940 SAN MIGUEL 30651.999192 PEDRO AGUIRRE CERDA 31835.999442 QUINTA NORMAL 34468.000265 MACUL 37326.999615 CONCHALI 37328.999468 LA GRANJA 37334.000273 CERRO NAVIA 39628.000065 ESTACION CENTRAL 39834.000005 RENCA 42543.999567 EL BOSQUE 48126.999224 RECOLETA 50610.000119 LA PINTANA 51689.000224 QUILICURA 56449.999658 PROVIDENCIA 64380.999773 PUDAHUEL 68050.999267 PEÑALOLEN 70227.999492 SAN BERNARDO 74922.997784 ÑUÑOA 76525.999693 LAS CONDES 103107.999004 LA FLORIDA 115075.999184 SANTIAGO 146801.000245 MAIPU 153245.998990 PUENTE ALTO 165758.997016 Name: FactorHogar, dtype: float64
Las Series y DataFrames de pandas tienen un método plot que nos permite graficar el resultado directamente, sin necesidad de crear la figura y los ejes:
hogares_x_comuna.plot(kind="barh")
plt.show()
Aunque claramente necesitamos trabajar en la apariencia del gráfico. Como usa matplotlib, y sabiendo que el método plot entrega el eje donde se grafica, podemos entregarle parámetros que conocimos y configurar el eje a nuestra pinta:
fig, ax = plt.subplots(figsize=(8,9))
ax = hogares_x_comuna.plot(kind="barh", width=0.9, ax=ax)
ax.set_xlabel("Cantidad de hogares")
ax.set_title("Distribución de hogares por comuna")
fig.tight_layout()
El gráfico responde nuestra pregunta. Sin embargo, quizás la interpretación de "dónde" es geográfica. En tal caso podemos utilizar los atributos DirCoordX y DirCoordY que vienen en la tabla, y usar un gráfico de dispersión (scatterplot):
hogares.plot(x="DirCoordX", y="DirCoordY", kind="scatter")
plt.show()
C:\Users\franc\AppData\Local\pypoetry\Cache\virtualenvs\db-connectors-RsEieBu8-py3.8\lib\site-packages\pandas\plotting\_matplotlib\core.py:1114: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored scatter = ax.scatter(
Reconocemos la forma de los puntos, sabemos que es Santiago. Ahora bien, quizás quisiéramos darles un color para representar otra variable, por ejemplo, el sector de la ciudad donde se ubican. Para ello el método plot de pandas por sí solo ya es limitado.
Aquí es donde podemos usar la biblioteca seaborn, que se construye sobre pandas y matplotlib. También incluye un scatterplot.
import seaborn as sns
El uso del método es similar al de pandas: debemos decirle cuál columna del DataFrame corresponde a cada eje del gráfico, x e y. Debemos entregarle también un parámetro data, y tenemos un parámetro adicional hue que recibe el nombre de la variable que se usará para pintar cada punto:
sns.scatterplot(x="DirCoordX", y="DirCoordY", data=hogares, hue="Sector")
plt.show()
Otra manera de interactuar con seaborn (y también con pandas) es a través de la creación manual de la figura. Luego le podemos decir al método scatterplot que utilice un ax específico para el gráfico:
fig, ax = plt.subplots(figsize=(12, 10))
sns.scatterplot(
x="DirCoordX", y="DirCoordY", data=hogares, hue="Sector", alpha=0.1, ax=ax
)
ax.set_title("Distribución de Hogares por Sector")
ax.set_axis_off()
fig.tight_layout()
Veremos más sobre visualización de datos geográficos en la clase de mapas.
¿Cuál es la distribución de edad de las personas encuestadas?¶
Sin visualización podemos utilizar el método describe que calcula estadística descriptiva simple para tener una primera aproximación de la respuesta:
personas["AnoNac"].describe()
count 60054.000000 mean 1975.421920 std 22.189995 min 1904.000000 25% 1959.000000 50% 1977.000000 75% 1994.000000 max 2013.000000 Name: AnoNac, dtype: float64
Sin embargo, uno podría tener más preguntas implícitas. ¿Cuándo nació más gente? ¿Cuál es la forma de la distribución?¿Tiene forma de campana?¿Es bimodal?
Podemos utilizar el método histplot (histogram plot) que se encarga de mostrarnos la distribución:
sns.histplot(data=personas, x='AnoNac')
plt.show()
Como observamos, el método se encarga de todo lo necesario para hacer el gráfico, incluso de determinar los rangos de las barras del histograma. Al igual que el gráfico anterior, podemos configurar todo a través de matplotlib:
fig, ax = plt.subplots(figsize=(8, 6))
sns.histplot(data=personas, x="AnoNac", ax=ax)
ax.set_xlabel("Año de nacimiento")
ax.set_ylabel("# de Personas")
ax.set_title("Distribución de Personas en la EOD por Año de Nacimiento")
fig.tight_layout()
Este gráfico responde la pregunta. Podríamos utilizar algunas opciones de histplot si queremos indagar más, pero lo dejaremos paralas siguientes clases.
¿Cuáles son los tipos de viaje y cuánto duran?¶
En este caso, lo que buscamos es saber para cada propósito de viaje el promedio de duración de éstos. Sin embargo, todes sabemos que existe variabilidad en los tiempos de viaje, ya que dependen de los modos de transporte utilizados y de la posición de los lugares de origen y destino. Entonces, necesitamos una manera de poder graficar esa variabilidad.
Una manera directa de hacerlo es utilizar un gráfico de barras para mostrar el promedio, junto con barras de error que codifican el intervalo de confianza del 95%. Esto lo permite el método barplot de seaborn:
sns.barplot(
y="Proposito", x="TiempoViaje", errorbar=('ci', 99), data=viajes_persona_hogar, color="magenta"
)
plt.show()
Ahora bien, existe también variabilidad en tanto los orígenes y destinos de los viajes tienen influecia en el tiempo de viaje, así como también lo tiene el modo de transporte. La misma función nos permite considerar estos factores:
fig, ax = plt.subplots(figsize=(7, 10))
sns.barplot(
y="Proposito",
x="TiempoViaje",
hue="Sector",
hue_order=["Poniente", "Centro", "Oriente", "Sur", "Norte"],
errorbar=('ci', 99),
data=viajes_persona_hogar,
palette="Set2",
ax=ax,
)
# esto quita algunos bordes del gráfico
sns.despine(ax=ax)
ax.set_title("Tiempo de Viaje por Sectores de la Ciudad")
ax.set_xlabel("Minutos")
ax.set_ylabel("")
fig.tight_layout()
El viaje al trabajo es el que toma más tiempo -- algo esperable. Al trabajo tenemos que ir esté donde esté. Pero hay diferencias notables de acuerdo al sector en el que viven las personas.
¿Cuál es la distribución de uso de modo de transporte en viajes al trabajo?¶
Una pregunta relevante siempre. En tiempos de COVID-19 está en debate si el uso de transporte público es un foco de contagio. Entender la distribución de usos de transporte por comuna es importante para la definición de estrategias de desconfinamiento.
Primero, debemos calcular la distribución de uso de modos de transporte por comuna.
modo_comuna = (
viajes_persona_hogar[viajes_persona_hogar["Proposito"] == "Al trabajo"]
.groupby(["Comuna", "ModoDifusion"])
.apply(lambda x: (x["FactorPersona"] * x["FactorLaboralNormal"]).sum())
.unstack(fill_value=0)
)
modo_comuna
| ModoDifusion | Auto | Bicicleta | Bip! | Bip! - Otros Privado | Bip! - Otros Público | Caminata | Otros | Taxi | Taxi Colectivo |
|---|---|---|---|---|---|---|---|---|---|
| Comuna | |||||||||
| BUIN | 2604.287917 | 1838.216760 | 0.000000 | 0.000000 | 576.978699 | 869.704559 | 3934.48018 | 0.000000 | 5681.632623 |
| CALERA DE TANGO | 1628.919020 | 642.495585 | 3.706586 | 16.158820 | 212.209146 | 909.887551 | 1087.09394 | 0.000000 | 291.329418 |
| CERRILLOS | 7278.168297 | 478.489772 | 5964.067992 | 87.431490 | 40.535112 | 402.210460 | 1863.36785 | 0.000000 | 352.205110 |
| CERRO NAVIA | 8706.967095 | 1034.222148 | 16864.492718 | 933.778450 | 404.005672 | 469.046166 | 820.43391 | 0.000000 | 242.490891 |
| COLINA | 6718.857099 | 370.928385 | 0.000000 | 0.000000 | 3220.845089 | 867.775136 | 8212.55015 | 502.411557 | 68.318662 |
| CONCHALI | 6299.645186 | 434.891474 | 17126.517983 | 0.000000 | 1604.135632 | 2957.574960 | 497.99987 | 49.149598 | 102.756385 |
| EL BOSQUE | 16445.271169 | 586.278278 | 14785.541405 | 715.383222 | 1172.337045 | 1907.773457 | 7171.06598 | 180.507835 | 1196.467388 |
| EL MONTE | 2860.131985 | 668.082644 | 55.245320 | 0.000000 | 581.060140 | 497.541306 | 1685.99493 | 0.000000 | 611.007226 |
| ESTACION CENTRAL | 13861.929832 | 4192.279694 | 11212.645868 | 174.581208 | 201.299562 | 2791.598117 | 704.01529 | 31.385195 | 369.339968 |
| HUECHURABA | 10567.805907 | 362.896408 | 7865.904119 | 124.104644 | 83.200940 | 1696.181215 | 140.36235 | 0.000000 | 0.000000 |
| INDEPENDENCIA | 7049.456489 | 479.658828 | 4290.214947 | 92.245456 | 37.126033 | 3757.574150 | 494.76924 | 39.946994 | 341.238672 |
| ISLA DE MAIPO | 1717.102923 | 1035.803274 | 0.000000 | 0.000000 | 343.387190 | 466.770180 | 1621.47675 | 0.000000 | 61.594354 |
| LA CISTERNA | 4912.993454 | 677.679351 | 5013.223944 | 166.864179 | 346.670014 | 1548.060657 | 700.80827 | 0.000000 | 1031.996882 |
| LA FLORIDA | 39849.657862 | 1019.116188 | 50257.042839 | 2346.100309 | 3305.184229 | 3688.291372 | 8686.11113 | 536.227848 | 1997.313116 |
| LA GRANJA | 11265.258210 | 745.511294 | 17513.244996 | 864.162371 | 1326.575360 | 4542.520134 | 1168.27267 | 117.423493 | 728.935024 |
| LA PINTANA | 13546.625876 | 1780.466735 | 30066.275046 | 1791.077982 | 2397.546290 | 1163.430326 | 2318.42142 | 245.597971 | 243.139134 |
| LA REINA | 16824.081676 | 802.586625 | 7764.121718 | 146.843572 | 250.921950 | 3182.166177 | 122.26073 | 794.655343 | 0.000000 |
| LAMPA | 5282.097556 | 710.775693 | 0.000000 | 0.000000 | 4059.491790 | 1763.177443 | 4521.96020 | 36.382405 | 322.075949 |
| LAS CONDES | 57469.862733 | 5618.557105 | 24339.769660 | 2307.469730 | 493.877852 | 9663.345311 | 1190.32459 | 2551.038137 | 0.000000 |
| LO BARNECHEA | 24087.789506 | 375.456593 | 4649.059991 | 13.670969 | 102.150568 | 1258.985908 | 163.53153 | 0.000000 | 179.893514 |
| LO ESPEJO | 9265.124719 | 988.177511 | 6494.811212 | 199.943689 | 419.324429 | 2187.933296 | 1321.49532 | 215.259349 | 859.015486 |
| LO PRADO | 3036.340451 | 209.716502 | 17354.628601 | 54.269463 | 375.516545 | 117.007819 | 1152.93832 | 159.768933 | 0.000000 |
| MACUL | 12006.096018 | 1544.053377 | 11827.193372 | 138.465662 | 296.734406 | 1832.776320 | 952.16157 | 0.000000 | 421.113594 |
| MAIPU | 44626.033264 | 3272.242485 | 51626.365739 | 2113.808223 | 3843.540015 | 4709.540371 | 12877.65188 | 179.131851 | 4847.982557 |
| MELIPILLA | 6715.669081 | 895.240722 | 0.000000 | 0.000000 | 5004.513097 | 1894.259706 | 2740.80208 | 11.200659 | 1819.031523 |
| PADRE HURTADO | 2419.059681 | 503.964859 | 563.188972 | 0.000000 | 2132.615949 | 398.066866 | 5038.28977 | 0.000000 | 619.227097 |
| PEDRO AGUIRRE CERDA | 16825.662876 | 589.247388 | 11092.871117 | 624.237377 | 2325.207464 | 1342.762502 | 3175.80817 | 539.059743 | 73.214659 |
| PEÑAFLOR | 4724.060215 | 1315.700200 | 0.000000 | 296.527305 | 3504.007266 | 796.970545 | 9640.83193 | 0.000000 | 855.071320 |
| PEÑALOLEN | 25646.419013 | 1409.311934 | 20756.485759 | 284.983292 | 592.636261 | 6376.664779 | 2525.33412 | 152.005714 | 465.356612 |
| PIRQUE | 2698.688628 | 146.633283 | 19.723293 | 45.206591 | 340.423360 | 130.540490 | 897.48657 | 0.000000 | 0.000000 |
| PROVIDENCIA | 30621.371523 | 4836.648080 | 10372.637809 | 79.610034 | 709.966324 | 3650.727556 | 9540.21896 | 1000.632171 | 0.000000 |
| PUDAHUEL | 16342.504667 | 729.644351 | 30665.502820 | 806.108336 | 2437.721997 | 2563.743385 | 2671.21258 | 525.106239 | 52.419229 |
| PUENTE ALTO | 29565.154755 | 4264.434827 | 58635.720224 | 3494.222546 | 6488.074040 | 4561.975734 | 5712.24263 | 305.471320 | 4097.389900 |
| QUILICURA | 10661.093482 | 5464.371927 | 23171.688073 | 1179.575830 | 1226.544366 | 2285.998626 | 1306.23892 | 896.073583 | 2056.782076 |
| QUINTA NORMAL | 8466.765274 | 822.206090 | 11123.127357 | 194.449763 | 56.256749 | 2093.875988 | 743.56336 | 0.000000 | 237.349574 |
| RECOLETA | 19720.480113 | 3655.395791 | 13474.814561 | 178.183427 | 607.094116 | 6196.260901 | 113.39506 | 0.000000 | 428.232789 |
| RENCA | 22531.535875 | 1476.788396 | 18471.541892 | 457.723874 | 308.408381 | 1836.615804 | 855.42596 | 0.000000 | 122.966073 |
| SAN BERNARDO | 20631.386410 | 5910.237474 | 13428.267808 | 629.530086 | 2278.850521 | 3809.231992 | 4241.40580 | 153.072382 | 4597.468776 |
| SAN JOAQUIN | 7626.677796 | 3060.318031 | 10583.115521 | 99.329415 | 429.647318 | 1269.774515 | 41.88562 | 0.000000 | 277.904015 |
| SAN MIGUEL | 10782.442801 | 410.109555 | 7794.372003 | 228.157139 | 1327.266822 | 1417.465114 | 117.43024 | 0.000000 | 59.120291 |
| SAN RAMON | 13775.813876 | 819.262462 | 9609.330184 | 1126.451906 | 938.653993 | 508.300911 | 675.97684 | 247.317172 | 353.957244 |
| SANTIAGO | 28702.228276 | 6953.107973 | 40638.612145 | 3786.748125 | 1380.806672 | 35315.310467 | 1308.56440 | 2620.399560 | 175.186679 |
| TALAGANTE | 3111.839231 | 785.539891 | 0.000000 | 0.000000 | 719.049262 | 269.558237 | 6029.91515 | 0.000000 | 4333.050668 |
| VITACURA | 21804.513246 | 538.862184 | 6504.761053 | 364.844007 | 357.555308 | 3387.548576 | 235.90843 | 27.213349 | 1283.308418 |
| ÑUÑOA | 34568.371855 | 6413.218643 | 22944.041823 | 801.969981 | 197.954922 | 8587.995246 | 278.30963 | 2815.733041 | 1383.450339 |
Veamos esta tabla como un gráfico que nos permita comparar la distribución por comunas.
def normalize_rows(df):
"""
Normaliza las filas de un DataFrame dividiendo cada valor de la fila por la suma total de la fila.
:param df: DataFrame que se desea normalizar.
:type df: pandas.DataFrame
:return: Un nuevo DataFrame con las filas normalizadas.
:rtype: pandas.DataFrame
"""
return df.div(df.sum(axis=1), axis=0)
def barchart(
ax,
df,
palette="plasma",
stacked=False,
normalize=False,
sort_items=False,
sort_categories=False,
fill_na_value=None,
bar_width=0.9,
legend=True,
legend_args=None,
return_df=False,
**kwargs
):
"""
Genera un gráfico de barras en un eje especificado (ax) a partir de un DataFrame (df) en Python.
:param ax: El eje en el que se dibujará el gráfico de barras.
:param df: El DataFrame que contiene los datos para el gráfico de barras.
:param palette: Paleta de colores a utilizar en las barras. Puede ser un nombre de paleta válido o una lista de colores personalizados.
Por defecto es "plasma".
:param stacked: Indica si las barras deben apilarse una encima de otra (True) o si deben colocarse una al lado de la otra (False).
Por defecto es False.
:param normalize: Indica si los valores de las barras deben normalizarse a la suma total (porcentajes).
Por defecto es False.
:param sort_items: Indica si se deben ordenar los ítems (barras) en función de sus valores.
Por defecto es False.
:param sort_categories: Indica si se deben ordenar las categorías (eje x) en función de sus valores.
Por defecto es False.
:param fill_na_value: Valor que se utilizará para rellenar los valores faltantes en el DataFrame.
Por defecto es None.
:param bar_width: Ancho de las barras. Un valor más cercano a 1 creará barras más anchas y cercanas entre sí.
Por defecto es 0.9.
:param legend: Indica si se debe mostrar la leyenda en el gráfico.
Por defecto es True.
:param legend_args: Argumentos adicionales para personalizar la apariencia de la leyenda.
Por defecto es None.
:param return_df: Indica si se debe devolver el DataFrame transformado después de aplicar posibles modificaciones.
Por defecto es False.
:param **kwargs: Argumentos adicionales para personalizar el aspecto de las barras. Estos argumentos serán pasados a la función
de trazado de barras (barplot) de Matplotlib.
:return: Si return_df es True, la función devuelve el DataFrame transformado. De lo contrario, devuelve None.
"""
sns.set_palette(palette, n_colors=len(df.columns))
if fill_na_value is not None:
df = df.fillna(fill_na_value)
if normalize:
df = df.pipe(normalize_rows)
if sort_categories:
sort_values = df.mean(axis=0).sort_values(ascending=False)
df = df[sort_values.index].copy()
if sort_items:
df = df.sort_values(df.columns[0])
df.plot.bar(
ax=ax,
stacked=stacked,
width=bar_width,
edgecolor="none",
legend=legend,
**kwargs
)
if legend:
if legend_args is None:
legend_args = dict(
bbox_to_anchor=(1.0, 0.5), loc="center left", frameon=False
)
handles, labels = map(reversed, ax.get_legend_handles_labels())
ax.legend(handles, labels, **legend_args)
ax.ticklabel_format(axis="y", useOffset=False, style="plain")
sns.despine(ax=ax, left=True)
if normalize:
ax.set_ylim([0, 1])
if return_df:
return df
fig, ax = plt.subplots(figsize=(14, 7))
barchart(
ax, modo_comuna, stacked=True, normalize=True, sort_categories=True, sort_items=True
)
ax.set_title("Uso de Modo de Transporte en Viajes al Trabajo (Día Laboral)")
ax.set_ylim([0, 1])
ax.set_xlabel("")
ax.set_ylabel("Fracción de los Viajes")
fig.tight_layout()
# fig.savefig('../reports/figures/example_barchart.png', dpi=150, bbox_inches='tight')
¡Es un gráfico interesante! Sin embargo está complejo, ya que cuesta diferenciar y comparar las distintas categorías. Creemos una categorización más sencilla que nos permita comparar mejor:
viajes_persona_hogar["ModoAgregado"] = viajes_persona_hogar["ModoDifusion"].map(
{
"Taxi": "Taxi",
"Bip! - Otros Privado": "Público",
"Bip!": "Público",
"Bip! - Otros Público": "Público",
"Taxi Colectivo": "Taxi",
"Bicicleta": "Activo",
"Caminata": "Activo",
"Auto": "Auto",
"Otros": "Otros",
}
)
modo_comuna = (
viajes_persona_hogar[viajes_persona_hogar["Proposito"] == "Al trabajo"]
.groupby(["Comuna", "ModoAgregado"])
.apply(lambda x: (x["FactorPersona"] * x["FactorLaboralNormal"]).sum())
.unstack(fill_value=0)
)
modo_comuna
| ModoAgregado | Activo | Auto | Otros | Público | Taxi |
|---|---|---|---|---|---|
| Comuna | |||||
| BUIN | 2707.921319 | 2604.287917 | 3934.48018 | 576.978699 | 5681.632623 |
| CALERA DE TANGO | 1552.383136 | 1628.919020 | 1087.09394 | 232.074553 | 291.329418 |
| CERRILLOS | 880.700232 | 7278.168297 | 1863.36785 | 6092.034594 | 352.205110 |
| CERRO NAVIA | 1503.268313 | 8706.967095 | 820.43391 | 18202.276840 | 242.490891 |
| COLINA | 1238.703521 | 6718.857099 | 8212.55015 | 3220.845089 | 570.730219 |
| CONCHALI | 3392.466434 | 6299.645186 | 497.99987 | 18730.653615 | 151.905982 |
| EL BOSQUE | 2494.051735 | 16445.271169 | 7171.06598 | 16673.261672 | 1376.975223 |
| EL MONTE | 1165.623951 | 2860.131985 | 1685.99493 | 636.305460 | 611.007226 |
| ESTACION CENTRAL | 6983.877811 | 13861.929832 | 704.01529 | 11588.526637 | 400.725164 |
| HUECHURABA | 2059.077623 | 10567.805907 | 140.36235 | 8073.209703 | 0.000000 |
| INDEPENDENCIA | 4237.232978 | 7049.456489 | 494.76924 | 4419.586435 | 381.185666 |
| ISLA DE MAIPO | 1502.573454 | 1717.102923 | 1621.47675 | 343.387190 | 61.594354 |
| LA CISTERNA | 2225.740008 | 4912.993454 | 700.80827 | 5526.758137 | 1031.996882 |
| LA FLORIDA | 4707.407560 | 39849.657862 | 8686.11113 | 55908.327377 | 2533.540964 |
| LA GRANJA | 5288.031428 | 11265.258210 | 1168.27267 | 19703.982727 | 846.358517 |
| LA PINTANA | 2943.897061 | 13546.625876 | 2318.42142 | 34254.899318 | 488.737105 |
| LA REINA | 3984.752802 | 16824.081676 | 122.26073 | 8161.887240 | 794.655343 |
| LAMPA | 2473.953136 | 5282.097556 | 4521.96020 | 4059.491790 | 358.458354 |
| LAS CONDES | 15281.902415 | 57469.862733 | 1190.32459 | 27141.117242 | 2551.038137 |
| LO BARNECHEA | 1634.442502 | 24087.789506 | 163.53153 | 4764.881528 | 179.893514 |
| LO ESPEJO | 3176.110806 | 9265.124719 | 1321.49532 | 7114.079330 | 1074.274835 |
| LO PRADO | 326.724321 | 3036.340451 | 1152.93832 | 17784.414610 | 159.768933 |
| MACUL | 3376.829697 | 12006.096018 | 952.16157 | 12262.393441 | 421.113594 |
| MAIPU | 7981.782856 | 44626.033264 | 12877.65188 | 57583.713977 | 5027.114407 |
| MELIPILLA | 2789.500428 | 6715.669081 | 2740.80208 | 5004.513097 | 1830.232182 |
| PADRE HURTADO | 902.031725 | 2419.059681 | 5038.28977 | 2695.804921 | 619.227097 |
| PEDRO AGUIRRE CERDA | 1932.009891 | 16825.662876 | 3175.80817 | 14042.315957 | 612.274401 |
| PEÑAFLOR | 2112.670745 | 4724.060215 | 9640.83193 | 3800.534571 | 855.071320 |
| PEÑALOLEN | 7785.976714 | 25646.419013 | 2525.33412 | 21634.105312 | 617.362326 |
| PIRQUE | 277.173774 | 2698.688628 | 897.48657 | 405.353244 | 0.000000 |
| PROVIDENCIA | 8487.375635 | 30621.371523 | 9540.21896 | 11162.214167 | 1000.632171 |
| PUDAHUEL | 3293.387735 | 16342.504667 | 2671.21258 | 33909.333153 | 577.525468 |
| PUENTE ALTO | 8826.410560 | 29565.154755 | 5712.24263 | 68618.016810 | 4402.861220 |
| QUILICURA | 7750.370552 | 10661.093482 | 1306.23892 | 25577.808269 | 2952.855658 |
| QUINTA NORMAL | 2916.082079 | 8466.765274 | 743.56336 | 11373.833869 | 237.349574 |
| RECOLETA | 9851.656692 | 19720.480113 | 113.39506 | 14260.092103 | 428.232789 |
| RENCA | 3313.404200 | 22531.535875 | 855.42596 | 19237.674148 | 122.966073 |
| SAN BERNARDO | 9719.469466 | 20631.386410 | 4241.40580 | 16336.648415 | 4750.541159 |
| SAN JOAQUIN | 4330.092546 | 7626.677796 | 41.88562 | 11112.092255 | 277.904015 |
| SAN MIGUEL | 1827.574670 | 10782.442801 | 117.43024 | 9349.795965 | 59.120291 |
| SAN RAMON | 1327.563373 | 13775.813876 | 675.97684 | 11674.436084 | 601.274416 |
| SANTIAGO | 42268.418440 | 28702.228276 | 1308.56440 | 45806.166941 | 2795.586240 |
| TALAGANTE | 1055.098128 | 3111.839231 | 6029.91515 | 719.049262 | 4333.050668 |
| VITACURA | 3926.410761 | 21804.513246 | 235.90843 | 7227.160368 | 1310.521766 |
| ÑUÑOA | 15001.213889 | 34568.371855 | 278.30963 | 23943.966726 | 4199.183379 |
fig, ax = plt.subplots(figsize=(14, 7))
barchart(
ax,
modo_comuna.drop("Otros", axis=1),
stacked=True,
normalize=True,
sort_categories=True,
sort_items=True,
)
ax.set_title("Uso de Modo de Transporte en Viajes al Trabajo (Día Laboral)")
ax.set_ylim([0, 1])
ax.set_xlabel("")
ax.set_ylabel("Fracción de los Viajes")
fig.tight_layout()
Verlo nos lleva a preguntarnos si existe una relación entre las propiedades de una comuna y su uso de transporte público.
Para ello calcularemos el ingreso promedio en cada comuna. Al igual que con los viajes, debemos utilizar el ingreso considerando los factores de expansión:
ingreso_por_comuna = (
hogares.groupby("Comuna")
.apply(
lambda x: (x["FactorHogar"] * x["IngresoHogar"]).sum() / x["FactorHogar"].sum()
)
.rename("ingreso")
)
ingreso_por_comuna
Comuna BUIN 5.753514e+05 CALERA DE TANGO 5.059952e+05 CERRILLOS 5.287505e+05 CERRO NAVIA 4.213678e+05 COLINA 6.552432e+05 CONCHALI 6.050698e+05 EL BOSQUE 5.143629e+05 EL MONTE 4.247368e+05 ESTACION CENTRAL 4.832393e+05 HUECHURABA 7.918456e+05 INDEPENDENCIA 6.057736e+05 ISLA DE MAIPO 4.074196e+05 LA CISTERNA 6.160750e+05 LA FLORIDA 7.147872e+05 LA GRANJA 5.591557e+05 LA PINTANA 4.193542e+05 LA REINA 1.521881e+06 LAMPA 6.482933e+05 LAS CONDES 1.460452e+06 LO BARNECHEA 2.182363e+06 LO ESPEJO 4.978187e+05 LO PRADO 4.596349e+05 MACUL 7.739653e+05 MAIPU 5.953841e+05 MELIPILLA 4.594306e+05 PADRE HURTADO 4.827470e+05 PEDRO AGUIRRE CERDA 5.462924e+05 PEÑAFLOR 4.846970e+05 PEÑALOLEN 7.617944e+05 PIRQUE 7.117509e+05 PROVIDENCIA 1.370451e+06 PUDAHUEL 5.375919e+05 PUENTE ALTO 5.685155e+05 QUILICURA 5.940065e+05 QUINTA NORMAL 5.127847e+05 RECOLETA 6.350697e+05 RENCA 5.203086e+05 SAN BERNARDO 4.962993e+05 SAN JOAQUIN 5.121281e+05 SAN MIGUEL 8.023939e+05 SAN RAMON 5.232375e+05 SANTIAGO 7.423958e+05 TALAGANTE 4.606406e+05 VITACURA 1.666897e+06 ÑUÑOA 1.145987e+06 Name: ingreso, dtype: float64
Ahora que tenemos esta serie, podemos hacer un cruce entre las dos tablas que hemos calculado. utilizamos la función normalize_rows para normalizar los valores de cada comuna, y así poder compararlas:
modo_comuna_ingreso = modo_comuna.pipe(normalize_rows).join(ingreso_por_comuna)
modo_comuna_ingreso
| Activo | Auto | Otros | Público | Taxi | ingreso | |
|---|---|---|---|---|---|---|
| Comuna | ||||||
| BUIN | 0.174645 | 0.167961 | 0.253751 | 0.037212 | 0.366432 | 5.753514e+05 |
| CALERA DE TANGO | 0.323967 | 0.339939 | 0.226865 | 0.048432 | 0.060797 | 5.059952e+05 |
| CERRILLOS | 0.053484 | 0.441999 | 0.113161 | 0.369966 | 0.021389 | 5.287505e+05 |
| CERRO NAVIA | 0.051001 | 0.295397 | 0.027834 | 0.617541 | 0.008227 | 4.213678e+05 |
| COLINA | 0.062054 | 0.336588 | 0.411416 | 0.161351 | 0.028591 | 6.552432e+05 |
| CONCHALI | 0.116689 | 0.216686 | 0.017129 | 0.644270 | 0.005225 | 6.050698e+05 |
| EL BOSQUE | 0.056477 | 0.372397 | 0.162386 | 0.377559 | 0.031181 | 5.143629e+05 |
| EL MONTE | 0.167497 | 0.410994 | 0.242273 | 0.091436 | 0.087800 | 4.247368e+05 |
| ESTACION CENTRAL | 0.208231 | 0.413307 | 0.020991 | 0.345523 | 0.011948 | 4.832393e+05 |
| HUECHURABA | 0.098802 | 0.507081 | 0.006735 | 0.387382 | 0.000000 | 7.918456e+05 |
| INDEPENDENCIA | 0.255529 | 0.425121 | 0.029837 | 0.266525 | 0.022988 | 6.057736e+05 |
| ISLA DE MAIPO | 0.286415 | 0.327308 | 0.309080 | 0.065455 | 0.011741 | 4.074196e+05 |
| LA CISTERNA | 0.154584 | 0.341220 | 0.048673 | 0.383848 | 0.071675 | 6.160750e+05 |
| LA FLORIDA | 0.042149 | 0.356804 | 0.077773 | 0.500589 | 0.022685 | 7.147872e+05 |
| LA GRANJA | 0.138170 | 0.294348 | 0.030526 | 0.514842 | 0.022114 | 5.591557e+05 |
| LA PINTANA | 0.054972 | 0.252959 | 0.043292 | 0.639650 | 0.009126 | 4.193542e+05 |
| LA REINA | 0.133324 | 0.562911 | 0.004091 | 0.273086 | 0.026588 | 1.521881e+06 |
| LAMPA | 0.148177 | 0.316370 | 0.270842 | 0.243142 | 0.021470 | 6.482933e+05 |
| LAS CONDES | 0.147460 | 0.554545 | 0.011486 | 0.261893 | 0.024616 | 1.460452e+06 |
| LO BARNECHEA | 0.053014 | 0.781296 | 0.005304 | 0.154551 | 0.005835 | 2.182363e+06 |
| LO ESPEJO | 0.144690 | 0.422080 | 0.060202 | 0.324088 | 0.048939 | 4.978187e+05 |
| LO PRADO | 0.014547 | 0.135188 | 0.051333 | 0.791820 | 0.007113 | 4.596349e+05 |
| MACUL | 0.116368 | 0.413738 | 0.032812 | 0.422570 | 0.014512 | 7.739653e+05 |
| MAIPU | 0.062311 | 0.348379 | 0.100531 | 0.449535 | 0.039245 | 5.953841e+05 |
| MELIPILLA | 0.146195 | 0.351961 | 0.143643 | 0.262281 | 0.095921 | 4.594306e+05 |
| PADRE HURTADO | 0.077266 | 0.207210 | 0.431567 | 0.230916 | 0.053041 | 4.827470e+05 |
| PEDRO AGUIRRE CERDA | 0.052804 | 0.459867 | 0.086799 | 0.383795 | 0.016734 | 5.462924e+05 |
| PEÑAFLOR | 0.099969 | 0.223538 | 0.456194 | 0.179837 | 0.040461 | 4.846970e+05 |
| PEÑALOLEN | 0.133759 | 0.440590 | 0.043384 | 0.371661 | 0.010606 | 7.617944e+05 |
| PIRQUE | 0.064780 | 0.630726 | 0.209757 | 0.094737 | 0.000000 | 7.117509e+05 |
| PROVIDENCIA | 0.139568 | 0.503543 | 0.156881 | 0.183553 | 0.016455 | 1.370451e+06 |
| PUDAHUEL | 0.057988 | 0.287751 | 0.047033 | 0.597059 | 0.010169 | 5.375919e+05 |
| PUENTE ALTO | 0.075359 | 0.252425 | 0.048771 | 0.585854 | 0.037591 | 5.685155e+05 |
| QUILICURA | 0.160635 | 0.220963 | 0.027073 | 0.530128 | 0.061201 | 5.940065e+05 |
| QUINTA NORMAL | 0.122847 | 0.356682 | 0.031324 | 0.479149 | 0.009999 | 5.127847e+05 |
| RECOLETA | 0.222015 | 0.444417 | 0.002555 | 0.321362 | 0.009651 | 6.350697e+05 |
| RENCA | 0.071935 | 0.489167 | 0.018572 | 0.417656 | 0.002670 | 5.203086e+05 |
| SAN BERNARDO | 0.174561 | 0.370539 | 0.076175 | 0.293405 | 0.085319 | 4.962993e+05 |
| SAN JOAQUIN | 0.185136 | 0.326085 | 0.001791 | 0.475106 | 0.011882 | 5.121281e+05 |
| SAN MIGUEL | 0.082560 | 0.487092 | 0.005305 | 0.422373 | 0.002671 | 8.023939e+05 |
| SAN RAMON | 0.047320 | 0.491028 | 0.024095 | 0.416126 | 0.021432 | 5.232375e+05 |
| SANTIAGO | 0.349670 | 0.237442 | 0.010825 | 0.378936 | 0.023127 | 7.423958e+05 |
| TALAGANTE | 0.069192 | 0.204069 | 0.395431 | 0.047154 | 0.284154 | 4.606406e+05 |
| VITACURA | 0.113794 | 0.631932 | 0.006837 | 0.209456 | 0.037981 | 1.666897e+06 |
| ÑUÑOA | 0.192345 | 0.443235 | 0.003568 | 0.307009 | 0.053842 | 1.145987e+06 |
Para comparar el uso de transporte público y el ingreso poddemos utilizar un scatterplot:
modo_comuna_ingreso.plot(x="ingreso", y="Público", kind="scatter")
C:\Users\franc\AppData\Local\pypoetry\Cache\virtualenvs\db-connectors-RsEieBu8-py3.8\lib\site-packages\pandas\plotting\_matplotlib\core.py:1114: UserWarning: No data for colormapping provided via 'c'. Parameters 'cmap' will be ignored scatter = ax.scatter(
<Axes: xlabel='ingreso', ylabel='Público'>
Aunque nos gustaría saber cuál es la comuna que corresponde a cada punto, el gráfico no lo dice. Además el eje x utiliza una notación que nos impide apreciar los valores totales. Para ello podemos utilizar el método scatterplot, en conjunto con configuraciones de matplotlib:
import matplotlib.patheffects as path_effects
from adjustText import adjust_text
class LabelCollection(object):
def __init__(self, default_ha="center", default_va="center"):
self.elements = []
self.ha = default_ha
self.va = default_va
def add_text(self, s, x, y, ha=None, va=None):
self.elements.append((s, x, y, ha, va))
def render(
self,
ax,
fig=None,
tight_figure=True,
avoid_collisions=False,
outline=False,
adjustment_args={},
outline_args={"linewidth": 2, "foreground": "black"},
**kwargs
):
rendered = []
for s, x, y, ha, va in self.elements:
if ha is None:
ha = self.ha
if va is None:
va = self.va
text = ax.text(x, y, s, ha=ha, va=va, **kwargs)
if outline:
text.set_path_effects(
[
path_effects.Stroke(**outline_args),
path_effects.Normal(),
]
)
rendered.append(text)
if fig is not None and tight_figure:
fig.tight_layout()
if avoid_collisions:
adjust_text(rendered, ax=ax, **adjustment_args)
def scatterplot(
ax,
df,
x,
y,
hue=None,
annotate=False,
avoid_collisions=False,
scatter_args={},
text_args={},
na_value=0,
drop_na=False,
adjustment_args={"lim": 5},
label_filter_func=None,
):
"""
Genera un gráfico de dispersión (scatter plot) en un eje especificado (ax) a partir de un DataFrame (df) en Python.
:param ax: El eje en el que se dibujará el gráfico de dispersión.
:param df: El DataFrame que contiene los datos para el gráfico de dispersión.
:param x: El nombre de la columna del DataFrame `df` que se utilizará en el eje x del gráfico.
:param y: El nombre de la columna del DataFrame `df` que se utilizará en el eje y del gráfico.
:param hue: El nombre de la columna del DataFrame `df` que se utilizará para diferenciar los puntos en el gráfico.
Por defecto es None, lo que indica que no se utilizará ninguna columna adicional para diferenciar los puntos.
:param annotate: Indica si se deben agregar etiquetas (textos) cerca de los puntos en el gráfico.
Por defecto es False.
:param avoid_collisions: Indica si las etiquetas (textos) deben evitarse para que no se superpongan.
Por defecto es False.
:param scatter_args: Argumentos adicionales para personalizar la apariencia del gráfico de dispersión.
Estos argumentos serán pasados a la función de trazado de dispersión (scatter) de Matplotlib.
:param text_args: Argumentos adicionales para personalizar la apariencia de las etiquetas (textos) si annotate es True.
Estos argumentos serán pasados a la función de agregado de texto (annotate) de Matplotlib.
:param na_value: Valor que se utilizará para reemplazar los valores faltantes (NaN) en las columnas x e y del DataFrame.
Por defecto es 0.
:param drop_na: Indica si se deben eliminar las filas con valores faltantes en las columnas x e y antes de graficar.
Por defecto es False.
:param adjustment_args: Argumentos para controlar el ajuste automático de las etiquetas (textos) en el gráfico.
Por defecto, se utiliza {"lim": 5}, que ajusta automáticamente las etiquetas para evitar superposiciones
cuando se detectan colisiones.
:param label_filter_func: Función que permite filtrar las etiquetas que se agregarán en el gráfico si annotate es True.
La función debe recibir como argumento un valor de la columna hue y devolver True o False.
Si no se proporciona una función, se agregarán todas las etiquetas.
:return: None
"""
if not drop_na:
df = df.fillna(na_value)
else:
df = df.dropna()
sns.scatterplot(data=df, x=x, y=y, hue=hue, ax=ax, **scatter_args)
ax.ticklabel_format(useOffset=False, style="plain")
sns.despine(ax=ax)
if annotate:
collection = LabelCollection()
if label_filter_func is not None:
label_df = df.pipe(label_filter_func)
else:
label_df = df
for index, row in label_df.iterrows():
collection.add_text(index, row[x], row[y])
collection.render(
ax,
avoid_collisions=avoid_collisions,
adjustment_args=adjustment_args,
**text_args
)
fig, ax = plt.subplots(1, 1, figsize=(16, 8))
scatterplot(
ax,
modo_comuna_ingreso,
"ingreso",
"Público",
annotate=True,
avoid_collisions=True,
text_args=dict(fontsize="small"),
scatter_args=dict(color="purple"),
)
ax.set_xlabel("Ingreso Promedio por Hogar")
ax.set_ylabel("Proporción de Uso de Transporte Público")
ax.set_title(
"Relación entre Uso de Transporte Público e Ingreso por Comunas de RM (Fuente: EOD2012)"
)
ax.grid(alpha=0.5)
ax.ticklabel_format(style="plain")
sns.despine(ax=ax, left=True, bottom=True, right=True, top=True)
# fig.savefig('../reports/figures/example_scatterplot.png', dpi=150, bbox_inches='tight')
Observamos que los tres grupos de uso de transporte público son: las comunas fuera del radio urbano (esquina inferior izquierda), que no son más ricas que el resto y no usan transantiago porque no llega a ellas; las comunas ricas (inferior derecha), que casi no usan transporte público a pesar de estar bien conectadas; y el resto, que presenta tasas variables de uso de transporte público.
Conclusiones¶
Con estas herramientas podemos explorar las relaciones que hay entre las variables de nuestro dataset. Concluimos que la mayor dificultad no está en implementar las visualizaciones, sino en, primero, saber qué preguntarle a los datos, y segundo, elegir los métodos adecuados para responder la pregunta. Probablemente seaborn, pandas o matplotlib tienen dicha solución implementada, o al menos a unos pasos de ser implementada. También podemos utilizar los métodos implementados en aves.
El siguiente paso es entender cómo se comportan estos métodos con otras variables del dataset. También hemos probado distintos valores para atributos de apariencia, como los tamaños de figura y las paletas de colores.
Una dificultad en el aprendizaje es que no existen estándares para nombrar a los métodos y sus parámetros. Por ejemplo, el parámetro de la paleta de colores se suele llamar cmap en matplotlib y pandas, pero se llama palette en casi todos los métodos de seaborn --- digo casi todos porque algunos también usan cmap. Esto puede ser confuso para aprender, y creo que de momento no hay una solución más que ejercitar y aprenderse los nombres de parámetros y de métodos que sean más adecuados para la tarea a resolver.