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, чтобы заново инстанцировать подкласс и скопировать нужные атрибуты. С этим приёмом ваши цепочки остаются лаконичными, метаданные — целыми, а финальные шаги (например, вывод или использование атрибута) работают как задумано.