2025, Sep 25 07:16

Как в Polars получить скалярное среднее и центрированный столбец в одном графе выражений

Показываем, как в Polars одновременно получить скалярное среднее и центрированный столбец без eager, оставаясь в мире выражений. Трюк с implode и ScalarColumn.

Центрирование столбца в Polars с одновременным получением скалярного среднего выглядит просто, но на практике возникает вопрос: как остаться в мире выражений, не уходить в eager-подход и сохранить эффективную организацию памяти, когда один результат — скаляр, а другой — столбец?

Воспроизводимый пример: скаляр и столбец из одного графа выражений

Настройка минимальна. Мы считаем среднее по одному столбцу и строим его центрированную версию, используя только выражения.

import polars as pl
import numpy as np

frame = pl.DataFrame({"probe": np.array([0., 1, 2, 3, 4])})

avg_expr = pl.col("probe").mean().alias("avg")
res_avg = frame.select(avg_expr)

shifted_expr = pl.col("probe") - avg_expr
res_shifted = frame.select(shifted_expr)

Логично попробовать выбрать оба результата разом. На экране скалярное среднее будет показано так, будто оно «распространено» на все строки — выглядит как broadcasting и может сбивать с толку с точки зрения предполагаемого способа хранения.

Что происходит на самом деле

В Polars есть концепция ScalarColumn — столбца, который может хранить скаляры. То, что в выводе вы видите «распространение», не значит автоматически, что выполняется копирование для каждой строки. Однако это не жесткая гарантия, поэтому есть случаи, когда копия всё-таки создаётся.

Если хотите, чтобы визуальное представление отражало фактическую организацию памяти — один скаляр и один не-скаляр без построчного дублирования, — можно изменить подачу не-скалярного результата.

Решение: сделать не-скаляр явным с помощью implode

Чтобы сохранить оба результата рядом и при этом подчеркнуть идею «один скаляр + одна коллекция», примените implode к не-скалярному выражению. В итоге получится DataFrame с одной строкой: скаляр и список — так отображение будет согласовано с задуманной структурой.

out = frame.select(
    pl.col("probe").mean().alias("avg"),
    (pl.col("probe") - pl.col("probe").mean()).implode().alias("centered")
)

В итоговой схеме будет один скаляр f64 и list[f64] с центрированными значениями — оба результата получены выражениями, без eager-шага.

Почему это важно

При работе с выражениями в Polars часто приходится сочетать скалярные агрегации и постолбцовые преобразования в одном select. Понимание того, как представлены скаляры и как отображение соотносится с организацией памяти, помогает избежать неверных предположений о накладных расходах хранения. Использование implode приводит вывод к задуманному виду: один скаляр рядом с компактным «векторным» полем, рассчитанные за один проход.

Выводы

Если вам нужны и скаляр, и производный столбец из одного плана выражений, помните: скаляр может выглядеть как «распространённый» в выводе, даже если внутренне он не всегда копируется. Если хотите отразить «один скаляр рядом с одной коллекцией», примените implode к не-скалярному выражению и оставайтесь в конвейере выражений. Если такой шаблон не вписывается в ваш рабочий процесс, можно посчитать скаляр eagerly и продолжить далее — это тоже вариант, хотя он хуже масштабируется на общие случаи.

Статья основана на вопросе с StackOverflow от Felix Benning и ответе от Dean MacGregor.