2025, Oct 06 23:16

Проценты в кросс-таблице Polars: распаковка генераторов и нативный подход

Как превратить кросс-таблицу Polars в проценты без ошибок TypeError: распаковка генераторов в select и нативное решение через sum_horizontal и with_columns. Примеры кода.

Преобразовать кросс-таблицу в доли (проценты) в Polars вроде бы легко, пока в дело не вмешивается мелочь: сочетание генератора выражений с другими аргументами в select. Ниже — короткое объяснение проблемы, причины ошибки и два аккуратных решения: распаковать генератор или использовать нативное выражение Polars, которое вычисляет общий итог «на лету».

Постановка задачи

Цель — получить кросс-таблицу, где каждое значение выражено в процентах от общего числа строк. Начинаем с сводной таблицы, которая считает значения, а затем преобразуем счётчики в проценты.

import polars as pl
import polars.selectors as cs

# исходные данные
frame_src = pl.DataFrame({"a": [2, 0, 1, 0, 0, 0], "b": [1, 1, 1, 0, 0, 1]})

# кросс-таблица с подсчётами
xtab = (
    frame_src
    .pivot(on="b", index="a", values="b", aggregate_function="len", sort_columns=True)
    .fill_null(0)
    .sort("a")
)

# генератор выражений для процентов
def pct_exprs(tbl):
    total_all = tbl.select(~cs.by_index(0)).to_numpy().sum()
    for col_name in tbl.columns[1:]:
        yield (pl.col(col_name) / total_all) * 100

# попытка выбрать первый столбец плюс сгенерированные процентные столбцы
xtab.select(cs.by_index(0), pct_exprs(xtab))

Это приводит к ошибке:

TypeError: cannot create expression literal for value of type generator.

Важно: по отдельности выбор только первого столбца или только сгенерированных выражений работает. Сбой возникает, когда их передают как разные аргументы одновременно.

Почему это происходит

Корень проблемы в том, что при нескольких аргументах select переданный напрямую генератор воспринимается как один аргумент типа generator, а не как последовательность выражений. API ожидает отдельные выражения или явную распаковку последовательности в такие выражения. Без распаковки генератор трактуется как литерал, и Polars закономерно это отклоняет.

Решение 1: Явно распаковать генератор

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

xtab.select(cs.by_index(0), *pct_exprs(xtab))
shape: (3, 3)
┌─────┬───────────┬───────────┐
│ a   ┆ 0         ┆ 1         │
│ --- ┆ ---       ┆ ---       │
│ i64 ┆ f64       ┆ f64       │
╞═════╪═══════════╪═══════════╡
│ 0   ┆ 33.333333 ┆ 33.333333 │
│ 1   ┆ 0.0       ┆ 16.666667 │
│ 2   ┆ 0.0       ┆ 16.666667 │
└─────┴───────────┴───────────┘

Решение 2: Нативное выражение Polars для общего итога

Это можно сделать нативно в Polars с помощью построения выражений. Посчитайте общий итог по неключевым столбцам через sum и pl.sum_horizontal, затем разделите «широкие» столбцы на этот итог. Так всё остаётся выражениями, пригодными для lazy-вычислений.

# итог по каждому столбцу по строкам
xtab.select(cs.exclude(cs.first()).sum())
shape: (1, 2)
┌─────┬─────┐
│ 0   ┆ 1   │
│ --- ┆ --- │
│ u32 ┆ u32 │
╞═════╪═════╡
│ 2   ┆ 4   │
└─────┴─────┘
# общий итог по всем неключевым столбцам
xtab.select(pl.sum_horizontal(cs.exclude(cs.first()).sum()))
shape: (1, 1)
┌─────┐
│ 0   │
│ --- │
│ u32 │
╞═════╡
│ 6   │
└─────┘
# рассчитываем проценты «на лету»
xtab.with_columns(
    cs.exclude(cs.first()) / pl.sum_horizontal(cs.exclude(cs.first()).sum()) * 100
)
shape: (3, 3)
┌─────┬───────────┬───────────┐
│ a   ┆ 0         ┆ 1         │
│ --- ┆ ---       ┆ ---       │
│ i64 ┆ f64       ┆ f64       │
╞═════╪═══════════╪═══════════╡
│ 0   ┆ 33.333333 ┆ 33.333333 │
│ 1   ┆ 0.0       ┆ 16.666667 │
│ 2   ┆ 0.0       ┆ 16.666667 │
└─────┴───────────┴───────────┘

Если хотите оформить решение для повторного использования, селекторы и выражения можно сохранить в переменных и применить через pipe.

def to_percentages(wide_tbl):
    value_cols = cs.exclude(cs.first())
    grand = pl.sum_horizontal(value_cols.sum())
    return wide_tbl.with_columns(value_cols / grand * 100)

xtab.pipe(to_percentages)

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

Кросс-таблицы и сводные таблицы — частая вещь в аналитике. Понимание, когда нужно распаковать генератор, избавляет от запутывающих ошибок типов при композиции select. А вычисления на нативных выражениях Polars упрощают конвейер: можно считать итоги и доли, не выходя из API выражений.

Выводы

Для быстрого исправления распаковывайте результаты генератора, когда смешиваете их с другими аргументами в select. Для более идиоматичного подхода соберите общий итог через sum и pl.sum_horizontal и посчитайте проценты с помощью with_columns. Оба приёма делают код компактным и предсказуемым и хорошо сочетаются с селекторами и выражениями Polars.

Статья основана на вопросе на StackOverflow от robertspierre и ответе пользователя jqurious.