Visión General
Dask se apoya en pandas y NumPy para trabajar con datasets más grandes que la memoria. Corta un DataFrame en particiones, y cada partición es un pandas DataFrame normal. Dask construye un task graph de operaciones y las ejecuta de forma lazy, paralelizando entre cores con el scheduler local o entre máquinas con el scheduler distribuido. Como la API refleja la de pandas, la mayoría del código existente funciona con cambios mínimos.
Cuándo Usar
Dask rinde cuando los datasets van de unos pocos gigabytes a aproximadamente un terabyte, demasiado grandes para la memoria pero manejables en disco. Si ya tenés código de pandas que necesita escalar sin una reescritura grande, la API familiar de Dask es una victoria rápida. También sirve cuando querés paralelismo sin el overhead de un cluster de Spark, o cuando tus pipelines ETL mueven archivos Parquet, CSV o HDF5. Por último, usá Dask cuando necesitás lógica paralela custom que va más allá de group-by y join.
Cuándo Evitar
Evitá Dask cuando los datasets sean menores a 1 GB. pandas suele ser más rápido porque evita el overhead del task graph de Dask. También evitalo si necesitás el ecosistema completo de pandas, porque Dask no implementa todos los métodos de pandas. Para streams o procesamiento en tiempo real, Flink o Structured Streaming son un mejor ajuste. Y si Polars es suficiente, usalo: suele ser más rápido y ligero para la mayoría del trabajo con DataFrames.
Solución
Dask DataFrame Básico
import dask.dataframe as dd
# Leer CSV lazy — no carga hasta compute()
ddf = dd.read_csv("data/orders_*.csv")
# Leer Parquet
ddf = dd.read_parquet("data/orders/")
# Desde un pandas DataFrame
import pandas as pd
pdf = pd.read_csv("data.csv")
ddf = dd.from_pandas(pdf, npartitions=4)
# Inspeccionar particiones
print(ddf.npartitions) # número de particiones
print(ddf.divisions) # límites de particiones, conocidos si están ordenados
Operaciones Lazy
# Construir task graph — sin ejecución todavía
result = (
ddf
.query("amount > 100")
.groupby("customer_id")
.agg({"amount": "sum"})
.reset_index()
.sort_values("amount", ascending=False)
)
# Ejecutar el graph y obtener un pandas DataFrame
df = result.compute()
print(df.head(10))
Lectura y Escritura
# Leer múltiples archivos CSV
ddf = dd.read_csv("data/2025-*.csv", parse_dates=["order_date"])
# Leer con dtypes explícitos
ddf = dd.read_csv(
"data/orders.csv",
dtype={
"order_id": "int64",
"amount": "float64",
"customer_id": "object",
},
)
# Escribir Parquet (un archivo por partición por defecto)
ddf.to_parquet("data/output/", write_index=False)
# Escribir CSV con un archivo por partición
ddf.to_csv("data/output_*.csv", index=False)
Group-by y Agregación
# Agregación group-by en paralelo
result = (
ddf
.groupby("customer_id")
.agg({
"amount": ["sum", "mean", "count"],
"order_id": "nunique",
})
.compute()
)
# Agregaciones con nombres custom
result = (
ddf
.groupby("category")
.agg(
total_revenue=("amount", "sum"),
avg_order=("amount", "mean"),
order_count=("order_id", "count"),
)
.compute()
)
Joins
orders = dd.read_parquet("data/orders/")
customers = dd.read_parquet("data/customers/")
# Merge estándar; puede requerir un shuffle si no está ordenado por la join key
joined = orders.merge(customers, on="customer_id", how="left")
# Broadcast join cuando el lado derecho es chico
small_customers = customers.head(1000) # pandas DataFrame
joined = orders.merge(
dd.from_pandas(small_customers, npartitions=1),
on="customer_id",
how="left",
broadcast=True,
)
result = joined.compute()
Cómputo Custom con map_partitions
def process_partition(pdf: pd.DataFrame) -> pd.DataFrame:
pdf["amount_with_tax"] = pdf["amount"] * 1.1
pdf["order_date"] = pd.to_datetime(pdf["order_date"])
pdf["month"] = pdf["order_date"].dt.month
return pdf
ddf_processed = ddf.map_partitions(process_partition)
result = ddf_processed.compute()
Task Graphs Custom con delayed
import dask
@dask.delayed
def load_file(path):
return pd.read_csv(path)
@dask.delayed
def clean(df):
df["amount"] = pd.to_numeric(df["amount"], errors="coerce")
return df.dropna(subset=["amount"])
@dask.delayed
def combine(dfs):
return pd.concat(dfs).groupby("customer_id")["amount"].sum()
files = ["data/jan.csv", "data/feb.csv", "data/mar.csv"]
processed = [clean(load_file(f)) for f in files]
result = combine(processed)
df = result.compute()
Repartitioning
# Setear número de particiones
ddf = ddf.repartition(npartitions=10)
# O setear tamaño de partición
ddf = ddf.repartition(partition_size="100MB")
# Setear un índice ordenado para que divisions sean conocidas
ddf = ddf.reset_index(drop=True)
ddf = ddf.set_index("customer_id") # dispara un shuffle
Usar Dask Distributed
from dask.distributed import Client
# Cluster local usando todos los cores
client = Client(n_workers=4, threads_per_worker=2, memory_limit="4GB")
# Todos los .compute() usan el distributed scheduler
ddf = dd.read_parquet("data/orders/")
result = ddf.groupby("customer_id")["amount"].sum().compute()
# Cerrar el client al terminar
client.close()
Persistir Data en Memoria
# Cargar a memoria distribuida entre workers
ddf_persisted = ddf.persist()
# Reusar el objeto persistido para operaciones repetidas más rápidas
result = ddf_persisted.groupby("customer_id")["amount"].sum().compute()
Monitoreo de Progreso
from dask.distributed import progress
result = ddf.groupby("customer_id")["amount"].sum()
future = client.compute(result)
progress(future)
df = future.result()
Variantes
Dask con S3
ddf = dd.read_parquet(
"s3://my-bucket/data/orders/",
storage_options={"key": "aws-key", "secret": "aws-secret"},
)
ddf.to_parquet(
"s3://my-bucket/data/output/",
storage_options={"key": "aws-key", "secret": "aws-secret"},
)
Dask Bag para Data No Estructurada
import dask.bag as db
bag = db.read_text("data/events_*.jsonl").map(json.loads)
result = (
bag
.filter(lambda x: x["event_type"] == "purchase")
.map(lambda x: {"user": x["user_id"], "amount": x["amount"]})
.to_dataframe()
.compute()
)
Dask Array para Cargas de NumPy
import dask.array as da
x = da.random.random((10000, 10000), chunks=(1000, 1000))
mean = x.mean(axis=0)
result = mean.compute()
Mejores Prácticas
Para trabajo local, yo empiezo con npartitions alrededor de dos o cuatro veces el número de cores. Eso da
suficiente paralelismo sin generar demasiado overhead de scheduling. Apuntá a particiones de 50 MB a 200 MB.
Las chicas agregan overhead; las grandes dejan cores ociosos.
Llamá a .compute() solo al final de una cadena para que Dask optimice el task graph completo. Usá
.persist() cuando vayas a tocar un DataFrame más de una vez; así se mantiene en memoria de los workers.
Prefiero Parquet sobre CSV porque conserva tipos, soporta column pruning y se lee más rápido. Para
operaciones que la API de Dask no expone directamente, bajá a map_partitions y escribí código normal de
pandas en cada chunk. Evitá .set_index() en DataFrames grandes porque dispara un shuffle completo. Para
desarrollo local, usá el Dask Distributed scheduler; agrega un dashboard y mejores diagnósticos que el
scheduler síncrono por defecto.
Errores Comunes
Llamar a .compute() muy temprano materializa resultados intermedios y rompe la optimización del graph.
Encadená operaciones y llamá .compute() una sola vez al final. Demasiadas particiones, como 1.000
particiones chicas de 1 MB, generan un overhead de scheduling enorme. Llamá a .repartition() para caer en
el rango de 50–200 MB.
No usar .persist() para datos reusados significa que Dask recomputa el task graph cada vez. Dejá los
objetos que vas a reusar en memoria con .persist(). CSV es más lento que Parquet porque debe parsear tipos
en cada
lectura y no soporta column pruning. Además, Dask lee una muestra para inferir el dtype del CSV, lo cual
puede ser incorrecto. Declará los dtypes para evitar errores de tipo.
Preguntas frecuentes
¿En qué se diferencia Dask de pandas?
Dask parte los datos en particiones y las procesa en paralelo. pandas pone todo en un solo DataFrame. Dask
refleja la API de pandas pero evalúa lazy, así que las operaciones construyen un task graph que se ejecuta en
.compute().
¿En qué se diferencia Dask de Spark?
Dask es nativo de Python y usa pandas DataFrames como particiones. Spark usa su propio formato interno y convierte a y desde pandas. Dask es más ligero y fácil de levantar, pero Spark tiene un ecosistema de big data más amplio.
¿Cuántas particiones debería usar?
Apuntá a 50–200 MB por partición. Un archivo de 10 GB se parte en 50–200 chunks de ese tamaño. Para
ejecución local, yo empiezo con dos a cuatro veces el número de cores, y después verifico ddf.npartitions.
¿Puedo usar Dask en un cluster?
Sí. Creá un dask.distributed.Client("scheduler-address:8786") para conectarte a un scheduler remoto.
Levantá el scheduler con dask-scheduler y los workers con dask-worker.
¿Dask soporta todas las operaciones de pandas?
La mayoría de las operaciones comunes están soportadas, incluyendo groupby, merge, join, filter,
map_partitions y varias operaciones de ventana. Algunos menos comunes no están implementados, así que
consultá la documentación de la API de Dask para la lista más actualizada.
Recursos Relacionados
Operaciones de DataFrame de Alto Rendimiento con Polars
Cómo usar Polars para operaciones rápidas de DataFrame con lazy evaluation, expression API, streaming e interop con pandas para datasets grandes.
RecipeConstruir un Pipeline ETL con pandas y Parquet
Cómo construir un pipeline extract-transform-load usando pandas para procesamiento de datos y Parquet para almacenamiento columnar con coerción de tipos y validación.
RecipeAgregaciones a Gran Escala con PySpark
Cómo realizar agregaciones group-by en datasets grandes con PySpark, cubriendo window functions, UDFs, broadcast joins y tuning de performance.
RecipeValidar schemas de DataFrame con Pandera
Cómo validar schemas de DataFrames de pandas y Polars con Pandera: tipos de columnas, constraints, checks personalizados, hypothesis testing y herencia de schemas.
RecipeProgramar y Monitorear DAGs con Apache Airflow
Definí, programá y monitoreá DAGs de Airflow con operators, sensors, XCom, dependencias de tareas y la TaskFlow API.
RecipeLeer y Escribir Archivos Excel con Python
Cómo leer, escribir y formatear hojas de cálculo Excel usando openpyxl y pandas en Python.