Есть ли способ поместить несколько шагов в функцию выражения поляров?

Я учился писать функции-выражения в полярах, которые прекрасно подходят для создания самодокументируемых цепных операций. Но я борюсь с более сложными функциями. Допустим, я хочу заменить значение в столбце bar первым значением в столбце baz, когда baz пусто, в столбце группы foo. Чтобы сформулировать более четко: у меня есть набор столбцов, которые образуют отсортированную группу (только в моем примере foo). У меня есть еще один столбец bar, который может иметь или не иметь пустые значения. Если первое значение в bar для группы пусто ('' или NULL), возьмите соответствующее значение из другого столбца baz и примените к каждому bar в группе. Если первое значение в bar не пусто, ничего не делайте с группой.

Нижеприведенное работает правильно.

Начальный фрейм данных:

import polars as pl

df = pl.DataFrame({'foo': [1, 1, 1, 2, 2, 2, 3, 3],
                   'bar': ['a', 'a', 'a', None, None, None, 'c', 'c'],
                   'baz': ['x', None, 'q', 'z', 'r', None, 'y', 's']})
shape: (8, 3)
┌─────┬──────┬──────┐
│ foo ┆ bar  ┆ baz  │
│ --- ┆ ---  ┆ ---  │
│ i64 ┆ str  ┆ str  │
╞═════╪══════╪══════╡
│ 1   ┆ a    ┆ x    │
│ 1   ┆ a    ┆ null │
│ 1   ┆ a    ┆ q    │
│ 2   ┆ null ┆ z    │
│ 2   ┆ null ┆ r    │
│ 2   ┆ null ┆ null │
│ 3   ┆ c    ┆ y    │
│ 3   ┆ c    ┆ s    │
└─────┴──────┴──────┘

Преобразование, которое я хочу выполнить:

df = (df.with_columns(pl.col('baz').first().over(['foo']).alias('temp'))
        .with_columns(pl.when((pl.col('bar') == '') | (pl.col('bar').is_null()))
                      .then(pl.col('temp'))
                      .otherwise(pl.col('bar')).alias('bar2'))
        .with_columns(pl.col('bar2').alias('bar'))
        .drop(['temp', 'bar2'])
      )

Ожидаемый результат:

┌─────┬──────┬──────┐
│ foo ┆ bar  ┆ baz  │
│ --- ┆ ---  ┆ ---  │
│ i64 ┆ str  ┆ str  │
╞═════╪══════╪══════╡
│ 1   ┆ a    ┆ x    │
│ 1   ┆ a    ┆ null │
│ 1   ┆ a    ┆ q    │
│ 2   ┆ z    ┆ z    │
│ 2   ┆ z    ┆ r    │
│ 2   ┆ z    ┆ null │
│ 3   ┆ c    ┆ y    │
│ 3   ┆ c    ┆ s    │
└─────┴──────┴──────┘

В моей реальной задаче эта цепочка была бы лишь подмножеством более крупной цепочки, поэтому было бы здорово, если бы я мог написать

def update_bar() -> pl.expr:
   return (#some voodoo)

а потом:

df = (df.with_columns(update_bar())
        .drop(['temp', 'bar2'])
     )

или даже

df = (df.with_columns(update_bar())
        .with_columns(pl.col('bar2').alias('bar'))
        .drop(['temp', 'bar2'])
     )

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

Или, может быть, у кого-то есть более умный способ выполнить то, для чего мне нужен весь этот код? Обратите внимание, что наличие совпадающей группировки foo и bar верно только в этом упрощенном примере. В моем реальном случае foo — это 3 столбца, и полосу нельзя использовать отдельно как группу.

Я думаю, что мой ответ может быть неверным. использование примера с пустой строкой дает разные результаты: 'bar': ['a', 'a', 'a', 'x', '', None, 'c', 'c'] — Пожалуйста, проверьте это, если можете, и примите другой ответ, если это действительно так.

jqurious 14.04.2024 13:30
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
1
153
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Кажется, задачу можно решить одним выражением?

df.with_columns(
   pl.when((pl.col("bar").first() == "") | pl.col("bar").first().is_null())
     .then(pl.col("baz").first())
     .otherwise(pl.col("bar"))
     .over("foo")
     .alias("new_bar")
)
shape: (8, 4)
┌─────┬──────┬──────┬─────────┐
│ foo ┆ bar  ┆ baz  ┆ new_bar │
│ --- ┆ ---  ┆ ---  ┆ ---     │
│ i64 ┆ str  ┆ str  ┆ str     │
╞═════╪══════╪══════╪═════════╡
│ 1   ┆ a    ┆ x    ┆ a       │
│ 1   ┆ a    ┆ null ┆ a       │
│ 1   ┆ a    ┆ q    ┆ a       │
│ 2   ┆ null ┆ z    ┆ z       │
│ 2   ┆ null ┆ r    ┆ z       │
│ 2   ┆ null ┆ null ┆ z       │
│ 3   ┆ c    ┆ y    ┆ c       │
│ 3   ┆ c    ┆ s    ┆ c       │
└─────┴──────┴──────┴─────────┘

Как выражение внутри функции:

def update_bar() -> pl.Expr:
    bar_is_empty = pl.any_horizontal( # "easier" way to build | chains
        pl.col("bar").first() == "",
        pl.col("bar").first().is_null()
    )
   
    return (
        pl.when(bar_is_empty)
          .then(pl.col("baz").first())
          .otherwise(pl.col("bar"))
          .over("foo")
          .alias("bar")
    )
>>> df.with_columns(update_bar())
shape: (8, 3)
┌─────┬─────┬──────┐
│ foo ┆ bar ┆ baz  │
│ --- ┆ --- ┆ ---  │
│ i64 ┆ str ┆ str  │
╞═════╪═════╪══════╡
│ 1   ┆ a   ┆ x    │
│ 1   ┆ a   ┆ null │
│ 1   ┆ a   ┆ q    │
│ 2   ┆ z   ┆ z    │
│ 2   ┆ z   ┆ r    │
│ 2   ┆ z   ┆ null │
│ 3   ┆ c   ┆ y    │
│ 3   ┆ c   ┆ s    │
└─────┴─────┴──────┘

Существуют также методы Frame/Expression Pipe, которые могут иметь значение:

Это фантастика, и это работает и с моими более сложными данными. Еще мне нравится трюк Any.horizontal — я могу использовать его повсюду. Я все еще пытаюсь понять, как может работать фильтр по столбцу внутри предложения then. Раньше я делал что-то подобное случайно и до сих пор не знаю, как это возможно. Поляры - это потрясающе.

MikeP 10.04.2024 21:06

@MikeP Это похоже на то, что вы сделали с temp_col — Polars запускает выражения .when() и .then() параллельно и «маскирует» ложные значения — без необходимости явного создания временного столбца.

jqurious 10.04.2024 21:54

@MikeP Думаю, я неправильно истолковал, что на самом деле делает ваш код. например при использовании 'bar': ['a', 'a', 'a', 'x', '', None, 'c', 'c'] результаты различаются. (Я думал, что вам нужен первый baz, когда bar имеет значение null или пусто) - Итак, я думаю, что этот ответ на самом деле неверен - /user/Ramnath продемонстрировал гораздо более простой подход.

jqurious 14.04.2024 13:23

Результаты должны отличаться от предложенных вами входных данных bar. Это первая строка в группе, которая должна определять, что произойдет. В вашем примере первая строка в группе «2» больше не является нулевой или пустой, это «x», поэтому ожидается другой результат. Я не уточнил, что должно произойти, если в bar есть пустые значения внутри группы, где первое значение было непустым (ваш пример). В таком случае я бы предпочёл bar чтобы меня оставили в покое для всей группы. Вместо этого ваше решение будет принимать значение baz из первого соответствующего пустого значения в bar.

MikeP 14.04.2024 17:39

Позвольте мне еще раз сформулировать это требование как можно более четко. У меня есть набор столбцов, которые образуют отсортированную группу (только в моем примере foo). У меня есть еще один столбец bar, который может иметь или не иметь пустые значения. Если первое значение в bar для группы пусто ('' или NULL), возьмите соответствующее значение из другого столбца baz и примените к каждому bar в группе. Если первое значение в bar не пусто, ничего не делайте с группой. Надеюсь, это прояснит любую путаницу. Большое спасибо за помощь!

MikeP 14.04.2024 17:51

Спасибо за обновления @MikeP — теперь это должно точно соответствовать описанной вами логике.

jqurious 15.04.2024 11:01

Альтернативным подходом к решению этой проблемы может быть использование pl.coalesce(). ИМО также прекрасно читается:

Объедините первое значение baz поверх foo со значением bar

df.with_columns(
  new_bar = pl.coalesce(
      pl.col("bar"), 
      pl.col('baz').first().over("foo")
  )
)

ОБНОВЛЕНИЕ: Спасибо @jqurious за указание на то, чего хотел ОП.

Это немного сложнее, поскольку они также хотят сохранить исходные ненулевые значения от bar

jqurious 12.04.2024 14:07

Если я заменю имя столбца bar на new_bar, я получу тот же результат, что и ваше решение.

Ramnath 14.04.2024 03:04

Я предполагаю, что именно так был отформатирован пример кода OP — он уже включал преобразование. Вам нужно будет начать только с исходного кадра данных — и в этом случае одного объединения недостаточно. Я отредактировал вопрос, чтобы, надеюсь, прояснить это.

jqurious 14.04.2024 03:43

Поменяв порядок в coalesce, я могу сопоставить результат, который вы получите.

Ramnath 14.04.2024 04:04

Думаю, я слишком усложнил реальную задачу. Я думал, что пользователю нужен первый baz, когда bar имеет значение null или пусто, а не только первый baz в каждой группе — моя вина. Я думаю, использование pl.col("bar").replace('', None) в вашем ответе также будет обрабатывать случай пустой строки?

jqurious 14.04.2024 13:26

Не беспокойся! Спасибо, что нашли время поучаствовать в этом. Эти обсуждения очень полезны для понимания того, как писать идиоматический полярный код!

Ramnath 14.04.2024 19:25

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