Study OS

Resource

Unnest Polars

Polars · Listo

Fragmentos indexados

13

Kind

markdown

Attached

no

Leer recurso

Contenido renderizado para estudiar directamente desde el material fuente.

1) Array simple y acceso a un elemento

BigQuery

SELECT
  emp_id,
  name,
  pay_by_quarter[ORDINAL(3)] AS q3_pay
FROM employees;

Polars

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])

2) UNNEST de una lista

BigQuery

SELECT
  emp_id,
  name,
  quarter_pay
FROM employees, UNNEST(pay_by_quarter) AS quarter_pay;

Polars

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])

3) ARRAY<STRUCT> / List[Struct]

BigQuery

Una fila con book como ARRAY<STRUCT<title, chapter ARRAY<STRUCT<title>>>>.

Polars

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])

Una fila por libro

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")
)

Una fila por capítulo

chapters = (
    library
    .explode("book")
    .unnest("book")
    .rename({"title": "book_title"})
    .explode("chapter")
    .unnest("chapter")
    .rename({"title": "chapter_title"})
    .select("author", "book_title", "chapter_title")
)

Una fila por libro, manteniendo capítulos como lista

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])

4) ARRAY_AGG(STRUCT(...))

BigQuery

SELECT
  ein,
  ARRAY_AGG(STRUCT(elf, tax_pd, subseccd) ORDER BY tax_pd DESC) AS filing
FROM filings_raw
GROUP BY ein;

Polars

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])

5) NOT EXISTS sobre un array/lista de structs

BigQuery

SELECT ein
FROM filings
WHERE NOT EXISTS (
  SELECT 1
  FROM UNNEST(filing) AS f
  WHERE f.elf = 'E'
);

Polars

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.
  • luego niegas con ~. ([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])

6) GENERATE_DATE_ARRAY

Hay dos sabores en Polars:

a) una secuencia única

BigQuery

SELECT GENERATE_DATE_ARRAY('2026-01-01', '2026-01-21', INTERVAL 10 DAY);

Polars

from datetime import date

days = pl.date_range(
    date(2026, 1, 1),
    date(2026, 1, 21),
    interval="10d",
    eager=True,
)

b) una lista de fechas por fila

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])

7) convertir fechas generadas en filas

BigQuery

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;

Polars

exploded_days = days_df.explode("summer").rename({"summer": "day"})

De nuevo, el equivalente de UNNEST es explode. ([Polars User Guide][4])

8) WITH OFFSET / conservar posición

BigQuery 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])

9) ejemplo tipo “workers + modulo”

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])

10) ARRAY_CONCAT

BigQuery

SELECT ARRAY_CONCAT(['A', 'B'], ['C', 'D']) AS letters;

Polars

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])

11) ARRAY_TO_STRING

BigQuery

SELECT ARRAY_TO_STRING(['A', 'B', NULL, 'D'], '*', 'na') AS arr;

Polars

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.

12) TO_JSON_STRING

BigQuery

SELECT TO_JSON_STRING([STRUCT(1 AS a, 'bbb' AS b), STRUCT(2 AS a, 'ccc' AS b)]);

Polars

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])

Cheat sheet corto

# 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:

  • BigQuery ARRAY → Polars List
  • BigQuery STRUCT → Polars Struct
  • BigQuery UNNEST(arr) → Polars explode("arr") o pl.col("arr").list.explode()
  • BigQuery arr[OFFSET(i)] / arr[ORDINAL(i)] → Polars list.get(i) con índice 0-based
  • BigQuery ARRAY_LENGTH(arr) → Polars list.len()
  • BigQuery x IN UNNEST(arr) → Polars list.contains(x)
  • BigQuery ARRAY_AGG(...) → Polars group_by(...).agg(...), que agrega valores del grupo como listas
  • BigQuery GENERATE_DATE_ARRAY(...) → Polars date_ranges(...) para listas por fila, o date_range(..., eager=True) para una serie única
  • BigQuery ARRAY_TO_STRING(...) → Polars list.join(...) para listas de strings
  • BigQuery TO_JSON_STRING(struct) → Polars struct.json_encode() para Struct documentado oficialmente. ([Polars User Guide][2])