Вышел первый release candidate библиотеки Polars 2.0. Финальный релиз 2.0 появится в течение следующих недель. Цель обновления — не набор новых громких фич, а скорее «незаметный» переход для пользователей. Мажорная версия понадобилась, чтобы отказаться от прежних архитектурных решений, которые сейчас мешают развитию проекта, и поменять настройки по умолчанию на более разумные для большинства сценариев. Главное изменение — все запросы LazyFrame теперь по умолчанию выполняются на потоковом (streaming) движке. Обычные пользователи Polars получат заметный прирост по памяти и производительности: в среднем потоковый движок ожидается быстрее в 5 раз.
Для перехода на 2.0 подготовлено полное руководство по миграции. Здесь разберём основные моменты.
Потоковый движок по умолчанию
Это самое значимое изменение в 2.0. Вызов collect на LazyFrame теперь по умолчанию использует потоковый движок, что даёт существенный прирост по памяти и скорости для большинства запросов. Именно это изменение потребовало мажорного бампа версии: потоковый движок по умолчанию не гарантирует сохранение порядка строк для некоторых операций (join, group_by, unpivot и других). Если нужен предсказуемый порядок строк в этих операциях, его можно явно включить через maintain_order=True.
Тем, кто хочет оставить «in-memory» движок по умолчанию, достаточно настроить affinity движка.
lf = pl.LazyFrame({"k": [2, 1, 0], "v": ["a", "b", "c"]})
other = pl.LazyFrame({"k": [0, 1, 2], "r": ["x", "y", "z"]})
# 2.0: engine="auto" now resolves to the streaming engine.
# Row order is no longer guaranteed for joins, group_by, unpivot, ...
(
lf
.join(other, on="k", how="left")
.collect()
)
# ┌─────┬─────┬─────┐
# │ k ┆ v ┆ r │ <- order may not match `lf`'s original row order
# └─────┴─────┴─────┘
# Opt in to observable order for this query:
(
lf
.join(other, on="k", how="left", maintain_order="left")
.collect()
)
# Or keep the old in-memory engine as the default, process-wide:
pl.Config.set_engine_affinity("in-memory")
# ...or per query:
(
lf
.join(other, on="k", how="left")
.collect(engine="in-memory")
)
Более строгий Polars
Polars стремится быть строгим и «падать» как можно раньше. В идеале ошибки должны всплывать сразу, а не через 20 минут выполнения пайплайна. Неявное поведение при несовпадении данных должно включаться явно, а не быть настройкой по умолчанию, поскольку такие несовпадения могут маскировать баги. Эта строгость становится ещё важнее с ростом популярности разработки с участием AI-агентов: агент может заранее проверить структуру запроса вызовом collect_schema(), который разрешает типы и обнаруживает несовпадения на уровне схемы без материализации данных. Это даёт агентам быструю обратную связь и позволяет им итерироваться быстрее. Не все ошибки можно поймать на этапе компиляции плана запроса — некоторые зависят от самих данных. В таких случаях Polars по умолчанию выбирает более строгое поведение, чтобы несогласованности были явно обнаружены, а не приводили к тихому получению других результатов.
Несколько примеров того, где Polars стал строже:
Потеря точности при приведении типов в is_in
Раньше при выполнении выражения is_in на данных разных типов Polars приводил оба типа к общему супертипу, даже если такое преобразование было с потерей точности. Пример с идентификаторами пользователей показывает, как это может привести к скрытой ошибке из-за несовпадения типов данных.
# Checking if a user ID matches a list of "flagged" account IDs
# (flagged_ids loaded from a JSON export, where large IDs became floats)
flagged_ids = pl.Series([9007199254740992.0])
user_id = pl.Series([9007199254740993]) # Int64 -> a different ID, off by 1
user_id.is_in(flagged_ids)
До версии 2.0 user_id приводился к Float64, чтобы соответствовать типу flagged_ids. Но число 9007199254740993 превышает 2^53 (9007199254740992) — максимальное целое число, которое float64 может представить точно, — поэтому оно тихо округлялось вниз до 9007199254740992.0, давая ложное совпадение.
В версии 2.0 в такой ситуации будет выброшена ошибка: InvalidOperationError: 'is_in' cannot check for Int64 values in List(Float64) data. — пользователям нужно явно приводить типы, если преобразование связано с потерей точности.
Строгая конкатенация
Горизонтальная конкатенация теперь проверяет длины таблиц вместо того, чтобы тихо заполнять недостающие значения null.
# Joining per-day transaction counts with per-day fraud-flag counts,
transactions = pl.DataFrame({"day": [1, 2, 3, 4, 5], "count": [120, 98, 143, 87, 156]})
# Upstream job for day 5 failed silently
fraud_flags = pl.DataFrame({"flagged": [2, 0, 5, 1]}) # only 4 rows
pl.concat([transactions, fraud_flags], how="horizontal")
shape: (5, 2)
┌─────┬───────┬─────────┐
│ day ┆ count ┆ flagged │
│ 1 ┆ 120 ┆ 2 │
│ 2 ┆ 98 ┆ 0 │
│ 3 ┆ 143 ┆ 5 │
│ 4 ┆ 87 ┆ 1 │
│ 5 ┆ 156 ┆ null │ <- day 5 silently has no flag count
└─────┴───────┴─────────┘
В версии 2.0 этот код выбросит ошибку:
ShapeError: cannot concat dataframes with different heights in 'strict' mode
Если требуется именно заполнение недостающих значений, это нужно явно указать через how="horizontal_extend" — чтобы намерение было понятно тому, кто читает код.
Удаление неоднозначных приведений типов в пользу отдельных методов и конструкторов
Ещё одно заметное изменение — удаление многих операций приведения типов (cast), которые были неоднозначными или должны применяться через отдельное выражение парсинга. Цель — оставить один очевидный способ разбора данных.
Enum/Categorical и целые числа
pl.Series([None, 1, 0, 2], dtype=pl.UInt32).cast(pl.Enum(["a", "b", "c"]))
# ComputeError: casting from u32 to enum is not supported.
Вместо этого используйте .cat.to(dtype) для перехода из int в categorical и .cat.physical() для перехода из categorical в int.
Парсинг строк во временные типы данных
pl.Series(["2022-08-30"]).cast(pl.Date)
# InvalidOperationError: casting from string to date is not supported.
Вместо этого используйте .str.to_date() / .str.to_datetime(). Эти методы позволяют указать формат парсинга, давая больше контроля над тем, как разбираются данные.
Это лишь несколько примеров — на деле изменений, повышающих строгость, гораздо больше. Полный список доступен в руководстве по миграции.
Информативные сообщения об ошибках
Много усилий было вложено в то, чтобы пользователь или его AI-агент могли легко разобраться, если использовались старые параметры, которые больше не поддерживаются. Для этого добавлены два новых типизированных исключения: polars.exceptions.AttributeRemovedError и polars.exceptions.ArgumentRemovedError — они обрабатывают, соответственно, удалённые атрибуты/методы и удалённые параметры.
Сообщения об ошибках должны сразу указывать на новый способ вызова API. Ниже два примера.
>>> lf.melt(id_vars="a", value_vars="b")
polars.exceptions.AttributeRemovedError: `melt` was removed in version 2.0;
use `LazyFrame.unpivot` instead, with `index` instead of `id_vars`
and `on` instead of `value_vars`
>>> df.join(df, on="a", join_nulls=True)
polars.exceptions.ArgumentRemovedError: the argument 'join_nulls' for
'DataFrame.join' was deprecated in version 1.24 and has been removed
in 2.0.0. It was renamed to 'nulls_equal' in version 2.0.
Большая часть удалённого функционала была помечена как deprecated уже давно, и если пайплайны поддерживались в актуальном состоянии, это не должно было на них повлиять. Разработчики Polars просят сообщить им, если какая-то нужная функциональность всё же была удалена без должной альтернативы.
Итоги
Polars 2.0 — это прежде всего более разумные значения по умолчанию (главное из них — потоковый движок) и улучшенный API. Ожидается, что релиз пройдёт незаметно для большинства пользователей. Новые функции в проекте не привязываются к мажорным версиям — они выходят сразу, как только готовы.
При этом ветка Polars 2.x обещает быть значительно лучше, чем 1.x. В разработке много вещей, о которых пока мало говорилось публично: полноценная поддержка out-of-core обработки для потокового движка, новый дизайн IO-плагинов, что должно стать одним из самых быстрых читателей S3, значительное расширение покрытия SQL, планировщик на основе стоимости выполнения (cost-based planner), переупорядочивание join-ов и отказ от mmap, что сделает пайплайны полностью асинхронными от начала до конца.
Попробовать release candidate можно, установив его командой pip install polars==2.0rc1. Обратную связь можно оставить на GitHub: https://github.com/pola-rs/polars/issues или в Discord: https://discord.gg/4UfP5cfBE7.