2026, Jan 01 18:02

Почему pyjanitor теряет _metadata у подклассов pandas и как это исправить

Почему pyjanitor сбрасывает _metadata у подклассов pandas.DataFrame, и как хелпер carry_meta помогает сохранять атрибуты в цепочках методов в pandas-пайплайнах.

Пользовательские подклассы DataFrame в pandas — мощный способ прикреплять к данным доменное состояние через _metadata. Но как только в цепочку попадает pyjanitor, это состояние может незаметно исчезнуть. В результате посреди аккуратной цепочки методов вы получаете сбивающий с толку AttributeError.

Минимальный пример, воспроизводящий проблему

Ниже — фрагмент, который определяет подкласс DataFrame с одним пользовательским атрибутом, пропускает его через пару операций pandas, а затем — через преобразование из pyjanitor. Атрибут переживает «родной» шаг pandas, но пропадает после вызова janitor.

import pandas as pd
import janitor  # noqa: F401
import pandas_flavor as pf

# См.: https://pandas.pydata.org/pandas-docs/stable/development/extending.html#define-original-properties
class CustomFrame(pd.DataFrame):

    _metadata = ["flag"]

    @property
    def _constructor(self):
        return CustomFrame

@pf.register_dataframe_method
def setflag(self):
    new_obj = CustomFrame(self)
    new_obj.flag = 2
    return new_obj

@pf.register_dataframe_method
def showflag(self):
    print(self.flag)
    return self

frame = pd.DataFrame(
    {
        "Year": [1999, 2000, 2004, 1999, 2004],
        "Taxon": [
            "Saccharina",
            "Saccharina",
            "Saccharina",
            "Agarum",
            "Agarum",
        ],
        "Abundance": [4, 5, 2, 1, 8],
    }
)

Это работает и выводит 2, потому что подкласс и его метаданные сохраняются:

ok = frame.setflag().query("Taxon=='Saccharina'").showflag()

А здесь возникнет AttributeError: 'DataFrame' object has no attribute 'flag', поскольку вызов janitor возвращает обычный DataFrame — без вашего подкласса и его метаданных:

idx = pd.Index(range(1999, 2005), name="Year")
bad = frame.setflag().complete(idx, "Taxon", sort=True).showflag()

Что происходит

Функции pyjanitor часто создают новые структуры данных «с нуля», используя pandas.DataFrame как шаблон, либо объединяют данные через операции вроде pandas.merge. В первом случае вы получаете новый DataFrame, который несёт только стандартный _metadata. Во втором возвращаемый объект может вовсе не сохранить ваш подкласс, и нет гарантий, что его метаданные будут перенесены. Такое поведение обусловлено реализацией самих методов и не является багом pandas. Вокруг _metadata есть смежные обсуждения в репозитории pandas-dev, но коротко: эти функции janitor не распространяют подкласс и его атрибуты автоматически.

Практическое решение с пользовательским помощником для цепочек вызовов

В связке pandas 2.2.3 и janitor 0.31.0 надёжный способ сохранить метаданные — оборачивать функции, которые могут вернуть «голый» DataFrame, и явно восстанавливать ваш подкласс и его _metadata. Хелпер ниже делает именно это и органично встраивается в цепочки методов.

@pf.register_dataframe_method
def carry_meta(df_obj: pd.DataFrame, fn: callable, *args, **kwargs):
    result = fn(df_obj, *args, **kwargs)
    if isinstance(result, pd.DataFrame):
        result = df_obj.__class__(result)
        for name in df_obj._metadata:
            setattr(result, name, getattr(df_obj, name, None))
    return result

Используйте его, чтобы выполнить исходную задачу и сохранить пользовательский атрибут:

fixed = (
    frame
    .setflag()
    .carry_meta(janitor.complete, idx, "Taxon", sort=True)
    .showflag()
)

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

Цепочки методов — ключевая часть рабочих процессов в pandas и pyjanitor. Если ваш пайплайн опирается на _metadata (например, чтобы переносить конфигурацию или контекст между шагами), потеря метаданных в середине ломает корректность и усложняет отладку. Небольшой хелпер, который заново оборачивает результат в ваш подкласс и возвращает атрибуты из _metadata, делает преобразования предсказуемыми от начала до конца.

Выводы

Если вы наследуетесь от pandas.DataFrame и полагаетесь на _metadata, учитывайте, что методы pyjanitor могут возвращать обычные DataFrame и тем самым терять ваши пользовательские атрибуты. Когда знаете, что функция создаёт новый объект или объединяет данные, пропускайте вызов через обёртку вроде carry_meta, чтобы заново инстанцировать подкласс и скопировать нужные атрибуты. С этим приёмом ваши цепочки остаются лаконичными, метаданные — целыми, а финальные шаги (например, вывод или использование атрибута) работают как задумано.