Módulo 2: Declarative Data Quality Tests With Pandera
Instalando Pandera y construyendo el puente DuckDB → Polars
Descripción
Esta lección instala, de verdad, la herramienta que eligió la lección 3, y construye el puente técnico que el resto de este módulo va a usar sin volver a explicarlo: orders_2026-08-14.csv entra a kiosko.duckdb como una tabla SQL real, y sale de ahí como un DataFrame de Polars, el único formato que Pandera necesita para funcionar en esta guía. Ninguna línea de este puente usa pandas — es una regla dura de todo este ecosistema, no una preferencia de esta lección.
Conexión con el módulo. Esta lección es la bisagra entre la decisión (lección 3) y la construcción (lecciones 5 a 8): todo lo que sigue en este módulo —y buena parte del resto de esta guía— parte de la misma tabla orders_s04 y del mismo patrón con.sql(...).pl() que esta lección construye por primera vez.
El caso que nos acompaña: orders_2026-08-14.csv entra al warehouse de Kiosko
Hasta ahora, en el módulo 1 de esta guía, orders_2026-08-14.csv se leyó siempre con csv.DictReader() —una lista de diccionarios de Python, sin ningún motor SQL de por medio—. Esta lección lo lee de otra forma: como una tabla dentro de kiosko.duckdb, el mismo archivo de warehouse que data-modeling-for-analytics-guide construyó y que dbt-analytics-engineering-guide versionó. Esta guía no vuelve a levantar ese warehouse desde cero —esa no es su tarea—, pero sí usa el mismo motor, DuckDB, para aterrizar el archivo nuevo de S04 como una tabla SQL real: orders_s04.
Una analogía: el adaptador universal de enchufes
Cuando viajas a un país con un estándar eléctrico distinto, tu cargador de teléfono no cambia — sigue esperando el mismo tipo de corriente que siempre esperó. Lo que cambia es el enchufe de la pared. El adaptador universal no genera electricidad ni la transforma — solo tiene, de un lado, la forma exacta que encaja en el tomacorriente del país donde estás, y del otro, la forma exacta que tu cargador reconoce. Sin ese adaptador, dos objetos perfectamente funcionales —el tomacorriente, el cargador— simplemente no pueden conectarse entre sí.
DuckDB y Polars son, en ese sentido, dos objetos perfectamente funcionales que hablan formatos distintos: DuckDB organiza los datos como una relación SQL, Polars los organiza como columnas de Apache Arrow en memoria. .pl(), el método que usa esta lección, es el adaptador — no transforma el contenido de los datos, solo cambia la forma en la que están organizados, para que Pandera (que solo reconoce el "enchufe" de Polars, nunca el de una relación DuckDB directamente) pueda leerlos. Y, como cualquier adaptador real, tiene un componente físico concreto detrás: la librería pyarrow, que hace el trabajo real de traducir el formato interno de una relación DuckDB al formato interno de un DataFrame de Polars.
Ejemplo trabajado: instalar, aterrizar el CSV, cruzar el puente
Paso 1 — instalar las tres piezas
pip install "pandera[polars]" duckdb pyarrow
Tres paquetes, tres roles distintos. pandera[polars] instala Pandera y Polars a la vez —el extra [polars] declara polars>=0.20.0 como dependencia, así que un solo comando basta para las dos—. duckdb es el motor SQL que ya usaron las guías anteriores del ecosistema. pyarrow es el adaptador de la analogía: sin él, .pl() falla con ModuleNotFoundError: No module named 'pyarrow' — la documentación oficial de DuckDB lo dice explícitamente: "the pyarrow library must be installed for the integration to work".
Qué esperar. Confirma las tres versiones exactas que usa esta guía corriendo:
# check_versions.py
import pandera
import polars as pl
import duckdb
print(f"pandera: {pandera.__version__}")
print(f"polars: {pl.__version__}")
print(f"duckdb: {duckdb.__version__}")
pandera: 0.32.1
polars: 1.43.2
duckdb: 1.5.5
duckdb 1.5.5 es la misma versión que ya viste correr en data-modeling-for-analytics-guide (módulo 8). pandera 0.32.1 es la versión vigente que investigó la lección 3, publicada el 29 de junio de 2026. Tu instalación puede traer una versión de polars levemente distinta —Pandera solo exige >=0.20.0—, y eso no rompe nada de lo que sigue.
Paso 2 — aterrizar orders_2026-08-14.csv como tabla de kiosko.duckdb
# bridge_duckdb_to_polars.py
import duckdb
con = duckdb.connect("kiosko.duckdb")
con.execute("""
CREATE OR REPLACE TABLE orders_s04 AS
SELECT * FROM read_csv('orders_2026-08-14.csv', header=True,
columns={
'order_id': 'VARCHAR', 'store_id': 'VARCHAR', 'product_id': 'VARCHAR',
'quantity': 'BIGINT', 'unit_price': 'DOUBLE', 'order_ts': 'TIMESTAMP'
})
""")
row_count = con.sql("SELECT COUNT(*) FROM orders_s04").fetchone()[0]
print(f"Filas cargadas en orders_s04: {row_count}\n")
print(con.sql("DESCRIBE orders_s04"))
Fíjate en dos decisiones deliberadas de este bloque. Primero, CREATE OR REPLACE TABLE, no CREATE TABLE a secas — si corres este script una segunda vez sin OR REPLACE, DuckDB rechaza la operación con CatalogException: Catalog Error: Table with name "orders_s04" already exists!, porque ya existe una tabla con ese nombre en kiosko.duckdb de la corrida anterior; OR REPLACE hace que este script sea seguro de correr las veces que haga falta, sin acumular tablas fantasma. Segundo, el parámetro columns={...} de read_csv() — declara explícitamente el tipo de cada columna, en vez de dejar que DuckDB lo adivine. Esto importa especialmente para unit_price: la fila de ORD-9503 tiene ese campo vacío en el CSV, y con el tipo declarado como DOUBLE, DuckDB convierte ese vacío en NULL de forma limpia y predecible — exactamente el comportamiento que las lecciones 5 y 6 necesitan para poder atrapar esa fila con nullable=False.
Qué esperar. Al correr python3 bridge_duckdb_to_polars.py en la carpeta donde guardaste orders_2026-08-14.csv, la salida es exactamente esta:
Filas cargadas en orders_s04: 12
┌─────────────┬─────────────┬─────────┬─────────┬─────────┬─────────┐
│ column_name │ column_type │ null │ key │ default │ extra │
│ varchar │ varchar │ varchar │ varchar │ varchar │ varchar │
├─────────────┼─────────────┼─────────┼─────────┼─────────┼─────────┤
│ order_id │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ store_id │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ product_id │ VARCHAR │ YES │ NULL │ NULL │ NULL │
│ quantity │ BIGINT │ YES │ NULL │ NULL │ NULL │
│ unit_price │ DOUBLE │ YES │ NULL │ NULL │ NULL │
│ order_ts │ TIMESTAMP │ YES │ NULL │ NULL │ NULL │
└─────────────┴─────────────┴─────────┴─────────┴─────────┴─────────┘
Doce filas, las mismas doce del módulo 1, ahora viviendo como una tabla SQL de verdad, con tipos declarados columna por columna — DESCRIBE orders_s04 es el equivalente en DuckDB de preguntarle a una tabla "¿de qué estás hecha?", el mismo tipo de pregunta que ya hiciste con validate_gold_schema() en data-modeling-for-analytics-guide.
Paso 3 — cruzar el puente: .pl()
# bridge_duckdb_to_polars.py -- continuacion
df = con.sql("SELECT * FROM orders_s04").pl()
print(f"\ntype(df): {type(df)}")
print(f"df.shape: {df.shape}")
print("\ndf.schema:")
print(df.schema)
Qué esperar.
type(df): <class 'polars.dataframe.frame.DataFrame'>
df.shape: (12, 6)
df.schema:
Schema({'order_id': String, 'store_id': String, 'product_id': String, 'quantity': Int64, 'unit_price': Float64, 'order_ts': Datetime(time_unit='us', time_zone=None)})
con.sql("SELECT * FROM orders_s04") construye una relación DuckDB —una consulta, no todavía un resultado materializado—. .pl(), encadenado al final, es el adaptador completo: ejecuta la consulta y entrega el resultado como un polars.DataFrame, con los tipos SQL de DuckDB (VARCHAR, BIGINT, DOUBLE, TIMESTAMP) ya traducidos a sus equivalentes de Polars (String, Int64, Float64, Datetime). Nada de este paso tocó pandas en ningún momento — el puente es directo, DuckDB a Polars, exactamente la regla dura que sostiene toda esta guía.
Diagrama: el camino completo, de un CSV a un esquema de Pandera
flowchart LR
A["orders_2026-08-14.csv\n(12 lineas)"] -->|"read_csv() con\ncolumns= explicito"| B["kiosko.duckdb\ntabla orders_s04"]
B -->|"con.sql('SELECT * FROM\norders_s04').pl()\n(necesita pyarrow)"| C["polars.DataFrame\n12 filas x 6 columnas"]
C -->|"leccion 5 en adelante"| D["OrdersSchema.validate(df)\n(Pandera)"]
El diagrama tiene tres flechas, y cada una es una traducción de formato distinta: la primera (read_csv) va de texto plano a una tabla SQL tipada; la segunda (.pl()) va de una relación SQL a un DataFrame en memoria; la tercera (que construyen las lecciones 5 a 8) va de un DataFrame sin reglas a un DataFrame ya comparado contra un esquema declarado. Ninguna de las tres flechas cambia el contenido de los datos — las doce filas siguen siendo las mismas doce filas en cada paso—, solo cambia la forma en la que ese contenido vive, para que la herramienta siguiente pueda leerlo.
Profundización: por qué DuckDB sigue siendo la fuente, aunque Pandera trabaje sobre Polars
Vale la pena que quede claro un punto que se presta a confusión: Pandera no reemplaza a DuckDB como fuente de datos, y Polars no reemplaza el rol de DuckDB como motor SQL. kiosko.duckdb sigue siendo, en esta guía y en las anteriores del ecosistema, el lugar donde viven las tablas —orders_s04 hoy, dim_product/dim_store a partir del módulo 3—. Polars es exclusivamente el formato intermedio que necesita Pandera para poder leer el resultado de una consulta SQL; ni siquiera necesitas usar la API de Polars más allá de .pl() en esta guía —esa API a fondo (filtros encadenados, group_by, expresiones lazy) es terreno de python-for-data-engineering-guide, no de esta—. La secuencia siempre es la misma: SQL en DuckDB decide qué datos entran a la validación (un filtro, un join, una tabla completa), y Polars es solo el conducto por el que esos datos llegan hasta Pandera.
Esto también explica por qué pandas está prohibido en esta guía, y no es solo una regla arbitraria: si el puente fuera DuckDB → pandas → Pandera, agregarías una dependencia extra (pandas) al problema, cuando Pandera ya sabe hablar directamente el formato de Polars sin ningún paso intermedio. Menos pasos significa menos superficie de error, y una regla más simple de recordar: en esta guía, el único formato de DataFrame es Polars.
Errores comunes
Olvidar pyarrow y confundir el error con un problema de Pandera. Qué pasa: alguien instala pandera[polars] y duckdb, pero no pyarrow, y al llegar a .pl() recibe ModuleNotFoundError: No module named 'pyarrow'. Por qué pasa: pandera[polars] instala Polars, pero no instala pyarrow — es una dependencia del puente DuckDB↔Polars, no de Pandera en sí. Cómo detectarlo: si tu error menciona pyarrow y ocurre en la línea de .pl(), no en ninguna línea que use pa.DataFrameModel o pa.Field, el problema es del puente, no de tu esquema. Cómo corregirlo: pip install pyarrow (o, de una sola vez desde el principio, el comando completo de esta lección: pip install "pandera[polars]" duckdb pyarrow).
Correr CREATE TABLE sin OR REPLACE una segunda vez. Qué pasa: alguien corre bridge_duckdb_to_polars.py una vez, funciona perfecto, y al correrlo de nuevo (por ejemplo, después de editar otra parte del script) recibe CatalogException: Catalog Error: Table with name "orders_s04" already exists!. Por qué pasa: kiosko.duckdb es un archivo persistente en disco — a diferencia de una conexión en memoria (duckdb.connect(), sin argumento), lo que creaste en una corrida sigue ahí en la siguiente. Cómo detectarlo: el mensaje de error nombra exactamente la tabla y dice "already exists" — no es un error de sintaxis SQL ni de datos. Cómo corregirlo: usa siempre CREATE OR REPLACE TABLE para cualquier tabla que un script de esta guía pueda necesitar recrear, exactamente como hace el ejemplo de esta lección — es una práctica general útil para cualquier script que se vuelva a correr sobre un archivo DuckDB persistente.
Importar pandera en vez de pandera.polars. Qué pasa: alguien copia un ejemplo de la documentación de Pandera que empieza con import pandera as pa (sin .polars), y más adelante, al declarar un Field con una sintaxis específica de Polars, obtiene un error que no tiene sentido a simple vista. Por qué pasa: Pandera soporta varios motores de DataFrame —pandas, Polars, PySpark— y cada uno tiene su propio submódulo con una API ligeramente distinta; import pandera as pa a secas apunta, por default, a la integración con pandas. Cómo detectarlo: si tu código no importa explícitamente pandera.polars, y estás pasándole un polars.DataFrame a un esquema, revisa la primera línea de tu archivo. Cómo corregirlo: en esta guía, la única forma correcta es import pandera.polars as pa — así lo usó ya el ejemplo de la lección 3, y así lo va a usar cada lección que siga.
Ejercicios
Ejercicio 1 — Provoca el error de pyarrow a propósito, después arréglalo. Si tienes pyarrow instalado, desinstálalo temporalmente (pip uninstall pyarrow -y), corre bridge_duckdb_to_polars.py hasta la línea de .pl(), y confirma el mensaje de error exacto. Después reinstálalo y confirma que el script vuelve a correr completo.
Ver solución
Con pyarrow desinstalado, la línea df = con.sql("SELECT * FROM orders_s04").pl() lanza:
ModuleNotFoundError: No module named 'pyarrow'
Después de pip install pyarrow, la misma línea corre sin ningún cambio de código, y produce exactamente el df.shape: (12, 6) que ya viste en el "Qué esperar" de esta lección. Este ejercicio confirma, de primera mano, que el error pertenece al puente (.pl()), no a nada relacionado con Pandera ni con la sintaxis SQL de la consulta.
Ejercicio 2 — Carga solo las filas de S04 con quantity positivo, usando SQL, antes de llegar a Polars. Modifica la consulta de con.sql(...) para que filtre WHERE quantity > 0 en SQL, antes de convertir a Polars, y cuenta cuántas filas quedan.
Ver solución
df_positive = con.sql("SELECT * FROM orders_s04 WHERE quantity > 0").pl()
print(f"Filas con quantity > 0: {df_positive.height}")
Salida esperada:
Filas con quantity > 0: 11
Once de las doce filas —todas menos ORD-9507, que tiene quantity=-1—. Este ejercicio demuestra un punto importante de la Profundización de esta lección: puedes decidir qué datos llegan a Pandera usando SQL común, antes de que Polars o Pandera entren en juego. No hace falta, y en muchos casos no conviene, filtrar datos con Pandera cuando SQL ya puede hacerlo de forma más eficiente sobre la tabla completa.
Ejercicio 3 — Explica, sin mirar el diagrama, por qué el orden de las tres flechas no se puede invertir. En 2-3 frases, explica por qué la secuencia tiene que ser CSV → DuckDB → Polars → Pandera, y no, por ejemplo, CSV → Polars → DuckDB → Pandera.
Ver solución
DuckDB tiene que ir antes que Polars porque es la fuente de verdad SQL de todo el warehouse de Kiosko —orders_s04 hoy, y en los módulos siguientes dim_product/dim_store, que viven exclusivamente ahí—; si Polars leyera el CSV directamente, sin pasar por DuckDB, este módulo perdería la capacidad de combinar orders_s04 con esas otras tablas usando SQL, algo que el módulo 3 de esta guía necesita para el check de consistencia. Polars tiene que ir antes que Pandera porque, en esta guía, Pandera solo sabe leer el formato Polars (pandera.polars) — nunca lee una relación DuckDB directamente. Invertir el orden rompería la razón de ser de cada pieza: DuckDB como fuente SQL compartida por todo el warehouse, Polars como el único formato intermedio que Pandera entiende.
Resumen y siguiente paso
En esta lección instalaste, de verdad, las tres piezas que sostienen el resto de este módulo —pandera[polars], duckdb, pyarrow— y confirmaste sus versiones exactas. Aterrizaste orders_2026-08-14.csv como una tabla real de kiosko.duckdb (orders_s04, con tipos declarados columna por columna), y cruzaste el puente hasta Polars con con.sql(...).pl(), confirmando con evidencia ejecutada que las doce filas llegan intactas, con los tipos correctos, sin que pandas participara en ningún momento del camino.
Antes de avanzar deberías poder: explicar qué hace exactamente pyarrow en este puente, y por qué el error que provoca su ausencia no tiene nada que ver con Pandera; reproducir la tabla orders_s04 desde cero en un kiosko.duckdb nuevo; y nombrar la diferencia entre pandera (a secas) y pandera.polars.
Tienes el DataFrame listo, en el formato correcto, con los datos reales de S04. La lección 5 escribe, por primera vez en esta guía, un DataFrameModel de Pandera de verdad — y lo corre contra este mismo df.
Recursos
- DuckDB — "Integration with Polars" (la sintaxis exacta de
.pl(), y la confirmación de quepyarrowes requisito para que la integración funcione). duckdb.org/docs/lts/guides/python/polars. En inglés. - DuckDB — Python Client API Reference (métodos de conversión de una relación DuckDB a Polars/Arrow/pandas). duckdb.org/docs/current/clients/python/reference. En inglés.
- Pandera — documentación oficial, integración con Polars (
import pandera.polars as pa, la diferencia con la integración de pandas). pandera.readthedocs.io. En inglés. data-modeling-for-analytics-guide, módulo 8 — fuente dekiosko.duckdby del patrónduckdb.connect(...)+CREATE TABLEque esta lección reutiliza.src/guides/data-modeling-for-analytics-guide/workbook/module-08-project-kioskos-analytics-warehouse/es/. En español.- DISEÑO de esta guía — la decisión de motor completa (DuckDB como fuente SQL, Polars como único puente, pandas prohibido).
src/guides/data-reliability-and-governance-guide/DISENO.md. En español.