Классификатор должностей: сопоставление зашумлённых 1С-выгрузок с закрытым справочником
Детерминированный пайплайн сопоставляет зашумлённые названия должностей из выгрузки 1С с закрытым классификатором — без LLM, эмбеддингов и внешних API. Исходный код открыт, покрыт тестами.
- Масштаб
- 300 записей, справочник из 56 позиций
- Этап
- Открытый код
- Сдан
- август 2026
TL;DR. Названия должностей из выгрузки 1С — это свободный текст с опечатками, сокращениями и сленгом. Детерминированный пайплайн — нормализация → нечёткое лексическое сопоставление (RapidFuzz) → калибровка уверенности с ручной проверкой всех отказов по умолчанию — сопоставляет такие строки с закрытым справочником из 56 позиций без LLM, эмбеддингов и внешних API. Код открыт.
Ситуация: должности в 1С не совпадают со справочником
Кадровые данные из 1С — свободный текст: «маш. бетоононасоса», «инж. пто», «бульдозерист 5 разряда», «ООО СтройМонтаж, вахта». Справочник закрыт и содержит всего 56 канонических позиций. Прямое сравнение строк почти никогда не срабатывает, а вручную разбирать сотни записей — не масштабируется.
Задача
- Сопоставлять зашумлённые строки со справочником, возвращая код, каноническое название и уверенность.
- Отсекать должности не по профилю, не придумывая код там, где совпадения нет.
- Обойтись без LLM, эмбеддингов и внешних API — по условиям задачи.
- Размечать записи, которые требуют проверки человеком, а не тихо терять ошибку.
Что сделали
Детерминированный пайплайн из трёх стадий.
Нормализация. Удаление юрлиц и разрядов, раскрытие отраслевых сокращений, приведение синонимов к канонической форме, исправление частых опечаток. 300 входных строк схлопываются в 78 уникальных канонических форм — большая часть сравнений сводится к точным совпадениям.
Нечёткое сопоставление. Взвешенная метрика RapidFuzz (0.5 · token_sort_ratio + 0.5 · ratio) сравнивает нормализованную строку с каждой из 56 позиций справочника.
Калибровка уверенности. По умолчанию любой отказ («нет соответствия») уходит на ручную проверку независимо от скора: низкий скор — не доказательство, что должность не по профилю, он может означать, что нормализация не распознала непривычную формулировку реальной должности. Пороги, веса и словари вынесены в конфиг и текстовые файлы — ретюнинг под новые данные не требует правки кода.
Проект покрыт тестами, исходный код опубликован на github.com/pimenoffd.
Результат
- Accuracy 100% на размеченной выборке.
- На 300 записях — 277 матчей приняты автоматически, 23 отправлены на проверку (7.7% при консервативном дефолте, снижается до 5.3% при более строгом пороге).
- Обработка полного датасета — менее 0.2 секунды на CPU, без GPU и внешних сервисов.
- Тестовое покрытие на pytest, открытый исходный код.
Ключевые технические решения
- Лексическое сопоставление вместо эмбеддингов. Справочник закрыт и содержит всего 56 позиций, шум в данных — орфографический и структурный, а не семантический. На таком шуме лексический поиск не уступает плотному семантическому, а инфраструктуры под него (GPU, модель) не требует вовсе.
- Ручная проверка всех отказов по умолчанию. Низкий скор при отсеве — не проверенный факт «не по профилю»: единственный известный случай пропуска статистически неотличим от легитимных отказов при текущем объёме данных. Сужать порог сейчас значило бы подгонять его под один пример, а не считать его.
- Пороги и словари — в конфиге, а не в коде. Отдельный конфиг-файл для порогов и весов, текстовые файлы для словарей сокращений, синонимов и непрофильной лексики. Ретюнинг под новый поток данных — правка файла, а не деплой.
- Обучаемая модель отклонена сознательно. Разметочная выборка (50 строк) меньше числа классов (56) — обучение почти гарантированно переобучилось бы и потеряло редкие классы.
Частые вопросы
Почему не эмбеддинги или LLM?
Шум в данных — опечатки и сокращения, а не смысловая неоднозначность. При 56 классах лексическое сопоставление даёт то же качество без GPU, внешних API и затрат на инференс.
Что происходит с должностями, которых нет в справочнике?
Система размечает их как несоответствие и по умолчанию отправляет на проверку человеку — не присваивает код «на всякий случай» и не отбрасывает запись молча.
Можно ли использовать это на своём справочнике?
Код открыт на GitHub. Под свой справочник и свои сокращения нужно донастроить словари и пороги — без изменения логики пайплайна.
Что было бы дальше
Естественное продолжение — active learning по записям, отправленным на проверку (подтверждённые правки пополняют словарь синонимов), подключение отраслевых справочников (ЕТКС, ОКПДТР) и статистическая калибровка порога ручной проверки по мере накопления эксплуатационных данных.
Если у вас есть кадровые данные, которые не совпадают со справочником построчно, — обсудим за 30 минут.
Стек
- Python
- RapidFuzz
- pytest