Análisis exploratorio
Stack moderno de Python para EDA y visualización
Sobre análisis exploratorio en Python
El stack de EDA en Python se asienta sobre dos capas: una capa de cálculo y manipulación tabular (numpy, pandas, polars) y una capa de visualización fragmentada (matplotlib como base imperativa, seaborn y plotnine como gramáticas declarativas sobre matplotlib, plotly y altair como alternativas interactivas basadas en JavaScript). A diferencia del ecosistema tidyverse en R, donde todo converge hacia un único data frame y una única gramática gráfica, en Python conviven varias estructuras de datos y varias filosofías de gráfico, y conocer cuándo usar cada una es lo que distingue un análisis maduro de uno improvisado.
Instalación mínima recomendada para un entorno de exploración serio:
# Con uv (rápido y reproducible) o pip
uv pip install numpy pandas polars matplotlib seaborn plotly altair pyarrowTres principios del ecosistema que conviene tener interiorizados:
- NumPy es la base. Tanto
pandascomo buena parte dematplotliboperan sobre arrays NumPy por debajo. Entender vectorización, broadcasting ydtypeahorra cuellos de botella opacos más adelante. - Apache Arrow es el formato franco.
pyarrowypolarsoperan sobre columnas Arrow. Pandas 2.x admite backend Arrow opcional. Esto permite intercambiar datos entre librerías sin copia y leer Parquet eficientemente. - Visualización fragmentada por diseño. No hay un equivalente único a
ggplot2. Para producir un gráfico de publicación, casi siempre acabarás encadenando matplotlib (base) + seaborn o plotnine (gramática) + ajustes manuales. Para dashboards interactivos, plotly o altair son la elección natural.
Esta página cataloga siete librerías que estructuran la mayor parte de los flujos de EDA en Python. El orden refleja jerarquía conceptual: primero la base numérica, después la manipulación tabular (pandas y su alternativa moderna polars), y finalmente las cuatro grandes opciones de visualización ordenadas de menor a mayor abstracción.
NumPy
NumPy es la librería fundacional del cálculo científico en Python: define el ndarray, un array multidimensional homogéneo con operaciones vectorizadas en C, y la maquinaria de broadcasting que permite combinar arrays de formas distintas sin loops explícitos.
Todo el stack científico de Python descansa sobre NumPy: pandas, scipy, scikit-learn, matplotlib, PyTorch (vía interop) y un largo etcétera consumen o producen ndarray. Internalizar su API y su modelo mental, dtype, shape, strides, views vs copies, es prerrequisito para cualquier análisis cuantitativo serio en Python.
Cuándo usarlo
Siempre que necesites computación numérica vectorizada: álgebra lineal, estadísticas elementales, simulaciones, generación de datos sintéticos, manipulación de imágenes o tensores. También como capa de interoperabilidad entre librerías que esperan arrays, incluso si tu objeto principal es un DataFrame, los métodos .values o .to_numpy() aparecen continuamente.
Cuándo NO usarlo
- Datos tabulares heterogéneos. Si tus columnas tienen distintos tipos (string, datetime, numérico), usa
pandasopolars. NumPy fuerza un únicodtypepor array y degrada todo aobjecten caso contrario, perdiendo rendimiento. - Cálculo distribuido o out-of-core. Para datasets que no caben en RAM, mira
dask.arrayopolarscon streaming. NumPy es estrictamente en memoria. - Cálculo en GPU. Sustituye por
cupy(API casi idéntica) ojax.numpysi necesitas aceleración. NumPy clásico es solo CPU.
Conceptos clave
dtype. Tipo elemental del array (float64,int32,bool,object…). Determina precisión y consumo de memoria.dtype=objectes la trampa habitual: indica que NumPy ha caído al path lento de Python puro.shapeystrides. Elshapees la forma lógica (filas, columnas, …). Losstridesson los saltos en bytes que NumPy usa internamente para indexar. Operaciones como.To.reshape()modificanstridessin copiar memoria, útil, pero también fuente de bugs si no entiendes qué es una view.- Broadcasting. Reglas para combinar arrays de formas distintas. La regla práctica: dos dimensiones son compatibles si son iguales o si una es 1.
- Vectorización. Reemplaza loops Python por operaciones sobre arrays completos. Diferencias de rendimiento típicas: 50-200×.
np.random.default_rng(). El generador moderno con semilla explícita. La API legacynp.random.seed()+np.random.rand()sigue funcionando pero es desaconsejada desde NumPy 1.17.
Patrón mínimo
import numpy as np
rng = np.random.default_rng(seed=42)
# Array 2D con broadcasting
x = rng.normal(size=(1000, 4))
means = x.mean(axis=0) # estadística por columna
x_centered = x - means # broadcasting: (1000,4) - (4,)
# Álgebra lineal
cov = (x_centered.T @ x_centered) / (x.shape[0] - 1)
eigvals, eigvecs = np.linalg.eigh(cov)
# Indexación booleana (la herramienta más usada en EDA)
outliers = np.abs(x).max(axis=1) > 3
x_clean = x[~outliers]Trampas habituales
- Views vs copies.
arr[:, 0]devuelve una vista (modificarla altera el original).arr[[0, 2, 4]]devuelve una copia. Si dudas, usa.copy()explícitamente. El warningSettingWithCopyWarningno existe en NumPy puro, los efectos colaterales pasan silenciosos. np.nanpropaga. Cualquier operación conNaNproduceNaN. Usanp.nanmean,np.nansum, etc., opandasque gestiona NaN automáticamente.- Comparaciones de enteros con
NaN.NaNsolo existe en floats. AsignarNaNa un arrayintfalla o promueve silenciosamente. En pandas 2.x con backend Arrow, la nullable integer (Int64) resuelve este problema.
Enlaces
pandas
pandas es la librería estándar de manipulación tabular en Python. Su estructura central, el DataFrame, combina la ergonomía de un data frame de R con la potencia del ndarray de NumPy. Cubre lectura/escritura (CSV, Parquet, Excel, SQL, JSON), limpieza, joins, group-by, pivot, resampling temporal y todo el aparato clásico de EDA.
Maduro, omnipresente y con una API muy amplia. La versión 2.x introdujo backend opcional basado en PyArrow, que mejora rendimiento, soporta enteros nullable y unifica el manejo de strings, vale la pena habilitarlo en proyectos nuevos.
Cuándo usarlo
Análisis tabular interactivo en notebooks Jupyter, pipelines de limpieza, integración con scikit-learn, matplotlib y casi cualquier otra librería del ecosistema. Es el formato franco para datasets que caben cómodamente en RAM (hasta unos pocos GB en máquinas modernas).
Cuándo NO usarlo
- Datasets grandes (>10 GB). Considera
polars(que soporta streaming y lazy execution) oduckdb(SQL embebido sobre Parquet). Pandas degrada drásticamente por encima del tamaño de RAM. - Pipelines de producción con tipos estrictos. La API de pandas tolera mucha ambigüedad de tipos. En producción
polarsofrece más garantías (lazy validation, esquema explícito). - Cálculo distribuido. Mira
dask.dataframe,modin(drop-in replacement) opyspark.
Conceptos clave
Index. CadaDataFrameySeriestiene un índice. Útil para alineación automática en operaciones, pero también fuente recurrente de sorpresas (resets implícitos, multi-índices que se cuelan). En la práctica,reset_index(drop=True)se usa más de lo que parece..loc[]vs.iloc[].locindexa por etiqueta,ilocpor posición. No las mezcles,df[0]es ambiguo y comportamiento varía según el tipo del índice.groupby+ agregaciones. El idioma central de EDA en pandas:df.groupby("col").agg({"x": "mean", "y": ["sum", "std"]}). Devuelve resultados con multi-índices que muchas veces conviene aplanar.merge/join. Equivalentes a JOIN de SQL. Especifica siemprehow=yvalidate=para detectar duplicados inesperados en las claves.- Backend Arrow. Habilítalo con
pd.options.mode.string_storage = "pyarrow"odtype_backend="pyarrow"enread_*. Mejora rendimiento y soluciona limitaciones históricas de tipos.
Patrón mínimo
import pandas as pd
df = pd.read_parquet("ventas.parquet")
# Limpieza básica
df = (
df.dropna(subset=["cliente_id"])
.assign(fecha=lambda d: pd.to_datetime(d["fecha"]))
.query("importe > 0")
)
# Group-by con agregaciones múltiples
resumen = (
df.groupby(["region", pd.Grouper(key="fecha", freq="ME")])
.agg(total=("importe", "sum"),
media=("importe", "mean"),
n=("importe", "size"))
.reset_index()
)
# Pivot largo → ancho
tabla = resumen.pivot_table(index="fecha", columns="region", values="total")Trampas habituales
SettingWithCopyWarning. Modificar un slice sin saber si es vista o copia. La regla: usa.loc[fila, columna] = valorpara asignaciones, nuncadf[df["x"] > 0]["y"] = 5. Pandas 3.0 (vía Copy-on-Write) cambia este comportamiento. Conviene activarpd.options.mode.copy_on_write = Trueya.objectdtype para strings. Sin backend Arrow, las columnas de texto se almacenan comoobject(puntero a objetos Python), con rendimiento pésimo. Activapd.options.future.infer_string = Trueo usa el backend Arrow.- Multi-índice tras
groupby. Devuelve resultados con índices jerárquicos. Si no los necesitas, encadena.reset_index()siempre. .apply()es lento. Casi siempre existe una operación vectorizada equivalente (.map,.replace,np.where,pd.cut…)..applycae al loop de Python y es 10-100× más lento.
Enlaces
Relacionados en esta página
NumPy, base sobre la que se construye pandas.polars, alternativa moderna con mejor rendimiento y API más estricta.
polars
polars es una librería tabular moderna escrita en Rust sobre Apache Arrow. Ofrece una API expresiva basada en expresiones encadenables, un query optimizer tipo SQL y dos modos de ejecución: eager (estilo pandas) y lazy (construye un plan que solo se ejecuta al pedir .collect()).
En la mayoría de benchmarks supera a pandas por un factor de 5-30× y maneja datasets que no caben en RAM mediante streaming. La sintaxis es más estricta, no hay índice, las operaciones se expresan con pl.col(...), pero esa misma rigidez evita la mayoría de bugs sutiles de pandas.
Cuándo usarlo
- Datasets de tamaño medio-grande (cientos de MB a varios GB) donde pandas empieza a sentirse lento.
- Pipelines de producción donde quieres garantías de tipo y un plan de ejecución optimizado.
- ETL sobre Parquet o CSV con filtros, joins y agregaciones complejas.
- Cuando trabajas mucho con expresiones encadenables y la API de pandas te resulta verbosa.
Cuándo NO usarlo
- Cuando ya tienes todo el código en pandas y el dataset cabe holgado en RAM. Migrar por migrar no suele compensar.
- Integración con el resto del ecosistema científico.
scikit-learn,statsmodels,seaborny compañía consumen pandas DataFrames o NumPy arrays. Polars expone.to_pandas()y.to_numpy(), pero la conversión añade fricción. - Manejo extensivo de índices. Polars no tiene
Index. Si tu flujo se apoya en multi-índices y reindexado, pandas es más natural.
Conceptos clave
- Expresiones (
pl.col,pl.when, etc.). El núcleo de la API. No son strings ni lambdas, son objetos que polars compone en un plan de ejecución. - Lazy vs eager.
pl.scan_parquet(...)devuelve unLazyFrame.pl.read_parquet(...)unDataFrame. El primero permite optimización (predicate pushdown, projection pushdown), usa lazy siempre que tu pipeline tenga más de dos pasos. group_by+agg+ expresiones.df.group_by("region").agg(pl.col("importe").sum()). Las expresiones se aplican dentro de cada grupo.with_columnspara añadir/sobreescribir columnas. Equivalente aassignen pandas pero composable con expresiones complejas.- Esquema explícito. Cada
DataFrametiene un.schemaaccesible. Los errores de tipo se detectan temprano.
Patrón mínimo
import polars as pl
# Lazy desde el principio: el plan se optimiza antes de leer
ventas = (
pl.scan_parquet("ventas.parquet")
.filter(pl.col("importe") > 0)
.with_columns(pl.col("fecha").cast(pl.Date))
.group_by(["region", pl.col("fecha").dt.month_start().alias("mes")])
.agg(
total=pl.col("importe").sum(),
media=pl.col("importe").mean(),
n=pl.len(),
)
.sort(["region", "mes"])
.collect() # ejecuta el plan
)
# Conversión a pandas si lo necesita el siguiente paso
ventas_pd = ventas.to_pandas()Trampas habituales
- Confundir
LazyFrameconDataFrame. Métodos como.head()o.describe()funcionan en ambos, pero el lazy no muestra datos hasta.collect(). Si ves<LazyFrame>al imprimir, te falta el collect. - No hay índice. Si vienes de pandas, vas a echar de menos
.loc["clave"]. La operación equivalente es.filter(pl.col("id") == "clave"). Acaba siendo más limpio una vez te acostumbras. group_byno preserva orden por defecto. Pasamaintain_order=Truesi lo necesitas, añade coste, por eso no es el default..to_pandas()con tipos Arrow. Por defecto usause_pyarrow_extension_array=False, lo que copia a NumPy. PasaTruepara preservar tipos Arrow en pandas.
Enlaces
Relacionados en esta página
matplotlib
matplotlib es la librería fundacional de gráficos en Python. Imperativa, de bajo nivel y omnipresente: prácticamente todas las librerías de visualización estática (seaborn, plotnine, pandas .plot) la usan como motor de renderizado. Conocer su modelo de objetos (Figure, Axes, Artist) es prerrequisito para producir gráficos publicables, incluso si normalmente operas con una capa de mayor nivel.
La API tiene dos interfaces que conviven: la interfaz pyplot (estado global, estilo MATLAB) y la interfaz orientada a objetos (explícita, recomendada). En código serio usa siempre la segunda.
Cuándo usarlo
- Cuando necesitas control fino sobre cada elemento del gráfico (ejes secundarios, anotaciones precisas, insets, layouts complejos con
gridspec). - Como motor de seaborn / plotnine. Aunque uses una capa declarativa por encima, casi siempre acabarás llamando a métodos de matplotlib para el ajuste final.
- Exportación de calidad publicación (PDF, SVG, PNG a 300 dpi).
Cuándo NO usarlo
- Para EDA rápida con datos tabulares. La sintaxis es verbosa. Empieza con
seabornodf.plot()y baja a matplotlib solo si necesitas afinar. - Para gráficos interactivos.
plotlyoaltairofrecen mucha mejor experiencia con un esfuerzo similar. - Para gramática declarativa. Mira
plotnine(port de ggplot2) oaltair(basado en Vega-Lite).
Conceptos clave
FigurevsAxes. LaFigurees el contenedor. LosAxesson los paneles donde se dibuja.fig, ax = plt.subplots()es el punto de partida canónico.- Interfaz orientada a objetos.
ax.plot(...),ax.set_xlabel(...),ax.legend(). Olvidaplt.plot,plt.xlabel,plt.legendpara todo lo que no sea un one-liner en consola. plt.subplots(nrows, ncols, ...). Crea grids de paneles. Para layouts irregulares,gridspecosubplot_mosaic.tight_layout()yconstrained_layout. Ajustan automáticamente márgenes. Usalayout="constrained"ensubplots(), es más robusto que llamartight_layout()al final.- Estilos y rcParams.
plt.style.use("seaborn-v0_8-whitegrid")o configuración fina víaplt.rcParams. Definir un estilo propio (archivo.mplstyle) ahorra horas en proyectos serios.
Patrón mínimo
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(42)
x = np.linspace(0, 10, 200)
y = np.sin(x) + rng.normal(0, 0.1, size=x.size)
fig, ax = plt.subplots(figsize=(7, 4), layout="constrained")
ax.scatter(x, y, s=10, alpha=0.6, label="datos")
ax.plot(x, np.sin(x), color="black", lw=1.5, label="modelo")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_title("Ejemplo mínimo en matplotlib OO")
ax.legend(frameon=False)
ax.spines[["top", "right"]].set_visible(False)
fig.savefig("figura.pdf", dpi=300, bbox_inches="tight")Trampas habituales
- Mezclar
plt.yax.. El estado global depyplotgenera bugs sutiles cuando hay variosAxes. Decide la convención al principio del archivo y mantenla. plt.show()en scripts vs notebooks. En notebooks, los gráficos se muestran automáticamente si la última expresión es la figura. En scripts,plt.show()es necesario y bloquea. En contexto headless (CI, servidor), usamatplotlib.use("Agg")antes de importar pyplot.- No cerrar figuras. En loops largos, no llamar
plt.close(fig)causa fugas de memoria. En notebooks, también,%matplotlib inlineretiene referencias.
Enlaces
Relacionados en esta página
seaborn, capa declarativa estadística sobre matplotlib.plotnine, gramática de gráficos al estilo ggplot2.
seaborn
seaborn es una capa declarativa estadística sobre matplotlib. Diseñada por Michael Waskom, ofrece defaults estéticos sensatos y funciones de alto nivel para los gráficos estadísticos más habituales: distribuciones, relaciones bivariadas, categorical plots, regresiones, heatmaps, pairplots. Internamente sigue siendo matplotlib, así que cualquier ajuste fino se hace bajando al Axes subyacente.
La versión 0.12 introdujo la API seaborn.objects (también llamada new interface), inspirada en la gramática de ggplot2. Conviven con la API clásica. La nueva es más composable, pero la clásica sigue siendo la más usada en EDA cotidiana.
Cuándo usarlo
- EDA rápida sobre
DataFramescon varias variables categóricas y numéricas. - Visualización estadística integrada:
regplot,lmplot,kdeplot,boxplot,violinplot,pairplot,heatmap. - Facetas (
relplot,catplot,displot,FacetGrid) para explorar relaciones condicionadas a variables categóricas, el caso de uso donde brilla.
Cuándo NO usarlo
- Cuando necesitas control milimétrico. Vuelve a matplotlib puro. Seaborn oculta detalles que a veces necesitas.
- Para gráficos interactivos. Usa
plotly,altairobokeh. Seaborn produce solo estático. - Para gramática declarativa pura. Mira
plotnineoaltairsi quieres una API más cercana a ggplot2.
Conceptos clave
- Functions vs axes-level vs figure-level. Hay funciones que operan sobre un único
Axes(scatterplot,boxplot) y funciones que crean su propia figura con facetas (relplot,catplot,displot). Las segundas devuelven unFacetGrid, no unAxes. hue,style,size,col,row. Estéticas que mapean variables a propiedades visuales. La filosofía es declarativa: describes qué mapear, no cómo dibujar.- Paletas (
palette=...).seaborntiene paletas integradas ("viridis","deep","colorblind"…). Para datos ordinales o divergentes, usa paletas apropiadas, el default no siempre es adecuado. seaborn.objects. API nueva:so.Plot(data, x=, y=).add(so.Dot()).facet("group"). Más composable, pero todavía con funcionalidad incompleta frente a la clásica.set_theme(). Configura defaults globales (paleta, estilo, escala de fuente). Llamarlo al inicio del notebook es el patrón habitual.
Patrón mínimo
import seaborn as sns
import matplotlib.pyplot as plt
sns.set_theme(style="whitegrid", palette="deep")
penguins = sns.load_dataset("penguins")
# Figure-level con facetas
g = sns.relplot(
data=penguins,
x="bill_length_mm", y="bill_depth_mm",
hue="species", col="island",
kind="scatter", height=3.5, aspect=1,
)
g.set_titles("{col_name}")
g.figure.suptitle("Pico de pingüinos por isla", y=1.05)
# Axes-level: control sobre el objeto matplotlib
fig, ax = plt.subplots(figsize=(6, 4))
sns.violinplot(data=penguins, x="species", y="body_mass_g", ax=ax)
ax.set_ylabel("Masa corporal (g)")Trampas habituales
- Confundir axes-level y figure-level. Pasar
ax=arelplotfalla.relplotcrea su propia figura. Usascatterplotsi quieres dibujar sobre unAxesexistente. huecon variable continua. Seaborn discretiza por defecto en bins, a veces es lo que quieres, a veces no. Pasapalette=explícita o usa unacmapde matplotlib.set_theme()afecta a matplotlib global. Si compartes proceso con otros gráficos, los rcParams quedan modificados. En notebooks tipicamente está bien. En pipelines, encapsula consns.axes_style()oplt.rc_context().
Enlaces
Relacionados en esta página
matplotlib, motor subyacente. Imprescindible para ajustes finos.plotnine, alternativa con gramática ggplot2 más estricta.
plotnine
plotnine es el port más fiel de ggplot2 a Python. Implementa la gramática de gráficos de Wilkinson tal como la formalizó Hadley Wickham: ggplot(data) + aes(...) + geom_*() + facet_*() + scale_*() + theme_*(). Si vienes de R y echas de menos esa composabilidad, es la opción natural.
Internamente usa matplotlib como motor de renderizado, así que comparte limitaciones (estático) y se integra con el resto del stack. Está bien mantenido (versión activa en 2024-2025) y cubre la mayor parte de la API de ggplot2.
Cuándo usarlo
- Cuando ya conoces ggplot2 y la gramática te resulta más natural que la API de seaborn.
- Gráficos compuestos con muchas capas (puntos + suavizados + facetas + escalas personalizadas).
- Reproducir figuras de papers en R sin reescribir la lógica de visualización.
Cuándo NO usarlo
- Si no vienes de ggplot2. La curva de aprendizaje frente a seaborn no compensa si no tienes el modelo mental previo.
- Para gráficos interactivos. No es su dominio. Usa
plotlyoaltair. - En pipelines donde rendimiento importa. plotnine añade overhead frente a matplotlib puro. Para producir cientos de figuras automatizadas es más lento.
Conceptos clave
- Composición con
+. La API es idéntica a ggplot2: cadageom_*,scale_*,facet_*,theme_*se añade con+. Esto permite construir gráficos paso a paso. aes(). Mapeo entre columnas del DataFrame y propiedades estéticas (x, y, color, fill, shape, size, alpha).- Estadísticas (
stat_*). Casi todos losgeom_*tienen unstat_*asociado que computa la transformación (binning, suavizado, conteo). Sobreescribir el stat es habitual. - Facets (
facet_wrap,facet_grid). Equivalente exacto al de ggplot2. - Temas.
theme_bw(),theme_minimal(),theme_classic()están portados. También se puede componer un tema propio.
Patrón mínimo
from plotnine import (
ggplot, aes, geom_point, geom_smooth,
facet_wrap, scale_color_brewer, labs, theme_minimal
)
import pandas as pd
# Asumiendo un DataFrame `penguins` (e.g. de palmerpenguins o seaborn)
import seaborn as sns
penguins = sns.load_dataset("penguins").dropna()
p = (
ggplot(penguins, aes(x="bill_length_mm", y="bill_depth_mm", color="species"))
+ geom_point(alpha=0.6, size=2)
+ geom_smooth(method="lm", se=False)
+ facet_wrap("~island")
+ scale_color_brewer(type="qual", palette="Set1")
+ labs(x="Longitud pico (mm)", y="Profundidad pico (mm)",
title="Morfometría de pingüinos por isla")
+ theme_minimal()
)
p.save("plot.pdf", width=8, height=4, dpi=300)Trampas habituales
- Strings en
aes()vs variables Python. En plotnine los nombres de columnas se pasan como strings (aes(x="bill_length_mm")), no como expresiones libres como en R. Es una diferencia ergonómica menor pero recurrente. - Compatibilidad con pandas. Funciona perfectamente con
DataFramede pandas. Conpolarsnecesita.to_pandas()primero (no soporta el protocolo dataframe interchange en todas las versiones). - Rendimiento con datasets grandes. Por encima de unos cientos de miles de puntos, considera precomputar bins o usar
geom_bin2d/geom_hex. matplotlib puro es más rápido para volcados masivos.
Enlaces
Relacionados en esta página
matplotlib, motor subyacente.seaborn, alternativa declarativa con sintaxis distinta.
plotly
plotly (la biblioteca Python. El motor JavaScript se llama plotly.js) es la opción dominante para visualización interactiva en notebooks y dashboards. Produce gráficos HTML con zoom, hover, toggle de series y exportación a PNG/SVG/PDF. Integra plotly.express como API de alto nivel (estilo seaborn) y plotly.graph_objects como API de bajo nivel.
Bajo el capó es JavaScript renderizado en el navegador. Esto le da interactividad pero limita su uso fuera del entorno web, para figuras de papers estáticas, prefiere matplotlib.
Cuándo usarlo
- Notebooks Jupyter con audiencia interactiva (presentaciones, exploración compartida).
- Dashboards (vía
Dash,Streamlit,Panelo exportación HTML standalone). - Gráficos 3D, mapas, Sankey diagrams, parallel coordinates, casos donde matplotlib es incómodo y plotly brilla.
- Cuando el lector necesita hacer hover para inspeccionar valores individuales.
Cuándo NO usarlo
- Figuras estáticas para publicación. Plotly puede exportar a PNG/PDF (vía
kaleido), pero la calidad tipográfica y el control fino son inferiores a matplotlib. - Datasets enormes (>50-100k puntos sin downsampling). El navegador se atasca. Usa
datashader+plotly(víaholoviews) o WebGL (scatter_gl). - Cuando quieres reproducibilidad estricta de byte. El output HTML cambia con la versión de plotly.js embebida.
Conceptos clave
plotly.expressvsplotly.graph_objects. Express es alto nivel (una línea por gráfico). Graph_objects construyeFigurepaso a paso conTraces. Empieza con express, baja a graph_objects para ajustes que express no cubre.Figure. El objeto resultante.fig.show()lo renderiza en notebook.fig.write_html(...)lo serializa.fig.write_image(...)exporta estático (requierekaleido).- Layouts y
update_layout. Cambiar título, anotaciones, márgenes, plantillas (template="plotly_white"). plotly.io.templates. Plantillas globales tipo theme. Útiles para coherencia de estilo en un dashboard.- Integración con pandas y polars.
px.scatter(df, x="a", y="b", color="c")acepta cualquier dataframe que cumpla el protocolo de intercambio.
Patrón mínimo
import plotly.express as px
import plotly.io as pio
pio.templates.default = "plotly_white"
df = px.data.gapminder().query("year == 2007")
fig = px.scatter(
df,
x="gdpPercap", y="lifeExp",
size="pop", color="continent",
log_x=True, hover_name="country",
size_max=60,
title="Esperanza de vida vs PIB per cápita (2007)",
)
fig.update_layout(xaxis_title="PIB per cápita (log)", yaxis_title="Esperanza de vida")
fig.write_html("gapminder.html", include_plotlyjs="cdn")
# Para PNG: fig.write_image("gapminder.png", scale=2) # necesita kaleidoTrampas habituales
- Exportar a imagen requiere
kaleido. Sin él,write_imagefalla con error opaco. Instalapip install -U kaleido. - HTML standalone es pesado. Por defecto embebe todo plotly.js (~3 MB). Usa
include_plotlyjs="cdn"para referenciar el CDN. px.scattercon muchos puntos. Por encima de 10-20k puntos el render lag se nota. Usarender_mode="webgl"(en express) oScattergl(en graph_objects).- Colores categóricos por orden de aparición. Si quieres un mapeo explícito, pasa
color_discrete_map={"A": "#1f77b4", ...}.
Enlaces
Relacionados en esta página
altair, alternativa interactiva declarativa basada en Vega-Lite.seaborn, para el mismo nicho exploratorio pero estático.
altair
altair es una librería declarativa interactiva basada en Vega-Lite. Producida por Jake VanderPlas (uno de los pesos pesados del ecosistema PyData), su filosofía es: describes la especificación del gráfico (qué codifica cada canal) y Vega-Lite la traduce a JavaScript interactivo. La API es notablemente más limpia que plotly y conceptualmente cercana a una gramática de gráficos.
Es la opción favorita de quien valora composabilidad y consistencia conceptual sobre cobertura. Cubre menos tipos de gráfico que plotly, pero los que cubre los hace de forma muy elegante.
Cuándo usarlo
- Dashboards interactivos en notebooks o Streamlit donde la gramática declarativa simplifica el código.
- Gráficos vinculados (selección en uno filtra otro), Vega-Lite los expresa de forma muy concisa.
- Cuando aprecias una API consistente: la misma sintaxis cubre scatter, bar, line, heatmap, area, etc.
- Publicación en notebooks o web donde Vega-Lite es ciudadano de primera clase (JupyterLab, VS Code, GitHub).
Cuándo NO usarlo
- Datasets grandes (>5-10k filas) sin pre-agregación. Altair, por defecto, embebe los datos en el spec JSON. Para volúmenes mayores necesitas
alt.data_transformers.enable("vegafusion")o pre-agregar. - Gráficos 3D, mapas complejos o tipos exóticos. Plotly cubre más casos.
- Figuras de publicación estáticas. Aunque exporta a PNG/SVG/PDF (vía
vl-convert), el ajuste tipográfico es menos refinado que en matplotlib.
Conceptos clave
Chart(data).mark_*().encode(...). El triple esquema: datos, geometría (mark), codificación (encodemapea columnas a canales x/y/color/size/…).- Canales con tipos explícitos.
alt.X("col:Q")declara el tipo:Qcuantitativo,Oordinal,Nnominal,Ttemporal. Vega-Lite usa esto para elegir escalas razonables automáticamente. - Composición (
|,&,+). Concatenación horizontal, vertical y layered. Equivalente alpatchworkde R, pero parte del core. - Selecciones e interactividad.
alt.selection_point(),alt.selection_interval()permiten enlazar gráficos con muy poco código. - VegaFusion. Para datasets grandes, habilita el backend Rust con
alt.data_transformers.enable("vegafusion"), preprocesa los datos antes de pasarlos al cliente.
Patrón mínimo
import altair as alt
from vega_datasets import data
cars = data.cars()
# Brush para selección
brush = alt.selection_interval()
points = (
alt.Chart(cars)
.mark_point()
.encode(
x=alt.X("Horsepower:Q"),
y=alt.Y("Miles_per_Gallon:Q"),
color=alt.condition(brush, "Origin:N", alt.value("lightgray")),
tooltip=["Name", "Origin", "Year"],
)
.add_params(brush)
.properties(width=400, height=300)
)
bars = (
alt.Chart(cars)
.mark_bar()
.encode(x="count():Q", y="Origin:N", color="Origin:N")
.transform_filter(brush)
)
chart = points | bars
chart.save("cars.html")Trampas habituales
- 5000 filas como límite por defecto. Altair embebe los datos en el spec. Superado el umbral falla con
MaxRowsError. Sube el límite conalt.data_transformers.disable_max_rows()o (mejor) activavegafusion. - Tipos no declarados. Si omites el sufijo
:Q/:N/:O/:T, Altair infiere, a veces mal. Para datos categóricos numéricos (códigos), declara:Nexplícitamente. - Exportación estática. Requiere
vl-convert-python. Sin él, solo HTML. - Render en JupyterLab vs notebooks clásicos. En clásicos puede requerir la extensión
vega. JupyterLab y VS Code lo manejan nativo.