Я учился писать функции-выражения в полярах, которые прекрасно подходят для создания самодокументируемых цепных операций. Но я борюсь с более сложными функциями. Допустим, я хочу заменить значение в столбце 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 столбца, и полосу нельзя использовать отдельно как группу.
Кажется, задачу можно решить одним выражением?
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 Это похоже на то, что вы сделали с temp_col
— Polars запускает выражения .when()
и .then()
параллельно и «маскирует» ложные значения — без необходимости явного создания временного столбца.
@MikeP Думаю, я неправильно истолковал, что на самом деле делает ваш код. например при использовании 'bar': ['a', 'a', 'a', 'x', '', None, 'c', 'c']
результаты различаются. (Я думал, что вам нужен первый baz
, когда bar
имеет значение null или пусто) - Итак, я думаю, что этот ответ на самом деле неверен - /user/Ramnath продемонстрировал гораздо более простой подход.
Результаты должны отличаться от предложенных вами входных данных bar
. Это первая строка в группе, которая должна определять, что произойдет. В вашем примере первая строка в группе «2» больше не является нулевой или пустой, это «x», поэтому ожидается другой результат. Я не уточнил, что должно произойти, если в bar
есть пустые значения внутри группы, где первое значение было непустым (ваш пример). В таком случае я бы предпочёл bar
чтобы меня оставили в покое для всей группы. Вместо этого ваше решение будет принимать значение baz
из первого соответствующего пустого значения в bar
.
Позвольте мне еще раз сформулировать это требование как можно более четко. У меня есть набор столбцов, которые образуют отсортированную группу (только в моем примере foo
). У меня есть еще один столбец bar
, который может иметь или не иметь пустые значения. Если первое значение в bar
для группы пусто ('' или NULL), возьмите соответствующее значение из другого столбца baz
и примените к каждому bar
в группе. Если первое значение в bar
не пусто, ничего не делайте с группой. Надеюсь, это прояснит любую путаницу. Большое спасибо за помощь!
Спасибо за обновления @MikeP — теперь это должно точно соответствовать описанной вами логике.
Альтернативным подходом к решению этой проблемы может быть использование pl.coalesce()
. ИМО также прекрасно читается:
Объедините первое значение baz
поверх foo
со значением bar
df.with_columns(
new_bar = pl.coalesce(
pl.col("bar"),
pl.col('baz').first().over("foo")
)
)
ОБНОВЛЕНИЕ: Спасибо @jqurious за указание на то, чего хотел ОП.
Это немного сложнее, поскольку они также хотят сохранить исходные ненулевые значения от bar
Если я заменю имя столбца bar
на new_bar
, я получу тот же результат, что и ваше решение.
Я предполагаю, что именно так был отформатирован пример кода OP — он уже включал преобразование. Вам нужно будет начать только с исходного кадра данных — и в этом случае одного объединения недостаточно. Я отредактировал вопрос, чтобы, надеюсь, прояснить это.
Поменяв порядок в coalesce
, я могу сопоставить результат, который вы получите.
Думаю, я слишком усложнил реальную задачу. Я думал, что пользователю нужен первый baz
, когда bar
имеет значение null или пусто, а не только первый baz
в каждой группе — моя вина. Я думаю, использование pl.col("bar").replace('', None)
в вашем ответе также будет обрабатывать случай пустой строки?
Не беспокойся! Спасибо, что нашли время поучаствовать в этом. Эти обсуждения очень полезны для понимания того, как писать идиоматический полярный код!
Я думаю, что мой ответ может быть неверным. использование примера с пустой строкой дает разные результаты:
'bar': ['a', 'a', 'a', 'x', '', None, 'c', 'c']
— Пожалуйста, проверьте это, если можете, и примите другой ответ, если это действительно так.