2025, Nov 01 07:46

Нормализация имён в Polars с coalesce: простой приём

Как привести разрозненные поля имени к единой схеме в Polars: разбор «Фамилия, Имя», list.get и coalesce без многословных ветвлений. Пошаговый пример и код.

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

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

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

import polars as pl
rows = [
    {"name_full": "McCartney, Paul"},
    {"name_last": "Lennon", "name_first": "John"},
    {"name_full": "Starr, Ringo"},
    {"name_last": "Harrison", "name_first": "George"}
]
people_df = pl.DataFrame(rows)

Почему наивный подход неудобен

Один способ — разбить полное имя, обрезать пробелы и для каждого целевого столбца прописать ветвление. Это работает, но на каждый столбец приходится отдельное условие, а по мере роста схемы такой код быстро разрастается и становится неудобным.

(
    people_df.with_columns(
        pl.col("name_full")
          .str.split(",")
          .list.eval(pl.element().str.strip_chars())
          .alias("pieces")
    ).with_columns(
        pl.when(pl.col("name_last").is_null())
          .then(pl.col("pieces").list.get(0, null_on_oob=True))
          .otherwise(pl.col("name_last")).alias("name_last"),
        pl.when(pl.col("name_first").is_null())
          .then(pl.col("pieces").list.get(1, null_on_oob=True))
          .otherwise(pl.col("name_first")).alias("name_first")
    ).select(pl.all().exclude("name_full", "pieces"))
)

Функционально всё верно, но каждый новый столбец влечёт ещё одну ветку — лишнюю обвязку и визуальный шум.

Лаконичный приём с coalesce

Если полное имя следует шаблону с запятой, достаточно хранить разобранный список токенов и отдавать приоритет уже заполненным раздельным столбцам. Ключевой приём — использовать coalesce, которая возвращает первое ненулевое значение. Это устраняет повторяющиеся условия и сохраняет смысл.

people_df.with_columns(
    pl.col("name_full")
      .str.split(",")
      .list.eval(pl.element().str.strip_chars())
      .alias("pieces")
).select(
    pl.coalesce(
        pl.col("name_last"),
        pl.col("pieces").list.get(0, null_on_oob=True)
    ).alias("name_last"),
    pl.coalesce(
        pl.col("name_first"),
        pl.col("pieces").list.get(1, null_on_oob=True)
    ).alias("name_first")
)

Так парсинг остаётся явным и компактным, а логика слияния сводится к паре читаемых выражений. Вызовы list.get используют null_on_oob=True, чтобы избежать ошибок, если в строке нет полного имени.

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

В конвейерах обработки данных часто приходится сводить разнородные входы к единой схеме. Компактные выражения уменьшают стоимость поддержки, снижают риск ошибок и ясно отражают намерение. В Polars опора на coalesce позволяет выразить «предпочитай этот столбец, иначе возьми разобранный запасной» без дублирования логики ветвлений для каждого поля.

Выводы

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

Материал основан на вопросе с StackOverflow от dewser_the_board и ответе Jonathan.