¡Hola a todos! por aqui Sunday – The Quant, y en este artículo, te guiaré a través del fascinante mundo del Análisis Exploratorio de Datos (EDA) utilizando Polars, la librería de procesamiento de datos ultrarrápida de Python. Exploraremos cómo transformar datos crudos en conocimientos valiosos, utilizando técnicas de limpieza, preprocesamiento, estadísticas descriptivas y visualización. ¡Prepárate para optimizar tu flujo de trabajo de EDA!

Key Takeaways: Lo que aprenderás

  • Cómo cargar y entender un dataset con Polars.
  • Técnicas esenciales para la limpieza y preprocesamiento de datos.
  • Cómo obtener estadísticas descriptivas y comprender las distribuciones de tus variables.
  • Métodos para identificar y manejar valores atípicos.
  • La importancia del análisis de correlación y la ingeniería de características.
  • Cómo crear visualizaciones impactantes para comunicar tus hallazgos.
  • Consideraciones avanzadas de Polars para optimizar el rendimiento.

1. Preparando nuestro Entorno de Trabajo

Antes de sumergirnos en el código, es fundamental asegurarnos de que tenemos todas las herramientas necesarias instaladas. Para este tutorial, utilizaremos Polars para el procesamiento de datos y Matplotlib y Seaborn para las visualizaciones. Te recomiendo crear un entorno virtual para mantener tus dependencias organizadas.

Aquí te muestro cómo puedes configurar tu entorno:


# Crear un entorno virtual (si aún no tienes uno)
python -m venv polars_eda_env
source polars_eda_env/bin/activate  # En Linux/macOS
# polars_eda_env\Scripts\activate  # En Windows

# Instalar las librerías necesarias
pip install polars pandas matplotlib seaborn jupyterlab
                


Una vez activado tu entorno y con las librerías instaladas, ¡estamos listos para comenzar!

2. El Dataset: Compartiendo Bicicletas en Washington D.C.

Para este EDA, he seleccionado el popular “Bike Sharing Dataset” de la Universidad de California, Irvine (UCI). En particular, nos centraremos en el archivo day.csv, que contiene el recuento diario de alquileres de bicicletas en el sistema Capital Bikeshare de Washington D.C. durante 2011 y 2012, junto con información meteorológica y estacional.

Este dataset es ideal para un EDA por su tamaño manejable (731 filas, 16 columnas) y la variedad de tipos de datos, lo que nos permitirá explorar diferentes facetas del análisis.

3. Carga de Datos y Primera Inspección con Polars

La carga de datos con Polars es increíblemente eficiente. Usaremos pl.read_csv para cargar nuestro dataset directamente desde la URL. Luego, realizaremos una primera inspección para entender la estructura de los datos.


import polars as pl
import matplotlib.pyplot as plt
import seaborn as sns

# URL directa del dataset day.csv
DATA_URL = "https://raw.githubusercontent.com/danwild/bike-share-prediction/master/Bike-Sharing-Dataset/day.csv"

# Cargar el dataset con Polars
df = pl.read_csv(DATA_URL)

print("### Primeras 5 filas del dataset:")
print(df.head())

print("\n### Esquema del dataset:")
print(df.schema)

print("\n### Estadísticas descriptivas básicas:")
print(df.describe())
                


Interpretación:

  • df.head() nos muestra las primeras filas, dándonos una vista rápida de los datos.
  • df.schema es crucial. Nos revela el nombre de las columnas y sus tipos de datos inferidos por Polars. Observo columnas como dteday (fecha), season, yr, mnth, holiday, weekday, workingday, weathersit (categóricas), y numéricas como temp, atemp, hum, windspeed, casual, registered, y cnt (conteo total de alquileres).
  • df.describe() nos da un resumen estadístico de las columnas numéricas: conteo, nulos, media, desviación estándar, mínimos, cuartiles y máximos. Esto es útil para detectar posibles valores atípicos o rangos inesperados.

4. Limpieza y Preprocesamiento Básico

Antes de un análisis más profundo, es bueno asegurar que nuestros datos estén en el formato correcto y listos para ser analizados. Esto incluye manejar fechas y convertir algunas columnas a tipos categóricos para un mejor manejo.


# 1. Convertir 'dteday' a tipo fecha (Date)
df = df.with_columns(
    pl.col("dteday").str.to_date("%Y-%m-%d")
)

# 2. Convertir columnas categóricas a tipo Categorical
# Esto es eficiente para Polars y mejora algunas operaciones
categorical_cols = ["season", "yr", "mnth", "holiday", "weekday", "workingday", "weathersit"]
df = df.with_columns([
    pl.col(c).cast(pl.Categorical) for c in categorical_cols
])

# 3. Renombrar algunas columnas para mayor claridad
df = df.rename({
    "yr": "year",
    "mnth": "month",
    "weathersit": "weather_situation",
    "cnt": "total_rentals",
    "hum": "humidity",
    "temp": "temperature",
    "atemp": "feels_like_temperature"
})

print("\n### Esquema del dataset después del preprocesamiento:")
print(df.schema)

print("\n### Primeras filas después del preprocesamiento:")
print(df.head())
                


Interpretación:

  • Hemos transformado dteday a un tipo de dato Date, lo cual es fundamental para operaciones basadas en tiempo.
  • Las columnas categóricas ahora son de tipo Categorical, lo que optimiza el uso de memoria y acelera las operaciones de agrupación y conteo en Polars.
  • Renombrar columnas hace que el dataset sea más legible y comprensible.

5. Estadísticas Descriptivas y Análisis de Distribución

Las estadísticas descriptivas nos dan un panorama general de la tendencia central, dispersión y forma de nuestras variables. El análisis de distribución, a menudo con histogramas, nos ayuda a entender cómo se distribuyen los valores de una variable.

5.1. Estadísticas para variables numéricas

Polars facilita el cálculo de estas estadísticas de forma concisa.


print("\n### Estadísticas descriptivas de columnas numéricas clave:")
numerical_summary = df.select([
    pl.col("total_rentals").mean().alias("mean_rentals"),
    pl.col("total_rentals").median().alias("median_rentals"),
    pl.col("total_rentals").std().alias("std_rentals"),
    pl.col("total_rentals").quantile(0.25).alias("q25_rentals"),
    pl.col("total_rentals").quantile(0.75).alias("q75_rentals"),
    pl.col("temperature").mean().alias("mean_temp"),
    pl.col("humidity").mean().alias("mean_humidity"),
    pl.col("windspeed").mean().alias("mean_windspeed")
]).collect() # .collect() is needed if using lazy evaluation, but here it's eager.

print(numerical_summary)
                


📊 Salida:

### Estadísticas descriptivas de columnas numéricas clave:


Interpretación:

  • Vemos que el promedio diario de alquileres es de aproximadamente 4504, con una mediana muy similar, lo que sugiere una distribución relativamente simétrica para total_rentals.
  • La desviación estándar de 1936 nos indica una variabilidad considerable en el número de alquileres diarios.
  • Las temperaturas, humedad y velocidad del viento muestran promedios esperados para un sistema de bicicletas compartidas.

5.2. Análisis de Distribución (Visualizaciones)

Los histogramas son perfectos para visualizar la distribución de variables numéricas.


# Configuración para las visualizaciones
sns.set_style("whitegrid")
plt.figure(figsize=(15, 5))

# Histograma de la distribución de 'total_rentals'
plt.subplot(1, 3, 1)
sns.histplot(df["total_rentals"].to_pandas(), bins=30, kde=True)
plt.title('Distribución de Alquileres Totales Diarios')
plt.xlabel('Número de Alquileres')
plt.ylabel('Frecuencia')

# Histograma de la distribución de 'temperature'
plt.subplot(1, 3, 2)
sns.histplot(df["temperature"].to_pandas(), bins=30, kde=True)
plt.title('Distribución de la Temperatura Normalizada')
plt.xlabel('Temperatura Normalizada')
plt.ylabel('Frecuencia')

# Histograma de la distribución de 'humidity'
plt.subplot(1, 3, 3)
sns.histplot(df["humidity"].to_pandas(), bins=30, kde=True)
plt.title('Distribución de la Humedad Normalizada')
plt.xlabel('Humedad Normalizada')
plt.ylabel('Frecuencia')

plt.tight_layout()
plt.show() # Placeholder for Plot 1
                
[Aquí se mostraría el Plot 1: Histogramas de total_rentals, temperature y humidity]


Interpretación del Plot 1:

  • La distribución de total_rentals parece ser bimodal, con picos alrededor de 2000 y 6000 alquileres, lo que podría indicar patrones estacionales o de días laborales/festivos.
  • La temperatura y la humedad muestran distribuciones más cercanas a la normal, lo cual es esperable para variables climáticas.

6. Análisis de Distribución para Variables Categóricas

Para las variables categóricas, las cuentas de valores nos permiten entender la frecuencia de cada categoría.


print("\n### Conteo de valores por estación:")
print(df.group_by("season").len().sort("season"))

print("\n### Conteo de valores por situación climática:")
print(df.group_by("weather_situation").len().sort("weather_situation"))

print("\n### Conteo de valores por día de la semana:")
print(df.group_by("weekday").len().sort("weekday"))
                


📊 Salida:

### Conteo de valores por estación:


Interpretación:

  • Observamos una distribución relativamente uniforme de días a lo largo de las estaciones, lo que indica una buena cobertura.
  • La mayoría de los días tienen una situación climática favorable (1: despejado/pocos nubes).
  • Los días de la semana tienen un conteo similar, lo cual es de esperar, excepto quizás por los fines de semana si el dataset no cubre un número exacto de semanas.

6.1. Visualización de Alquileres por Categoría

Los gráficos de barras son excelentes para comparar el número de alquileres entre diferentes categorías.


plt.figure(figsize=(15, 6))

# Gráfico de barras de alquileres por estación
plt.subplot(1, 2, 1)
avg_rentals_by_season = df.group_by("season").agg(pl.col("total_rentals").mean().alias("avg_rentals")).sort("season")
sns.barplot(x=avg_rentals_by_season["season"], y=avg_rentals_by_season["avg_rentals"], palette="viridis")
plt.title('Promedio de Alquileres por Estación')
plt.xlabel('Estación (1:Invierno, 2:Primavera, 3:Verano, 4:Otoño)')
plt.ylabel('Promedio de Alquileres')

# Gráfico de barras de alquileres por situación climática
plt.subplot(1, 2, 2)
avg_rentals_by_weather = df.group_by("weather_situation").agg(pl.col("total_rentals").mean().alias("avg_rentals")).sort("weather_situation")
sns.barplot(x=avg_rentals_by_weather["weather_situation"], y=avg_rentals_by_weather["avg_rentals"], palette="mako")
plt.title('Promedio de Alquileres por Situación Climática')
plt.xlabel('Situación Climática (1:Claro, 2:Nublado/Niebla, 3:Lluvia Ligera/Nieve, 4:Lluvia Fuerte/Nieve)')
plt.ylabel('Promedio de Alquileres')

plt.tight_layout()
plt.show() # Placeholder for Plot 2
                
[Aquí se mostraría el Plot 2: Promedio de alquileres por estación y por situación climática]


Interpretación del Plot 2:

  • Como era de esperar, los alquileres son significativamente más altos en primavera y verano, disminuyendo en otoño e invierno.
  • Claramente, una mejor situación climática (menor valor numérico) se correlaciona con un mayor número de alquileres.

7. Detección y Manejo de Valores Atípicos

Los valores atípicos (outliers) pueden distorsionar nuestros análisis. Usaremos box plots para visualizarlos en variables numéricas clave.


plt.figure(figsize=(15, 5))

# Box plot de 'total_rentals'
plt.subplot(1, 3, 1)
sns.boxplot(y=df["total_rentals"].to_pandas())
plt.title('Box Plot de Alquileres Totales')
plt.ylabel('Número de Alquileres')

# Box plot de 'windspeed'
plt.subplot(1, 3, 2)
sns.boxplot(y=df["windspeed"].to_pandas())
plt.title('Box Plot de Velocidad del Viento')
plt.ylabel('Velocidad del Viento Normalizada')

# Box plot de 'humidity'
plt.subplot(1, 3, 3)
sns.boxplot(y=df["humidity"].to_pandas())
plt.title('Box Plot de Humedad')
plt.ylabel('Humedad Normalizada')

plt.tight_layout()
plt.show() # Placeholder for Plot 3
                
[Aquí se mostraría el Plot 3: Box plots de total_rentals, windspeed y humidity]


Interpretación del Plot 3:

  • En total_rentals, el box plot muestra que no hay valores atípicos extremos por debajo del cuartil inferior, y un rango extendido hacia el cuartil superior, lo que reconfirma la forma de la distribución que vimos en el histograma.
  • En windspeed, hay algunos valores atípicos por encima del rango normal, indicando días con vientos excepcionalmente fuertes.
  • Para humidity, observamos algunos valores atípicos en el extremo inferior, lo que podría representar días de humedad inusualmente baja.

Para este EDA, no los removeremos, pero en un proyecto real, se podría optar por la imputación o la eliminación, dependiendo del contexto.

8. Análisis de Correlación

La correlación nos indica la fuerza y dirección de la relación lineal entre dos variables numéricas. Polars lo hace de manera eficiente.


# Seleccionar solo columnas numéricas para el cálculo de la matriz de correlación
numerical_df = df.select(pl.exclude(["dteday", "season", "year", "month", "holiday", "weekday", "workingday", "weather_situation", "instant"]))

# Calcular la matriz de correlación
# Polars no tiene un método directo de .corr() para DataFrame,
# lo haremos calculando la correlación de cada par de columnas.
# Para una matriz completa, a menudo es más sencillo convertir a Pandas
# para la visualización con Seaborn si el dataset no es masivo.

# Convertir a Pandas para la matriz de correlación y visualización
correlation_matrix = numerical_df.to_pandas().corr()

plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', fmt=".2f")
plt.title('Matriz de Correlación entre Variables Numéricas')
plt.show() # Placeholder for Plot 4
                
[Aquí se mostraría el Plot 4: Heatmap de correlación]


Interpretación del Plot 4:

  • Se observa una fuerte correlación positiva entre total_rentals, casual (usuarios ocasionales) y registered (usuarios registrados), lo cual es obvio ya que total_rentals es la suma de las otras dos.
  • temperature y feels_like_temperature están, como es de esperar, altamente correlacionadas.
  • Hay una correlación positiva moderada entre la temperatura (temperature, feels_like_temperature) y el número total de alquileres, lo que sugiere que a temperaturas más agradables, más personas alquilan bicicletas.
  • La humidity y windspeed tienen una correlación negativa ligera con los alquileres, lo que es razonable, ya que condiciones más húmedas o ventosas pueden disuadir a los ciclistas.

9. Ingeniería de Características Simple

La ingeniería de características es el proceso de crear nuevas características a partir de las existentes para mejorar el rendimiento de los modelos o facilitar el análisis. Con Polars, esto es sencillo y eficiente.


df = df.with_columns([
    # Extraer el día de la semana (0=Lunes, 6=Domingo)
    pl.col("dteday").dt.weekday().alias("day_of_week"),
    # Crear una característica para saber si es fin de semana
    (pl.col("workingday") == 0).alias("is_weekend")
])

print("\n### Primeras filas con nuevas características:")
print(df.head())

# Visualización de alquileres por día de la semana y si es fin de semana
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
avg_rentals_by_day_of_week = df.group_by("day_of_week").agg(pl.col("total_rentals").mean().alias("avg_rentals")).sort("day_of_week")
sns.barplot(x=avg_rentals_by_day_of_week["day_of_week"], y=avg_rentals_by_day_of_week["avg_rentals"], palette="pastel")
plt.title('Promedio de Alquileres por Día de la Semana')
plt.xlabel('Día de la Semana (1:Lunes, 7:Domingo)')
plt.ylabel('Promedio de Alquileres')

plt.subplot(1, 2, 2)
avg_rentals_by_is_weekend = df.group_by("is_weekend").agg(pl.col("total_rentals").mean().alias("avg_rentals")).sort("is_weekend")
sns.barplot(x=avg_rentals_by_is_weekend["is_weekend"], y=avg_rentals_by_is_weekend["avg_rentals"], palette="muted")
plt.title('Promedio de Alquileres: Días de Semana vs. Fin de Semana')
plt.xlabel('Es Fin de Semana (False:Día de Semana, True:Fin de Semana)')
plt.ylabel('Promedio de Alquileres')

plt.tight_layout()
plt.show() # Placeholder for Plot 5
                
[Aquí se mostraría el Plot 5: Promedio de alquileres por día de la semana y por fin de semana]


Interpretación del Plot 5:

  • Vemos que el número de alquileres es generalmente más bajo los sábados y domingos (días 6 y 7, respectivamente), lo que contradice ligeramente la expectativa de un aumento en el uso recreativo. Esto podría indicar que una gran parte de los usuarios son viajeros diarios.
  • El gráfico de días de semana vs. fin de semana confirma que los días de semana (False) tienen un promedio de alquileres considerablemente más alto que los fines de semana (True).

10. Consideraciones Avanzadas y Mejores Prácticas con Polars

Polars no es solo rápido; también ofrece características que lo hacen ideal para trabajar con grandes volúmenes de datos. Aquí algunas que debes conocer:

10.1. Evaluación Diferida (Lazy Evaluation)

Polars se destaca por su capacidad de operar en modo “lazy” (diferido). Esto significa que las operaciones no se ejecutan inmediatamente, sino que Polars construye un “plan de consulta” optimizado. La ejecución real ocurre solo cuando se llama a .collect().


# Ejemplo de Lazy Evaluation
lazy_df = pl.scan_csv(DATA_URL) # scan_csv es el punto de entrada para lazy

# Construyendo un plan de consulta
filtered_and_aggregated_lazy = lazy_df.filter(
    pl.col("temp") > 0.5 # Temperatura > 0.5 (normalizada)
).group_by("season").agg(
    pl.col("cnt").mean().alias("avg_rentals_warm_days")
).sort("season")

# La ejecución ocurre solo aquí
print("\n### Agregación de alquileres en días cálidos (Lazy Evaluation):")
print(filtered_and_aggregated_lazy.collect())
                


Beneficios:

  • Optimización automática: Polars puede reordenar y fusionar operaciones para minimizar el consumo de memoria y maximizar el rendimiento.
  • Procesamiento de datos fuera de memoria: Permite trabajar con datasets que no caben en la RAM.

10.2. Uso Eficiente de Expresiones

Las expresiones son el corazón de Polars para la manipulación de datos. Permiten realizar operaciones complejas de manera declarativa y vectorizada, lo que las hace muy eficientes.


# Ejemplo de uso eficiente de expresiones para crear múltiples columnas
df_expr = df.with_columns([
    (pl.col("casual") + pl.col("registered")).alias("calculated_total_rentals"),
    (pl.col("total_rentals") / pl.col("total_rentals").sum()).alias("proportion_of_total_rentals"),
    pl.col("total_rentals").log().alias("log_total_rentals")
])

print("\n### DataFrame con nuevas columnas usando expresiones:")
print(df_expr.select(["total_rentals", "calculated_total_rentals", "proportion_of_total_rentals", "log_total_rentals"]).head())
                


Consejo: Siempre que sea posible, utiliza las expresiones de Polars en lugar de bucles de Python o funciones que operen fila por fila. Esto aprovechará al máximo la paralelización y las optimizaciones del motor de Polars.

Conclusión y Próximos Pasos

Hemos recorrido un camino completo de Análisis Exploratorio de Datos utilizando Polars. Desde la carga inicial y la limpieza, hasta el cálculo de estadísticas descriptivas, el análisis de distribuciones, la detección de valores atípicos y la ingeniería de características, Polars ha demostrado ser una herramienta poderosa y eficiente. Sus capacidades de evaluación diferida y su sintaxis expresiva lo convierten en una excelente opción para cualquier proyecto de datos.

Te animo a que tomes este código, lo ejecutes, experimentes con diferentes datasets y sigas profundizando en Polars. Hay mucho más que descubrir, ¡y la mejor manera de aprender es haciendo!

El código completo y el Jupyter Notebook limpio y bien organizado de este proyecto estarán disponibles en mi repositorio de GitHub. ¡No dudes en visitarlo y dejar tus estrellas!

Recuerda que siempre siempre vas a aprender un bit a la vez!



 


🤖 Automatiza tu trading en 5 días con Python

Únete a mi Mini-Curso gratuito por email. Aprende a extraer datos reales, crear indicadores cuantitativos y hacer backtesting profesional.