Я делаю сервис, который наводится камерой на штрих-код и отдаёт светофор по составу плюс одну фразу человеческим языком. Побочный эффект такой работы — база. Сейчас в ней около 60 тысяч товаров из российских магазинов с разобранными составами.
В какой-то момент я перестал смотреть на отдельные пачки и посмотрел на полку целиком. Ниже — цифры, грабли парсинга и два места, где алгоритм врал. Про механику, без ссылок и названий.
Что считает алгоритм
ИИ в оценке нет. Это арифметика.
На вход идут: сахар на 100 г, соль, насыщенные жиры, трансжиры, позиция сахара в списке ингредиентов и добавки из «списка внимания». Каждый показатель даёт штрафные баллы, баллы складываются, сумма сравнивается с двумя порогами.
score = penalty_sugar(sugar_100g)
score += penalty_salt(salt_100g)
score += penalty_sat_fat(sat_fat_100g)
score += penalty_trans(trans_100g)
score += penalty_position(sugar_index) # где сахар стоит в составе
score += sum(penalty_additive(c) for c in codes if c in WATCHLIST)
color = GREEN if score <= T1 else (YELLOW if score <= T2 else RED)
Языковая модель в системе есть, но стоит она после вердикта: пересказывает готовый результат одной короткой фразой. Цвет она не выбирает, на баллы не влияет. Формула открытая — это пригодится в разделе про пороги, где меня будут бить.
Что оказалось на полке
Распределение по всей базе:
-
зелёных — около 43%
-
жёлтых — около 38%
-
красных — около 18%
Я ждал худшего. Ощущение «в магазине всё плохо» на цифрах не подтверждается: больше 40% ассортимента — нормальная еда. Крупы, молочка, вода, чай, овощи. Скучные категории, которые никто не обсуждает, потому что обсуждать нечего.
Красная зона тоже не монолит:
-
~40% красных — чистое питание: сахар, соль, жир. Добавок нет вообще.
-
~36% — нутриенты в норме, красный приехал из-за одной добавки.
-
остальные — пограничные. Держатся на одном показателе, который чуть перевалил. Сдвинь порог на пару единиц — половина уедет в жёлтый.
Отдельно я посчитал товары, где сахар стоит в первой тройке ингредиентов. Состав пишется по убыванию массы, так что первая тройка — это буквально «из чего продукт в основном сделан». Таких товаров неприлично много. И примерно половина из них на десерт не похожа: соусы, готовые завтраки, «фруктовые» творожки, часть хлеба.
Для этого, кстати, никакой софт не нужен. Читается глазами прямо у полки.
Пороги
Самое уязвимое место всей затеи. Я их не выдумывал, но и готовыми не взял — пришлось калибровать.
Ориентиры:
-
рекомендации ВОЗ по свободным сахарам и соли;
-
светофор британского FSA, у него пороги на 100 г опубликованы;
-
ТР ТС 029/2012 по добавкам.
Дальше начинается инженерия. Если поставить порог по сахару строго по ВОЗ, в красное уезжает половина молочки, включая обычный питьевой йогурт. Формально всё верно. Практически бесполезно: когда красное почти всё, цвет перестаёт нести информацию. Если порог смягчить, зелёным становится почти всё, и сервис не нужен.
Я калибровал по распределению внутри категорий и проверял контрольную выборку руками. Это не наука, это компромисс. Готов спорить в комментариях — формула открыта, конкретные значения можно обсуждать предметно.
Е-коды и грабли парсинга
В базе встречается 400+ уникальных Е-кодов. Под реальным вниманием токсикологов — горстка: нитриты в колбасе, часть синтетических красителей, диоксид титана. И претензии там обычно к дозе.
Остальное — витамины, кислоты, загустители. Е300 — витамин C. Е330 — лимонная кислота. Пектин, лецитин, сода. Буква Е означает «на вещество есть регламент», а не «химия».
Но с кодами есть проблема, которая портит статистику.
Производитель вправе писать добавку названием, а не кодом. Строка «стабилизатор полифосфат натрия, фиксатор окраски нитрит натрия» — это Е452 и Е250, и ни одного кода в ней нет. Парсер по кодам такое не видит.
Хуже, что недосчёт асимметричный. Он сильнее там, где производителю есть что прятать: «нитрит натрия» на этикетке звучит спокойнее, чем «Е250».
Лечится словарём синонимов. Беда в том, что имён у одного вещества много:
SYNONYMS = {
"E300": ["аскорбиновая кислота", "витамин c", "аскорбат"],
"E322": ["лецитин", "соевый лецитин", "лецитины"],
"E452": ["полифосфаты", "пирофосфат", "полифосфат натрия"],
"E250": ["нитрит натрия", "нитритная соль"],
}
А потом приходит засада с подстрокой:
# первая версия, выглядит разумно
if re.search(r"нитр", text):
codes.add("E250")
# ловит заодно нитраты, Е251 и Е252. Другие вещества, другой профиль риска.
# вторая версия
if re.search(r"нитрит", text):
codes.add("E250")
# «нитритная соль» — поймали, и правильно
# «нитрат натрия» — мимо, и тоже правильно
Такая ошибка не падает с исключением. Она просто тихо тащит лишние товары в статистику по нитритам.
Сейчас словарём покрыты только частые вещества. Хвост из редких написаний не разобран, и я не знаю, насколько он длинный. Честная формулировка: работает, но не полностью.
Два места, где алгоритм врал
Насыщенные жиры. В таблице на упаковке отдельной строки по ним часто нет. В первой версии я прикидывал их долей от общего жира. Логика вроде здравая. Результат — в красное уехали сыр, орехи, жирная рыба и оливковое масло.
Допущение я убрал. Несколько тысяч товаров честно вышли из красного. Правило, которое из этого осталось: пропуск в данных — не значение. Штрафовать за то, чего не знаешь, нельзя.
Вода. Этот баг нашли читатели, уже после публикации.
Питьевая вода получала зелёный и подпись «сбалансированный состав». В комментариях воду назвали цельнозерновой и предложили сварить на ней кашу. Скриншот собрал 90+ плюсов. Заслуженно.
Формально алгоритм прав: добавок нет, штрафов ноль, сумма ноль, зелёный. Ошибка смысловая. Отсутствие претензий к составу — не то же самое, что подтверждённая польза. Тот же баг сидел на соли.
Чинил в три захода.
-
Отправить все однокомпонентные товары в нейтральный статус. Неверно: гречке и молоку зелёный подходит нормально, менять надо было формулировку, а не цвет.
-
Отправить в нейтральный соль и сахар. Тоже неверно: так снимается единственное полезное предупреждение, которое там вообще есть.
-
Рабочий вариант: цвет отвечает на вопрос, превышает ли норму по этому веществу обычный человек на практике.
Как это ложится на примеры:
-
Соль. Норма ВОЗ — 5 г в сутки, в России реально едят около 11. Жёлтый.
-
Лимонная кислота. ADI не установлена, упереться в опасное количество нереально. Зелёный.
-
Нитриты. Есть механизм вреда, а не только вопрос дозы. Красный.
Штрих-коды съели больше времени, чем формула
Я думал, что сложное здесь — оценка. Нет. Сложное — идентификация товара.
Что вылезло:
-
Коды, начинающиеся с 2, — внутренние весовые коды магазина. В них зашит вес. Один и тот же окорок превращается в десятки «разных товаров».
-
Один товар живёт под несколькими кодами: другой объём, другой завод, ребрендинг.
-
Реже наоборот: один код на разные товары, код переиспользовали.
-
Ведущие нули.
0000050761166и50761166— это один товар. Но если хранить код числом, они разъезжаются. Дубли пришлось схлопывать отдельным проходом.
Стек при этом намеренно скучный: Python, PostgreSQL, никакого ML в оценке. Весь «интеллект» лежит в правилах и словарях, которые можно открыть и прочитать. Страницы отдаются с сервера без JS — их 60+ тысяч, они должны индексироваться.
Общий итог по данным получается такой: пугают не тем. Страх сфокусирован на букве Е, а основной вклад в красную зону дают сахар и соль. Сахар при этом ещё и прячется под десятком имён, а соль не прячется вовсе — написана русским языком в начале состава.
Формула открыта, вопросы по порогам принимаю.
Автор: Valery247500
