
Категориальные значения pandas: как проверить неизвестные статусы в выгрузке


Если в выгрузке появился новый статус, его легко потерять двумя разными способами. Первый — привести колонку к dtype="category" и решить, что справочник уже проверен. Но при таком приведении pandas по умолчанию просто соберет категории из самих данных, то есть новый статус станет обычной категорией и не будет считаться ошибкой. Второй способ опаснее: задать CategoricalDtype со своим списком статусов и сразу привести колонку к нему. Тогда значения вне справочника превратятся в NaN и смешаются с настоящими пропусками.
Поэтому на вопрос «как проверить неизвестные статусы в выгрузке, не превращая их молча в пропуски?» ответ такой: сначала хранить исходное значение, затем сделать аккуратную нормализацию, после этого отдельно вычислить маски missing и unknown, и только потом приводить подтвержденные значения к категориальному типу. Такой порядок дает проверяемый отчет и не стирает сигнал о новых статусах.
Почему простое приведение к category не решает задачу
У категориального типа в pandas есть полезное свойство: колонка хранит ограниченный набор допустимых значений и может иметь явный порядок. Но сама по себе категория не равна валидации справочника. Если написать astype("category"), pandas выведет список категорий из тех значений, которые уже встретились в колонке. Для сжатия памяти это удобно, для контроля качества — нет. Статус возврат, которого нет в вашем процессе, просто окажется еще одной категорией рядом с новый и оплачен.
Обратная крайность — заранее задать список допустимых статусов через CategoricalDtype. Это уже ближе к справочнику, потому что вы фиксируете разрешенные значения. Но здесь есть другой риск: все, что не входит в список категорий, станет NaN. После такого шага невозможно по самой категориальной колонке понять, где в исходнике было пусто, а где пришел новый или ошибочный статус. Для контроля выгрузки это слишком поздно.
Практический вывод простой: категориальный тип стоит использовать после проверки, а не вместо нее. Валидация отвечает на вопрос «что пришло и что с этим не так», а category — на вопрос «как удобно хранить и использовать уже проверенные значения».
Справочник статусов: задайте его отдельно от текущей выгрузки
Справочник должен жить отдельно от данных за день или за неделю. Если вы строите его из unique() по текущей колонке, вы не проверяете выгрузку, а просто переписываете ее состав в более компактном виде. Кроме того, уникальные значения показывают только то, что встретилось в этом файле, а не полный набор допустимых статусов. Поэтому опираться на них как на эталон неудобно: отсутствующий сегодня, но допустимый статус выпадет из такого «справочника» без всякой причины.
Для учебной задачи возьмем условный справочник заказов: новый, оплачен, отгружен, отменен. Это и есть список, с которым мы будем сравнивать вход. Важно, что он задается явно в коде или приходит из отдельной таблицы, а не извлекается из самой проверяемой выгрузки.
Еще один полезный принцип: справочник должен описывать бизнес-смысл, а не все возможные опечатки пользователей. Поэтому в него обычно не включают варианты вроде оплочен. Исправление известных синонимов лучше делать отдельным шагом нормализации, а не расширять справочник до бесконечности.
Нормализация входа без молчаливой подмены смысла
Нормализация нужна не для того, чтобы «натянуть» любой текст на справочник, а чтобы убрать технический шум: лишние пробелы, разный регистр, различие между ё и е, пустую строку вместо отсутствующего значения. Это безопасные операции, потому что они не придумывают новый статус, а лишь приводят запись к устойчивой форме.
Учебный пример. Ниже — условная выгрузка в среде Python с pandas. В ней есть корректные статусы, пустые значения, опечатка и новый статус, которого нет в справочнике. Мы специально оставляем колонку status_raw как есть, чтобы потом показать нарушение в исходном виде, а не только после очистки.
import pandas as pd
from pandas.api.types import CategoricalDtype
orders = pd.DataFrame({
"order_id": [101, 102, 103, 104, 105, 106, 107, 108, 109],
"status_raw": [
" Новый ",
"ОПЛАЧЕН",
"отгружен",
"возврат",
"",
None,
"оплочен",
"отменён",
"Оплата получена",
],
})
reference = pd.Index(["новый", "оплачен", "отгружен", "отменен"])
aliases = {"оплата получена": "оплачен"}
def normalize_status(s: pd.Series) -> pd.Series:
out = s.astype("string")
out = out.str.strip().str.lower()
out = out.str.replace(r"\s+", " ", regex=True)
out = out.str.replace("ё", "е", regex=False)
out = out.mask(out.eq(""), pd.NA)
out = out.replace(aliases)
return out
orders["status_norm"] = normalize_status(orders["status_raw"])Здесь нормализация делает четыре проверяемых вещи. " Новый " становится новый, ОПЛАЧЕН — оплачен, отменён — отменен, а пустая строка превращается в настоящий пропуск. Отдельно задан один согласованный алиас: оплата получена переводится в оплачен. При этом возврат и оплочен никуда не исчезают: они сохраняются как подозрительные значения, которые и нужно проверить.
Как найти неизвестные значения до приведения к категориальному типу
После нормализации задача сводится к трем маскам: пропуск, корректное значение из справочника и неизвестный статус. Ключевой момент в том, что неизвестное значение — это не пропуск. Оно присутствует в файле, просто не совпадает со справочником. Если смешать эти два случая, отчет потеряет смысл: вы не поймете, нужно чинить источник, обновлять справочник или просто дожидаться заполнения поля.
is_missing = orders["status_norm"].isna()
is_known = orders["status_norm"].isin(reference)
is_unknown = (~is_missing) & (~is_known)
orders["problem"] = "ok"
orders.loc[is_missing, "problem"] = "missing"
orders.loc[is_unknown, "problem"] = "unknown_status"
violations = orders.loc[
orders["problem"] != "ok",
["order_id", "status_raw", "status_norm", "problem"]
].copy()
summary = (
violations.groupby(["problem", "status_norm"], dropna=False)
.size()
.reset_index(name="rows")
.sort_values(["problem", "rows"], ascending=[True, False])
)
cat_type = CategoricalDtype(categories=reference.tolist(), ordered=False)
orders["status_checked"] = orders["status_norm"].where(is_known, pd.NA)
orders["status_cat"] = orders["status_checked"].astype(cat_type)В этом учебном наборе результат легко проверить вручную. Корректных строк после нормализации получилось 5: новый, оплачен, отгружен, отменен и алиас оплата получена → оплачен. Неизвестных статусов 2: возврат и оплочен. Пропусков тоже 2: пустая строка и None. Итого 5 + 2 + 2 = 9 строк, арифметика сходится.
Теперь видно, почему прямое приведение к CategoricalDtype без промежуточной проверки неудобно. Если бы мы сразу сделали orders["status_norm"].astype(cat_type), то получили бы 4 значения NaN: два настоящих пропуска и два неизвестных статуса. Число строк с проблемой сохранилось бы, а вот причина проблемы — нет. Разделение на missing и unknown_status нужно сделать раньше.
| Проверка | Результат в примере | Что это значит |
|---|---|---|
| Корректные статусы | 5 | Их можно безопасно приводить к category |
| Неизвестные статусы | 2 | Нужна проверка источника или справочника |
| Пропуски | 2 | Поле не заполнено, это отдельный тип нарушения |
NaN после прямого astype(cat_type) | 4 | Неизвестные и пустые значения смешались бы |
Отчет о нарушениях: что сохранять и как использовать дальше
Для отчета о качестве выгрузки полезны две сущности. Первая — список проблемных строк с идентификатором, исходным значением и нормализованной формой. Он нужен, когда вы идете к владельцу источника и хотите показать конкретные записи. Вторая — сводка по типам нарушений и значениям. Она помогает быстро увидеть, что происходит: один новый статус появился впервые, массово пришла пустая колонка или в данных пошла серия одинаковых опечаток.
В нашем примере список нарушений покажет четыре строки: две с missing, одну с unknown_status = возврат и одну с unknown_status = оплочен. Это уже практически полезный результат: по возврат вы решаете, действительно ли процесс изменился и нужно расширить справочник; по оплочен видно, что это, скорее всего, опечатка или ошибка источника; по пропускам можно отдельно считать долю незаполненных статусов и следить за ней во времени.
Рабочее правило для пайплайна такое: храните status_raw, создавайте status_norm, отмечайте problem, и только затем формируйте status_cat для дальнейшей аналитики. Тогда группировки, фильтры и сводные таблицы опираются на чистый категориальный столбец, а диагностика не теряется. Если позже справочник расширится, вы сможете пересчитать только этап валидации и увидеть, какие вчерашние ошибки стали допустимыми значениями, а какие нет.
Если хотите потренировать именно такую связку шагов, в сохраненном описании каталога курса «Pandas для анализа данных: практический курс» есть темы про очистку и предобработку данных, фильтрацию, группировку и агрегацию, а также автоматизацию отчетов. Официальная страница курса недоступна, поэтому ориентироваться можно только на сохраненное описание каталога.





















