Как разбить несколько столбцов List[_] с пропущенными значениями в полях Python?

Учитывая фрейм данных Polars, как показано ниже, как я могу вызвать explode() в обоих столбцах, одновременно расширяя нулевую запись до правильной длины, чтобы она совпадала с ее строкой?

shape: (3, 2)
┌───────────┬─────────────────────┐
│ x         ┆ y                   │
│ ---       ┆ ---                 │
│ list[i64] ┆ list[bool]          │
╞═══════════╪═════════════════════╡
│ [1]       ┆ [true]              │
│ [1, 2]    ┆ null                │
│ [1, 2, 3] ┆ [true, false, true] │
└───────────┴─────────────────────┘

В настоящее время вызов df.explode(["x", "y"]) приведет к этой ошибке.

polars.exceptions.ShapeError: exploded columns must have matching element counts

Я предполагаю, что встроенного способа нет. Но я не могу найти/придумать способ преобразовать этот нуль в список правильной длины, чтобы разнесение работало. Здесь требуемая длина заранее не известна статически.

Я рассмотрел возможность передачи выражений list.len() в repeat_by(), но repeat_by() не поддерживает значение null.

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
5
0
66
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вы были на правильном пути, пытаясь заполнить недостающие значения списком нулевых значений правильной длины.

Чтобы pl.Expr.repeat_by работал с нулевым значением, нам нужно убедиться, что базовое выражение имеет ненулевой тип. Этого можно добиться, установив аргумент dtype явности pl.lit.

Затем столбец списка (списков) нулей можно использовать для заполнения нулевых значений в y. После этого одновременное внесение x и y работает как обычно.

(
    df
    .with_columns(
        pl.col("y").fill_null(
            pl.lit(None, dtype=pl.Boolean).repeat_by(pl.col("x").list.len())
        )
    )
)
shape: (3, 2)
┌───────────┬─────────────────────┐
│ x         ┆ y                   │
│ ---       ┆ ---                 │
│ list[i64] ┆ list[bool]          │
╞═══════════╪═════════════════════╡
│ [1]       ┆ [true]              │
│ [1, 2]    ┆ [null, null]        │
│ [1, 2, 3] ┆ [true, false, true] │
└───────────┴─────────────────────┘

Отсюда df.explode("x", "y") должно работать как положено.

Примечание. Если имеется более двух столбцов, каждый из которых может содержать нулевые значения, можно объединить приведенный выше ответ с этим ответом, чтобы получить правильное решение.

Примечание.

есть ли причина использовать pl.Int64, а не pl.Boolean как dtype?

Roman Pekar 30.07.2024 10:32

@RomanPekar Спасибо, случайно просмотрел колонку x — исправлено!

Hericks 30.07.2024 10:34

ты забыл поставить explode('x','y') в конце.

Dean MacGregor 30.07.2024 21:37

@DeanMacGregor Первоначально я намеренно опубликовал ответ как есть, поскольку ОП попросил способ преобразовать нуль в список правильной длины, чтобы .explode("x", "y") работал. Однако в конце я добавлю короткую заметку. Спасибо!

Hericks 30.07.2024 21:56

См. производительность внизу:

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

pl.concat(
    [
        part.lazy().select("i", "x", pl.lit(None, pl.Boolean).alias("y")).explode("x")
        if isnull[0]
        else part.lazy().explode("x", "y")
        for isnull, part in df.with_row_index("i").group_by(
            pl.col("y").is_null(), maintain_order=True
        )
    ]
).sort("i").drop("i").collect()

У этого есть добавленный with_row_index, поэтому вы можете сохранить исходный порядок, но если порядок не важен, вы можете удалить его, а также последующую сортировку/отбросить. Это также делает part ленивыми и в конце собирает их. Это связано с тем, что если вы объедините несколько ленивых фреймов, каждый из их планов будет выполняться параллельно. Опять же, если это не важно, вы можете удалить 2 .lazy() и .collect().

Если вы начинаете с ленивого фрейма, вы не можете напрямую использовать group_by в качестве итератора, но можете использовать map_groups, чтобы получить тот же эффект.

Вам нужно создать такую ​​функцию, как:

def part_explode(part: pl.DataFrame):
    if part.select(pl.col('x').first().list.len()==pl.col('y').first().list.len()).item():
        return part.explode('x','y')
    else:
        return part.with_columns(pl.lit(None, pl.Boolean).alias('y')).explode('x')

и тогда ты делаешь

df.group_by(pl.col("y").is_null(), maintain_order=True).map_groups(
    part_explode, schema = {"i": pl.UInt32, "x": pl.Int64, "y": pl.Boolean}
).sort('i').drop('i').collect()

Я не думаю, что map_groups будет распараллеливать части, поскольку он основан на выполнении функции Python, поэтому не используйте этот подход, если вы не начинаете с лени и у вас нет памяти для первой реализации.

Производительность

Настройка с помощью

import polars as pl
import numpy as np
n=1_000_000
df=pl.DataFrame({
    'x':np.random.randint(0,10,n),
    'y':np.random.randint(0,2,n),
    'group':np.random.randint(0, 100_000, n),
}).with_columns(pl.col('y').cast(pl.Boolean)).group_by('group').agg('x','y').with_columns(
    y=pl.when(pl.col('group').mod(20)==0).then(pl.lit(None)).otherwise('y')
).drop('group')

а потом тесты

%%timeit
(
    df
    .with_columns(
        pl.col("y").fill_null(
            pl.lit(None, dtype=pl.Boolean).repeat_by(pl.col("x").list.len())
        )
    ).explode('x','y')
)
31.7 ms ± 5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

против конката сверху

84.1 ms ± 6.59 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Другие вопросы по теме