MATCH
Streamlit-приложение, которое подбирает до трёх event-подрядчиков из каталога (66 профилей) и объясняет выбор фактами профиля.
О проекте
Команда: InzhuNurlan
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Streamlit-приложение, которое подбирает до трёх event-подрядчиков из каталога (66 профилей) и объясняет выбор фактами профиля.
Как устроено
CSV-каталог → поиск по городу и категории → hard-фильтры (busy_dates, цена, формат, язык, длительность) → семантическое сходство пожеланий через OpenAI text-embedding-3-small (при отсутствии ключа TF-IDF + cosine) → final_score = 0.65·semantic + 0.25·budget + 0.10·duration со стабильной сортировкой → top-3. Объяснения детерминированно собираются из evidence без LLM; три исхода: MATCHED / CATEGORY_NOT_FOUND / NO_ELIGIBLE_CANDIDATES.
Ключевые решения
- hard filtering
- OpenAI embeddings
- TF-IDF
- cosine similarity
- hybrid weighted scoring
- evidence-based explanations
- pytest
Особенности
Жёсткое разделение: embeddings только ранжируют уже допустимых кандидатов и не могут отменить hard constraints; объяснения собираются из проверяемых фактов, а не генерируются LLM; есть скрипт воспроизводимых демо-сценариев и 27 тестов.
Стек
Python · pandas · NumPy · scikit-learn · Streamlit · pytest · python-dotenv
Модели
OpenAI text-embedding-3-small
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-55693f06-inzhunurlan.
README
README.mdMATCH
Умный подбор event-подрядчиков
MATCH помогает выбрать до трёх подходящих подрядчиков из уже существующего каталога и объясняет, почему каждый из них попал в рекомендацию.
Это не расширение каталога и не поиск новых исполнителей. Пользователь уже получил каталог для своего города, а MATCH сокращает выбор до вариантов, которые проходят обязательные условия и лучше соответствуют пожеланиям.
Проблема
Пользователь задаёт город, дату, формат мероприятия, категорию подрядчика и бюджет. Дополнительно можно указать язык, длительность и свободный текст с пожеланиями.
Проблема состоит не в том, чтобы показать ещё больше профилей. Нужно выбрать несколько подходящих профилей из существующего каталога и объяснить решение конкретными данными: ценой, форматом, языком, доступностью, длительностью и смысловым совпадением.
MATCH не использует generic-описания вроде «Отличный выбор для вашего мероприятия». Объяснения собираются из проверяемых фактов конкретного профиля.
Что реализовано
- загрузка предоставленного CSV-каталога с сохранением исходных полей;
- поиск профилей по городу и категории;
- hard-фильтры по
busy_dates, стартовой цене, формату мероприятия, языку и длительности; - semantic matching пожеланий пользователя через OpenAI embeddings;
- детерминированный TF-IDF + cosine similarity fallback без OpenAI;
- гибридное ранжирование eligible-кандидатов;
- возврат максимум трёх рекомендаций на уровне
recommender.py; - evidence-based explanations на русском языке;
- прозрачные поля
synthetic,city_imputedиprice_imputed; - Streamlit-интерфейс с карточками, empty states и диагностикой pipeline;
- автоматические тесты ranking, explanations и recommender;
- скрипт воспроизводимого поиска демонстрационных сценариев
scripts/find_demo_cases.py.
Как работает решение
- Пользователь задаёт параметры события.
- MATCH находит профили нужной категории в выбранном городе.
- Hard filters исключают занятых, слишком дорогих и неподходящих по обязательным условиям подрядчиков.
- Только среди оставшихся eligible-кандидатов рассчитывается смысловое соответствие пожеланиям.
- Для каждого eligible-кандидата рассчитываются budget fit и duration flexibility.
- Итоговый score используется для детерминированной сортировки.
recommender.pyвыбирает первые три строки ранжированного результата.- Для карточек формируются детерминированные evidence-based explanations.
- Пользователь получает карточки или объяснение, почему результатов нет.
AI не может отменить hard constraints: занятый подрядчик, профиль выше бюджета или профиль с неподдерживаемым форматом не возвращается из ranking в рекомендации.
Результаты поиска
MATCHED
В городе и категории есть профили, и хотя бы один из них проходит все обязательные условия. Возвращается от одной до трёх карточек.
CATEGORY_NOT_FOUND
В выбранном городе нет ни одного профиля запрошенной категории. Это отдельный результат, потому что проблема находится на уровне каталога, а не ограничений заказа.
NO_ELIGIBLE_CANDIDATES
Профили категории в городе существуют, но все нарушают хотя бы одно условие: заняты на выбранную дату, имеют стартовую цену выше бюджета, не поддерживают формат или язык либо не проходят по длительности.
В диагностике одна строка может учитываться сразу по нескольким причинам. Поэтому counts причин не являются взаимоисключающими и не обязаны складываться в число профилей.
AI и semantic matching
Основной semantic provider в src/ranking.py — модель OpenAI text-embedding-3-small. Пожелания пользователя используются как основной сигнал запроса, а представление подрядчика строится из description, categories, event_formats и languages. Сходство считается через cosine similarity.
OpenAI не выбирает подрядчиков напрямую и не получает право нарушать eligibility. Embeddings применяются только после hard filtering — для ранжирования уже допустимых кандидатов.
Если OPENAI_API_KEY отсутствует, OpenAI недоступен или embedding-запрос завершается ошибкой, ranking автоматически переключается на детерминированный TF-IDF + cosine similarity fallback. Fallback нужен для устойчивости MVP; OpenAI и TF-IDF могут по-разному оценивать смысловое совпадение.
Ranking
Текущий итоговый score:
final_score =
0.65 × semantic_score
+ 0.25 × budget_score
+ 0.10 × duration_score
semantic_score— соответствие текстовым пожеланиям и контексту запроса.budget_score— соответствие стартовой цены бюджету. Самый дешёвый профиль автоматически не считается лучшим.duration_score— запас поmax_hours, если пользователь задал длительность.NaNозначает, что длительность не привязана к физическому присутствию, и штраф не применяется.
price_from_kzt — стартовая цена «от», а не гарантированная итоговая стоимость заказа.
После расчёта score используется стабильная сортировка с такими ключами:
final_scoreпо убыванию;price_from_kztпо возрастанию;idпо возрастанию.
Объяснимость и прозрачность
src/explanations.py не обращается к LLM для свободной генерации текста. Объяснение детерминированно собирается из evidence:
- стартовая цена относительно бюджета;
- формат мероприятия и доступность на дату, подтверждённые hard filtering;
- запрошенный язык;
- наибольший запас длительности среди рекомендаций;
- самое сильное semantic совпадение среди рекомендаций;
- самая низкая стартовая цена среди рекомендаций.
Набор причин ограничивается конкретными фактами профиля и запроса, поэтому карточки остаются различимыми даже без имени подрядчика.
Поля прозрачности synthetic, city_imputed и price_imputed сохраняются для интерфейса. Они описывают происхождение или восстановление данных и не являются положительными сигналами ranking.
Данные
В каталоге data/hackathon_dataset_anonymized.csv находится 66 профилей и 17 категорий:
| Показатель | Значение |
|---|---|
| Всего профилей | 66 |
| Алматы | 50 |
| Астана | 15 |
| Зарубежье | 1 |
synthetic=True | 13 |
city_imputed=True | 8 |
price_imputed=True | 18 |
max_hours не задан | 9 |
Плотные категории:
- Ведущий — 15 профилей;
- Фотограф — 12;
- Банкетный зал — 8.
В каталоге также есть редкие категории, включая категории с тремя профилями. Приложение не изменяет исходный CSV.
Это не задача supervised model training: в проекте нет обучения модели и train/test split. Система выполняет filtering и semantic ranking над уже существующим каталогом.
Технологии
| Технология | Для чего используется |
|---|---|
| Python | код приложения и утилит |
| pandas | загрузка и обработка каталога |
| NumPy | численные операции со score и embeddings |
| scikit-learn | TF-IDF и cosine similarity fallback |
| OpenAI API | embeddings text-embedding-3-small |
| Streamlit | локальный пользовательский интерфейс |
| pytest | автоматические тесты |
| python-dotenv | доступная зависимость для локальной работы с .env, если она используется окружением |
Архитектура
flowchart TD
U[User Query] --> C[City + Category discovery]
C --> O{Profiles in city + category?}
O -- no --> CNF[CATEGORY_NOT_FOUND]
O -- yes --> H[Hard Filters]
H --> HF[availability / busy_dates<br/>budget<br/>event format<br/>language<br/>duration]
HF --> E[Eligible Candidates]
E --> S[Semantic Matching]
S --> OA[OpenAI embeddings<br/>text-embedding-3-small]
S --> TF[TF-IDF fallback]
OA --> R[Deterministic Ranking]
TF --> R
R --> T[Top 3]
T --> X[Evidence-Based Explanations]
X --> M[Recommendation Cards / MATCHED]
H --> N{Eligible candidates remain?}
N -- no --> NE[NO_ELIGIBLE_CANDIDATES]
N -- yes --> S
Структура проекта
app.py
requirements.txt
README.md
.env.example
data/
hackathon_dataset_anonymized.csv
src/
data_loader.py
models.py
filters.py
ranking.py
explanations.py
recommender.py
scripts/
find_demo_cases.py
validate_data.py
tests/
test_ranking.py
test_explanations.py
test_recommender.py
Установка в Windows PowerShell
git clone https://github.com/BAITC-Hacks/hack-55693f06-inzhunurlan.git
cd hack-55693f06-inzhunurlan
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
Если PowerShell запрещает запуск скриптов активации, политика окружения может потребовать отдельной настройки. Это ограничение локального PowerShell, а не логики приложения.
OpenAI API key
Настоящий ключ не хранится в репозитории. Для PowerShell можно задать placeholder так:
$env:OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
Для локальной настройки также доступен шаблон .env.example.
При отсутствии ключа приложение продолжает работу через TF-IDF fallback.
Запуск
streamlit run app.py
После запуска Streamlit обычно открывает локальное приложение по адресу http://localhost:8501.
Проверка
python -m pytest -q
python scripts/validate_data.py
python scripts/find_demo_cases.py
scripts/validate_data.py проверяет структуру и целостность предоставленного каталога до запуска recommendation pipeline.
Последний локально подтверждённый результат проекта:
27 passed
Скрипт scripts/find_demo_cases.py не вызывает OpenAI и помогает воспроизвести четыре сценария на реальном датасете.
Reproducible demo scenarios
Demo 1 — dense category
- Город: Алматы
- Дата:
2026-09-26 - Формат: конференция
- Категория: Ведущий
- Бюджет:
2 000 000 ₸ - City/category candidates: 10
- Eligible: 3
- Отклонения:
busy = 6,wrong_format = 5 - Результаты:
HK-44733 — Буллма,HK-88430 — Куррапика,HK-75012 — Джинбей
Demo 2 — rare category
- Город: Алматы
- Дата:
2026-09-23 - Формат: конференция
- Категория: Флорист
- Бюджет:
250 000 ₸ - City/category candidates: 2
- Eligible: 1
- Результат:
HK-39372 — Тони Тони Чоппер
Demo 3 — no eligible candidates
- Город: Алматы
- Дата:
2026-09-23 - Формат: день рождения
- Категория: Ведущий
- Бюджет:
400 000 ₸ - City/category candidates: 10
- Eligible: 0
- Причины:
busy = 4,over_budget = 10,wrong_format = 8
Counts причин не являются взаимоисключающими: один профиль может одновременно быть занят, превышать бюджет и не поддерживать формат.
Demo 4 — date sensitivity
Общие параметры: Алматы, конференция, Ведущий, бюджет 2 000 000 ₸, язык и длительность не указаны.
2026-09-23: eligibleHK-27222,HK-75012;2026-09-24: eligibleHK-44733,HK-75012.
Candidate pool меняется из-за busy_dates до semantic ranking. Разница не приписывается OpenAI.
Детерминизм
Hard filtering выполняется по фиксированным значениям запроса и каталога. Ranking использует стабильные сортировки и явные tie-breakers: итоговый score по убыванию, затем стартовая цена по возрастанию и id по возрастанию.
Для OpenAI-ветки воспроизводимость также зависит от одинаковых результатов semantic provider. TF-IDF fallback является локальным и детерминированным.
Ограничения MVP
- MATCH работает только с предоставленным каталогом и не добавляет новых подрядчиков.
price_from_kzt— стартовая цена, а не финальная смета.- Качество semantic matching зависит от заполненности и качества
description, категорий, форматов и языков в каталоге. - Доступность берётся из статических
busy_datesв пределах hackathon date window. - TF-IDF fallback может хуже передавать смысловые нюансы, чем embeddings.
- В проекте не реализованы бронирование, оплата, уведомления подрядчикам и внешняя синхронизация календарей.
- Каталог небольшой и содержит восстановленные/синтетические значения, отмеченные transparency flags.
Дальнейшее развитие
Возможные направления, которые не являются частью текущей реализации:
- предварительное кэширование embeddings профилей;
- масштабирование semantic retrieval на большой каталог;
- feedback-based ranking;
- интеграции с реальными календарями;
- booking workflow;
- дополнительные embedding или reranking providers.
Demo
Публичный deployment в текущем репозитории не настроен. Приложение запускается локально через Streamlit.
Технический вывод
Сначала ограничения. Затем смысл. MATCH отделяет eligibility от preference: обязательные бизнес-условия проверяются детерминированно, а AI используется только для смыслового ранжирования уже допустимых кандидатов. Поэтому рекомендации остаются объяснимыми и не возвращают профиль только из-за высокого semantic score, если он не проходит hard filters.
Авторы коммитов
История коммитов
- 2026-09-23 17:41docs: add final hackathon documentationInzhu
- 2026-09-23 17:23chore: add reproducible hackathon demo scenariosInzhu
- 2026-09-23 17:04style: finalize Streamlit hackathon interfaceInzhu
- 2026-09-23 16:32feat: integrate contractor recommendation pipelineInzhu
- 2026-09-23 16:18feat: add evidence-based recommendation explanationsInzhu
- 2026-09-23 16:04fix: add observable semantic ranking fallbackInzhu
- 2026-09-23 15:46feat: add hybrid semantic contractor rankingInzhu
- 2026-09-23 15:06feat: add contractor loading and eligibility filtersinnurlan
Все коммиты (10)
- 2026-09-23 15:01feat: add dataset validation pipelineinnurlan
- 2026-09-23 14:57chore: initialize vendor recommender projectinnurlan
Структура репозитория (18 файлов)
.env.example .gitignore README.md app.py data/hackathon_dataset_anonymized.csv requirements.txt scripts/find_demo_cases.py scripts/validate_data.py src/__init__.py src/data_loader.py src/explanations.py src/filters.py src/models.py src/ranking.py src/recommender.py tests/test_explanations.py tests/test_ranking.py tests/test_recommender.py
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.