Fragmentos indexados
13
Resource
Polars · Listo
Fragmentos indexados
13
Kind
markdown
Attached
no
SELECT
emp_id,
name,
pay_by_quarter[ORDINAL(3)] AS q3_pay
FROM employees;import polars as pl
employees = pl.DataFrame({
"emp_id": [1, 2],
"name": ["Ana", "Luis"],
"pay_by_quarter": [
[1000, 1100, 1200, 1300],
[2000, 2100, 2200, 2300],
],
})
q3 = employees.select(
"emp_id",
"name",
pl.col("pay_by_quarter").list.get(2).alias("q3_pay"), # 0-based
)En Polars, list.get(0) devuelve el primer elemento y list.get(-1) el último; además, si te sales del rango puede devolver None con null_on_oob=True. Eso se parece más a OFFSET que a ORDINAL, así que para traducir ORDINAL(3) usas list.get(2). ([Polars User Guide][3])
UNNEST de una listaSELECT
emp_id,
name,
quarter_pay
FROM employees, UNNEST(pay_by_quarter) AS quarter_pay;quarterly = (
employees
.explode("pay_by_quarter")
.rename({"pay_by_quarter": "quarter_pay"})
)DataFrame.explode en Polars “explodes the dataframe to long format” y requiere columnas de tipo List o Array. Expr.list.explode() hace lo mismo a nivel expresión: una fila por elemento de la lista. ([Polars User Guide][4])
ARRAY<STRUCT> / List[Struct]Una fila con book como ARRAY<STRUCT<title, chapter ARRAY<STRUCT<title>>>>.
library = pl.DataFrame({
"author": ["Borges"],
"book": [[
{
"title": "Ficciones",
"chapter": [
{"title": "Tlön"},
{"title": "Pierre Menard"},
],
},
{
"title": "El Aleph",
"chapter": [
{"title": "El inmortal"},
{"title": "El Aleph"},
],
},
]]
})En Polars esto queda naturalmente como una columna book de tipo list[struct], y el campo chapter dentro de cada libro puede ser otro list[struct]. Polars documenta Struct como el tipo para agrupar varios campos bajo un solo valor y permite extraer campos con struct.field(...) o expandir el struct con unnest. ([Polars User Guide][5])
books = (
library
.explode("book") # list[struct] -> una fila por libro
.unnest("book") # struct -> columnas title, chapter
.rename({"title": "book_title"})
.select("author", "book_title", "chapter")
)chapters = (
library
.explode("book")
.unnest("book")
.rename({"title": "book_title"})
.explode("chapter")
.unnest("chapter")
.rename({"title": "chapter_title"})
.select("author", "book_title", "chapter_title")
)book_with_chapter_titles = (
library
.explode("book")
.unnest("book")
.rename({"title": "book_title"})
.with_columns(
pl.col("chapter")
.list.eval(pl.element().struct.field("title"))
.alias("chapter_titles")
)
.select("author", "book_title", "chapter_titles")
)Aquí la pieza clave es list.eval(...): Polars lo documenta como “run any polars expression against the lists’ elements”, y dentro puedes referirte a cada elemento y sacar su campo title con struct.field("title"). ([Polars User Guide][6])
ARRAY_AGG(STRUCT(...))SELECT
ein,
ARRAY_AGG(STRUCT(elf, tax_pd, subseccd) ORDER BY tax_pd DESC) AS filing
FROM filings_raw
GROUP BY ein;filings_raw = pl.DataFrame({
"ein": ["100", "100", "200", "300"],
"elf": ["E", "E", "P", "E"],
"tax_pd": [201412, 201312, 201412, 201412],
"subseccd": [8, 8, 12, 8],
})
filings = (
filings_raw
.sort(["ein", "tax_pd"], descending=[False, True])
.group_by("ein", maintain_order=True)
.agg(
pl.struct("elf", "tax_pd", "subseccd").alias("filing")
)
)En Polars, group_by(...).agg(pl.col(...)) agrega valores del grupo en listas, y pl.struct(...) crea una columna Struct. Además, la documentación indica que dentro de cada grupo el orden de filas se preserva; por eso, si quieres replicar ORDER BY tax_pd DESC dentro del ARRAY_AGG, lo correcto es ordenar antes del group_by. maintain_order=True solo asegura el orden de los grupos en la salida. ([Polars User Guide][7])
NOT EXISTS sobre un array/lista de structsSELECT ein
FROM filings
WHERE NOT EXISTS (
SELECT 1
FROM UNNEST(filing) AS f
WHERE f.elf = 'E'
);no_electronic = filings.filter(
~pl.col("filing")
.list.eval(pl.element().struct.field("elf") == "E")
.list.any()
)Esto funciona así:
list.eval(...) evalúa una expresión sobre cada elemento de la lista.pl.element().struct.field("elf") == "E" produce una lista booleana por fila.list.any() devuelve si algún booleano de esa lista es True.~. ([Polars User Guide][6])Si el caso fuera simplemente una list[str], entonces la traducción directa de x IN UNNEST(arr) sería más simple:
df.filter(pl.col("arr").list.contains("E"))porque list.contains está documentado exactamente para comprobar si una sublista contiene un valor. ([Polars User Guide][8])
GENERATE_DATE_ARRAYHay dos sabores en Polars:
SELECT GENERATE_DATE_ARRAY('2026-01-01', '2026-01-21', INTERVAL 10 DAY);from datetime import date
days = pl.date_range(
date(2026, 1, 1),
date(2026, 1, 21),
interval="10d",
eager=True,
)Si quieres algo más parecido a una columna con listas, como en tu ejemplo con summer, el análogo más fiel es date_ranges:
days_df = pl.DataFrame({
"id": [1],
"start": [date(2026, 1, 1)],
"end": [date(2026, 1, 21)],
}).with_columns(
summer=pl.date_ranges("start", "end", interval="10d")
)La documentación de date_ranges dice explícitamente que devuelve una columna List(Date) o List(Datetime). ([Polars User Guide][9])
WITH days AS (
SELECT GENERATE_DATE_ARRAY('2026-01-01', '2026-01-21', INTERVAL 10 DAY) AS d
)
SELECT day
FROM days, UNNEST(d) AS day;exploded_days = days_df.explode("summer").rename({"summer": "day"})De nuevo, el equivalente de UNNEST es explode. ([Polars User Guide][4])
WITH OFFSET / conservar posiciónBigQuery usa WITH OFFSET para sacar la posición de cada elemento tras UNNEST. En Polars no se usa la misma sintaxis; una forma limpia y documentada es generar una lista paralela de índices con int_ranges(...) y luego explotar ambas columnas juntas:
t = pl.DataFrame({
"arr": [["A", "B", "C"]],
}).with_columns(
pos=pl.int_ranges(pl.col("arr").list.len())
)
with_pos = (
t
.explode(["arr", "pos"])
.rename({"arr": "letter"})
.select("letter", "pos")
)int_ranges genera una lista de enteros por fila, y list.len() devuelve la longitud de cada lista. ([Polars User Guide][10])
Tu ejemplo final con GENERATE_ARRAY, ARRAY_LENGTH, ORDINAL, OFFSET y MOD se puede escribir así:
plan = pl.DataFrame({
"days": [[
date(2026, 1, 1),
date(2026, 1, 11),
date(2026, 1, 21),
date(2026, 1, 31),
]],
"workers": [["Lak", "Jordan", "Graham"]],
}).with_columns(
dayno=pl.int_ranges(pl.col("days").list.len())
)
assigned = (
plan
.explode(["days", "dayno"])
.with_columns(
worker=pl.col("workers").list.get(pl.col("dayno") % pl.col("workers").list.len())
)
.select(
pl.col("days").alias("work_day"),
"worker",
)
.sort("work_day")
)La lógica es la misma que en SQL, pero en Polars armas primero la secuencia de índices por fila con int_ranges, luego haces explode, y finalmente usas list.get(...) con un índice calculado. list.get acepta incluso una expresión como índice. ([Polars User Guide][10])
ARRAY_CONCATSELECT ARRAY_CONCAT(['A', 'B'], ['C', 'D']) AS letters;Con columnas, lo más claro es:
df = pl.DataFrame({
"left": [["A", "B"]],
"right": [["C", "D"]],
}).with_columns(
letters=pl.concat_list("left", "right")
)pl.concat_list(...) está documentado como concatenación horizontal de columnas en una sola lista. ([Polars User Guide][11])
ARRAY_TO_STRINGSELECT ARRAY_TO_STRING(['A', 'B', NULL, 'D'], '*', 'na') AS arr;Para listas de strings:
df = pl.DataFrame({
"arr": [["A", "B", None, "D"]],
}).with_columns(
joined=pl.col("arr").list.join("*", ignore_nulls=False)
)list.join está documentado para unir todos los strings de una sublista con un separador; además, exige que el tipo interno sea String. ([Polars User Guide][12])
Aquí hay una diferencia importante: en la documentación oficial de Polars sí está claro ignore_nulls, pero no veo un equivalente 1:1 documentado al tercer argumento de BigQuery (null_text, como 'na') en esa misma API. Para reemplazar null por 'na' antes de unir, yo lo haría explícitamente con una transformación previa sobre la lista.
TO_JSON_STRINGSELECT TO_JSON_STRING([STRUCT(1 AS a, 'bbb' AS b), STRUCT(2 AS a, 'ccc' AS b)]);Lo que sí está documentado de forma directa es JSON encoding para Struct:
df = pl.DataFrame({
"obj": [
{"a": [1, 2], "b": [45]},
{"a": [9, 1, 3], "b": None},
]
}).with_columns(
encoded=pl.col("obj").struct.json_encode()
)struct.json_encode() convierte un Struct en string JSON. ([Polars User Guide][13])
Para List[Struct], no te diría que existe un reemplazo 1:1 igual de directo basándome solo en la documentación oficial que revisé. Ahí prefiero ser estricto: la parte claramente documentada es Struct -> JSON string; para List[Struct] ya entraría en patrones adicionales no tan directos en la docs que abrí. ([Polars User Guide][13])
# BigQuery ARRAY / repeated field -> Polars List
# BigQuery STRUCT -> Polars Struct
# BigQuery UNNEST(arr) -> df.explode("arr")
# BigQuery arr[ORDINAL(3)] -> pl.col("arr").list.get(2)
# BigQuery ARRAY_LENGTH(arr) -> pl.col("arr").list.len()
# BigQuery x IN UNNEST(arr) -> pl.col("arr").list.contains(x)
# BigQuery ARRAY_AGG(col) -> df.group_by(...).agg(pl.col("col"))
# BigQuery ARRAY_AGG(STRUCT(...)) -> df.group_by(...).agg(pl.struct(...))
# BigQuery GENERATE_DATE_ARRAY -> pl.date_ranges(...) / pl.date_range(..., eager=True)
# BigQuery ARRAY_CONCAT -> pl.concat_list(...)
# BigQuery ARRAY_TO_STRING -> pl.col("arr").list.join(sep)
# BigQuery TO_JSON_STRING(struct) -> pl.col("s").struct.json_encode()La diferencia conceptual más importante es esta: en BigQuery UNNEST siempre evoca “expandir arrays”; en Polars tienes dos operaciones distintas según el tipo: explode para List/Array, y unnest para Struct. Esa separación está explícita en la documentación. ([Polars User Guide][2])
La traducción mental más útil es esta:
ARRAY → Polars ListSTRUCT → Polars StructUNNEST(arr) → Polars explode("arr") o pl.col("arr").list.explode()arr[OFFSET(i)] / arr[ORDINAL(i)] → Polars list.get(i) con índice 0-basedARRAY_LENGTH(arr) → Polars list.len()x IN UNNEST(arr) → Polars list.contains(x)ARRAY_AGG(...) → Polars group_by(...).agg(...), que agrega valores del grupo como listasGENERATE_DATE_ARRAY(...) → Polars date_ranges(...) para listas por fila, o date_range(..., eager=True) para una serie únicaARRAY_TO_STRING(...) → Polars list.join(...) para listas de stringsTO_JSON_STRING(struct) → Polars struct.json_encode() para Struct documentado oficialmente. ([Polars User Guide][2])