chore: каркас проекта — контракты, справочник услуг, синтетические фикстуры
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Скопируй в .env и заполни значения. Файл .env в .gitignore — в публичный
|
||||
# репозиторий секреты не попадают.
|
||||
|
||||
# Пароль базы данных PostgreSQL
|
||||
DB_PASSWORD=
|
||||
|
||||
# Строка подключения к базе (для локального запуска API вне Docker)
|
||||
DATABASE_URL=postgresql://medarchive:CHANGE_ME@localhost:5432/medarchive
|
||||
|
||||
# Ключ Gemini для распознавания сложных страниц (Vision). Получить заранее.
|
||||
GEMINI_API_KEY=
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Node / Next.js
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
*.log
|
||||
|
||||
# Claude Code worktrees (изоляция агентов)
|
||||
.claude/worktrees/
|
||||
|
||||
# Вендоренные ассеты reveal.js (тянутся скриптом fetch-reveal.sh)
|
||||
pitch/deck/reveal/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Данные клиник: реальный архив и справочник в публичный репозиторий не коммитим
|
||||
data/
|
||||
*.sqlite
|
||||
@@ -0,0 +1,58 @@
|
||||
# MedArchive — обработка архива прайсов клиник-партнёров
|
||||
|
||||
Terricon Valley · Medtech Hackathon (26–28.06.2026).
|
||||
|
||||
**Живой демо: https://med.secondbrain.tools**
|
||||
|
||||
Система автоматически разбирает архив разнородных прайс-листов клиник (PDF, сканы, DOCX,
|
||||
XLSX/XLS), извлекает услуги и цены, нормализует их к справочнику из 1281 услуги и даёт поиск
|
||||
и сравнение «кто оказывает услугу и по какой цене».
|
||||
|
||||
## Результат на тестовом архиве (8 клиник, 10 файлов)
|
||||
|
||||
- **8 871 позиция** извлечена: 6 клиник из 8 — нативными парсерами, трудные PDF (битый
|
||||
текстовый слой, нестандартная вёрстка) — через Gemini Vision.
|
||||
- **73,9% позиций нормализованы автоматически** (цель ТЗ — не ниже 70%).
|
||||
- Остальное уходит в очередь ручной разметки, как и предполагает ТЗ.
|
||||
|
||||
Полный отчёт о качестве — `docs/quality_report.json`.
|
||||
|
||||
## Стек и решения
|
||||
|
||||
| Слой | Решение |
|
||||
|---|---|
|
||||
| Извлечение | Python: pdfplumber + PyMuPDF, openpyxl/xlrd, python-docx. Таблицы из PDF без линий восстанавливаются по координатам слов |
|
||||
| Vision-добор | Gemini (`gemini-2.5-flash`) — для сканов и битого текстового слоя |
|
||||
| Нормализация | Каскад: код тарификатора → точное совпадение → эмбеддинги Gemini → нечёткое сравнение → очередь `unmatched` |
|
||||
| Валидация | Правила ТЗ §4.4: положительность цены, резидент ≥ нерезидент, аномалии, версионирование |
|
||||
| Хранилище | SQLite (MVP, разворачивается без Docker). Боевая схема PostgreSQL + pgvector — в `db/migrations` |
|
||||
| API | FastAPI: 7 эндпоинтов ТЗ + `/stats`, OpenAPI на `/docs` |
|
||||
| Фронт | Одностраничный (Tailwind): поиск услуги и сравнение цен по клиникам |
|
||||
| Деплой | Контейнер API за Caddy на Selectel KZ |
|
||||
|
||||
## Запуск локально
|
||||
|
||||
```bash
|
||||
python -m venv .venv && .venv/bin/pip install -e .
|
||||
# архив прайсов → data/raw/, справочник → data/reference/dictionary.xlsx
|
||||
GEMINI_API_KEY=... python scripts/run_pipeline.py # → data/medarchive.db + отчёт
|
||||
uvicorn api.main:app # → http://localhost:8000/docs
|
||||
# открыть web/index.html?api=http://localhost:8000
|
||||
```
|
||||
|
||||
Эмбеддинги и Vision вызываются через Gemini API, поэтому локальная модель не нужна и контейнер
|
||||
остаётся лёгким. Поиск и сравнение в готовой базе работают без обращений к API.
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
contracts/ — общие модели данных (контракт между модулями)
|
||||
db/ — схема PostgreSQL + pgvector
|
||||
etl/ — извлечение, нормализация, валидация, конвейер
|
||||
api/ — REST API поиска и сравнения
|
||||
web/ — одностраничный фронт
|
||||
infra/ — деплой (compose, Caddyfile)
|
||||
docs/ — отчёт о качестве, план
|
||||
pitch/ — презентация и демо-сценарий
|
||||
scripts/ — запуск конвейера и проверки
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Общие модели данных — контракт между агентами (Pydantic v2).
|
||||
|
||||
`RawRow` и `ParsedDocument` — выход любого парсера (граница агентов B → C).
|
||||
`MatchResult` — выход сопоставления (агент C). `*Out`-модели — выдача API (агент D),
|
||||
её же потребляет фронт (агент E). Любая правка этих типов проходит через оркестратора.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Карта тарифов: ключ — тип цены, значение — сумма в KZT.
|
||||
# Возможные ключи: resident, nonresident, cis, far, insurance, partner.
|
||||
PriceTiers = dict[str, float]
|
||||
|
||||
|
||||
class RawRow(BaseModel):
|
||||
"""Одна сырая строка прайса — единый выход любого парсера."""
|
||||
|
||||
service_name_raw: str
|
||||
service_code_source: str | None = None
|
||||
prices: PriceTiers = Field(default_factory=dict)
|
||||
unit: str | None = None
|
||||
section: str | None = None # заголовок секции — контекст специальности
|
||||
|
||||
|
||||
class ParsedDocument(BaseModel):
|
||||
"""Результат разбора одного файла прайса."""
|
||||
|
||||
file_name: str
|
||||
file_format: str # pdf / scan_pdf / docx / xlsx / xls
|
||||
partner_name: str | None = None
|
||||
city: str | None = None
|
||||
effective_date: date | None = None
|
||||
rows: list[RawRow] = Field(default_factory=list)
|
||||
raw_content: str = ""
|
||||
parse_log: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MatchResult(BaseModel):
|
||||
"""Результат сопоставления строки со справочником."""
|
||||
|
||||
service_id: str | None = None
|
||||
method: str | None = None # code / exact / embedding / fuzzy / manual
|
||||
confidence: float = 0.0
|
||||
# Лучший кандидат для очереди верификации, даже если ниже порога автосопоставления.
|
||||
suggested_service_id: str | None = None
|
||||
suggested_score: float = 0.0
|
||||
|
||||
|
||||
# --- выдача API ---
|
||||
class ServiceOut(BaseModel):
|
||||
service_id: str
|
||||
specialty: str
|
||||
name_ru: str
|
||||
tarificator_code: str | None = None
|
||||
|
||||
|
||||
class PartnerOut(BaseModel):
|
||||
partner_id: str
|
||||
name: str
|
||||
city: str | None = None
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
class PriceOut(BaseModel):
|
||||
partner_id: str
|
||||
partner_name: str
|
||||
price_resident_kzt: float | None = None
|
||||
price_nonresident_kzt: float | None = None
|
||||
prices: PriceTiers = Field(default_factory=dict)
|
||||
effective_date: date | None = None
|
||||
|
||||
|
||||
class Stats(BaseModel):
|
||||
"""Сводка для дашборда и отчёта о качестве."""
|
||||
|
||||
documents_total: int = 0
|
||||
documents_done: int = 0
|
||||
items_total: int = 0
|
||||
auto_matched_pct: float = 0.0
|
||||
unmatched_total: int = 0
|
||||
@@ -0,0 +1,415 @@
|
||||
[
|
||||
{
|
||||
"canon_id": "cbc",
|
||||
"canonical_ru": "Общий анализ крови",
|
||||
"canonical_kz": "Қанның жалпы анализі",
|
||||
"canonical_en": "Complete blood count (CBC)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.003",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "ОАК",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Общий анализ крови (развернутый)",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Клинический анализ крови",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "КАК",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "CBC",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"text": "Қанның жалпы талдауы",
|
||||
"lang": "kz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "uzi_abd",
|
||||
"canonical_ru": "УЗИ органов брюшной полости",
|
||||
"canonical_kz": "Құрсақ қуысы ағзаларының УДЗ",
|
||||
"canonical_en": "Abdominal ultrasound",
|
||||
"category": "Диагностика/УЗИ",
|
||||
"code": "A04.16.001",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "УЗИ ОБП",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "УЗИ брюшной полости",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Ультразвуковое исследование брюшной полости",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Құрсақ қуысы УДЗ",
|
||||
"lang": "kz"
|
||||
},
|
||||
{
|
||||
"text": "Abdominal US",
|
||||
"lang": "en"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "therapist",
|
||||
"canonical_ru": "Приём врача-терапевта",
|
||||
"canonical_kz": "Терапевт дәрігердің қабылдауы",
|
||||
"canonical_en": "General practitioner consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.047.001",
|
||||
"unit": "1 приём",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "Прием терапевта",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Консультация терапевта первичная",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Терапевт қабылдауы",
|
||||
"lang": "kz"
|
||||
},
|
||||
{
|
||||
"text": "GP visit",
|
||||
"lang": "en"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "ecg",
|
||||
"canonical_ru": "Электрокардиография (ЭКГ)",
|
||||
"canonical_kz": "Электрокардиография (ЭКГ)",
|
||||
"canonical_en": "Electrocardiography (ECG)",
|
||||
"category": "Диагностика",
|
||||
"code": "A05.10.006",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "ЭКГ",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "ЭКГ с расшифровкой",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Электрокардиография",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "ECG",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"text": "ЭКГ + описание",
|
||||
"lang": "ru"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "glucose",
|
||||
"canonical_ru": "Глюкоза крови",
|
||||
"canonical_kz": "Қандағы глюкоза",
|
||||
"canonical_en": "Blood glucose",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.006",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "Глюкоза",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Сахар крови",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Қандағы глюкоза",
|
||||
"lang": "kz"
|
||||
},
|
||||
{
|
||||
"text": "Glucose",
|
||||
"lang": "en"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "cardiologist",
|
||||
"canonical_ru": "Консультация кардиолога",
|
||||
"canonical_kz": "Кардиолог кеңесі",
|
||||
"canonical_en": "Cardiologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.015.001",
|
||||
"unit": "1 приём",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "Прием кардиолога",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Кардиолог консультация первичная",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Кардиолог қабылдауы",
|
||||
"lang": "kz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "mri_brain",
|
||||
"canonical_ru": "МРТ головного мозга",
|
||||
"canonical_kz": "Бас миының МРТ",
|
||||
"canonical_en": "Brain MRI",
|
||||
"category": "Диагностика/МРТ",
|
||||
"code": "A05.23.009",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "МРТ г/м",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Магнитно-резонансная томография головного мозга",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Бас миы МРТ",
|
||||
"lang": "kz"
|
||||
},
|
||||
{
|
||||
"text": "Brain MRI",
|
||||
"lang": "en"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "biochem",
|
||||
"canonical_ru": "Биохимический анализ крови",
|
||||
"canonical_kz": "Қанның биохимиялық анализі",
|
||||
"canonical_en": "Blood chemistry panel",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.004",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "Биохимия крови",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Б/х крови",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Биохимический анализ крови (базовый)",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Қан биохимиясы",
|
||||
"lang": "kz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "tooth_ext",
|
||||
"canonical_ru": "Удаление зуба",
|
||||
"canonical_kz": "Тісті жұлу",
|
||||
"canonical_en": "Tooth extraction",
|
||||
"category": "Стоматология",
|
||||
"code": "A16.07.001",
|
||||
"unit": "1 зуб",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "Удаление зуба простое",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Экстракция зуба",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Тіс жұлу",
|
||||
"lang": "kz"
|
||||
},
|
||||
{
|
||||
"text": "Tooth removal",
|
||||
"lang": "en"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "xray_chest",
|
||||
"canonical_ru": "Рентген грудной клетки",
|
||||
"canonical_kz": "Кеуде клеткасының рентгені",
|
||||
"canonical_en": "Chest X-ray",
|
||||
"category": "Диагностика",
|
||||
"code": "A06.09.007",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "Рентгенография ОГК",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Флюорография",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Кеуде рентгені",
|
||||
"lang": "kz"
|
||||
},
|
||||
{
|
||||
"text": "Chest X-ray",
|
||||
"lang": "en"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "tsh",
|
||||
"canonical_ru": "Тиреотропный гормон (ТТГ)",
|
||||
"canonical_kz": "Тиреотропты гормон (ТТГ)",
|
||||
"canonical_en": "Thyroid-stimulating hormone (TSH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.021",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "ТТГ",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Тиреотропный гормон",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "TSH",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"text": "ТТГ гормоны",
|
||||
"lang": "kz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "urinalysis",
|
||||
"canonical_ru": "Общий анализ мочи",
|
||||
"canonical_kz": "Зәрдің жалпы анализі",
|
||||
"canonical_en": "Urinalysis",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.010",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "ОАМ",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Общий анализ мочи",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Зәр анализі",
|
||||
"lang": "kz"
|
||||
},
|
||||
{
|
||||
"text": "Urine test",
|
||||
"lang": "en"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "gyn",
|
||||
"canonical_ru": "Приём гинеколога",
|
||||
"canonical_kz": "Гинеколог қабылдауы",
|
||||
"canonical_en": "Gynecologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.001.001",
|
||||
"unit": "1 приём",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "Прием гинеколога",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Консультация гинеколога первичная",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Гинеколог кеңесі",
|
||||
"lang": "kz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "covid_pcr",
|
||||
"canonical_ru": "ПЦР на COVID-19",
|
||||
"canonical_kz": "COVID-19 ПТР",
|
||||
"canonical_en": "COVID-19 PCR test",
|
||||
"category": "Лаборатория",
|
||||
"code": "A26.08.027",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "ПЦР COVID-19",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "ПЦР-тест на коронавирус",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "COVID PCR",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"text": "COVID ПТР",
|
||||
"lang": "kz"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"canon_id": "vit_d",
|
||||
"canonical_ru": "Витамин D (25-OH)",
|
||||
"canonical_kz": "D дәрумені (25-OH)",
|
||||
"canonical_en": "Vitamin D (25-OH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.115",
|
||||
"unit": "1 иссл.",
|
||||
"synonyms": [
|
||||
{
|
||||
"text": "25-OH витамин D",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Витамин Д",
|
||||
"lang": "ru"
|
||||
},
|
||||
{
|
||||
"text": "Vitamin D",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"text": "D витамині",
|
||||
"lang": "kz"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
-- Схема MedArchive. Четыре сущности из ТЗ §3, адаптированные под реальные данные:
|
||||
-- многотарифные цены (резидент/нерезидент/СНГ/…) хранятся в prices (jsonb),
|
||||
-- а два главных тарифа продублированы отдельными колонками под ТЗ и API.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS vector; -- pgvector для семантического сопоставления
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- триграммы для нечёткого поиска
|
||||
|
||||
-- Эталонный справочник услуг (1281 позиция, загружается из xlsx).
|
||||
CREATE TABLE service (
|
||||
service_id text PRIMARY KEY, -- «<ID>-<Code>» из справочника
|
||||
specialty text NOT NULL DEFAULT '', -- специальность = категория + сужение поиска
|
||||
name_ru text NOT NULL,
|
||||
name_norm text NOT NULL, -- нормализованная форма для точного совпадения
|
||||
tarificator_code text, -- код тарификатора, если задан
|
||||
synonyms jsonb NOT NULL DEFAULT '[]'::jsonb, -- наращиваем при ручной верификации
|
||||
embedding vector(384), -- эмбеддинг name_ru (multilingual)
|
||||
is_active boolean NOT NULL DEFAULT true
|
||||
);
|
||||
CREATE INDEX service_tarificator_idx ON service (tarificator_code) WHERE tarificator_code IS NOT NULL;
|
||||
CREATE INDEX service_name_trgm_idx ON service USING gin (name_ru gin_trgm_ops);
|
||||
CREATE INDEX service_embedding_idx ON service USING hnsw (embedding vector_cosine_ops);
|
||||
|
||||
-- Клиника-партнёр.
|
||||
CREATE TABLE partner (
|
||||
partner_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
city text,
|
||||
address text,
|
||||
bin varchar(12), -- БИН организации, для дедупликации
|
||||
contact_email text,
|
||||
contact_phone text,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE UNIQUE INDEX partner_bin_idx ON partner (bin) WHERE bin IS NOT NULL;
|
||||
|
||||
-- Исходный прайс-документ (один файл = прайс одной клиники на дату).
|
||||
CREATE TABLE price_document (
|
||||
doc_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
partner_id uuid NOT NULL REFERENCES partner(partner_id),
|
||||
file_name text NOT NULL,
|
||||
file_format text NOT NULL, -- pdf / scan_pdf / docx / xlsx / xls
|
||||
effective_date date, -- дата вступления в силу (год из имени файла)
|
||||
parsed_at timestamptz,
|
||||
parse_status text NOT NULL DEFAULT 'pending', -- pending/processing/done/error/needs_review
|
||||
parse_log text,
|
||||
raw_content text -- сырой извлечённый текст для аудита
|
||||
);
|
||||
|
||||
-- Позиция прайса (услуга + цены). Это рабочая лошадка системы.
|
||||
CREATE TABLE price_item (
|
||||
item_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
doc_id uuid NOT NULL REFERENCES price_document(doc_id),
|
||||
partner_id uuid NOT NULL REFERENCES partner(partner_id), -- денормализация ради скорости
|
||||
service_name_raw text NOT NULL, -- название как в документе
|
||||
service_code_source text, -- код из источника (внутренний или тарификатора)
|
||||
service_id text REFERENCES service(service_id), -- нормализованная услуга (nullable)
|
||||
prices jsonb NOT NULL DEFAULT '{}'::jsonb, -- {resident, nonresident, cis, far, insurance, partner}
|
||||
price_resident_kzt numeric, -- главный тариф, продублирован под ТЗ/API
|
||||
price_nonresident_kzt numeric,
|
||||
currency_original text NOT NULL DEFAULT 'KZT',
|
||||
unit text,
|
||||
effective_date date,
|
||||
map_method text, -- code/exact/embedding/fuzzy/manual
|
||||
map_confidence numeric,
|
||||
is_verified boolean NOT NULL DEFAULT false,
|
||||
verification_note text,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX price_item_service_idx ON price_item (service_id);
|
||||
CREATE INDEX price_item_partner_idx ON price_item (partner_id);
|
||||
CREATE INDEX price_item_unmatched_idx ON price_item (service_id) WHERE service_id IS NULL;
|
||||
CREATE INDEX price_item_raw_trgm_idx ON price_item USING gin (service_name_raw gin_trgm_ops);
|
||||
-- Активная позиция уникальна по клинике, названию и дате прайса (версионирование — через is_active).
|
||||
CREATE UNIQUE INDEX price_item_active_idx
|
||||
ON price_item (partner_id, service_name_raw, effective_date) WHERE is_active;
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Загрузчик целевого справочника медицинских услуг.
|
||||
|
||||
Справочник (`Справочник услуг.xlsx`) — это эталон, к которому нормализуются все
|
||||
извлечённые из прайсов позиции. Колонки исходного файла:
|
||||
|
||||
ID | Специальность | Code | Name_ru | TarificatrCode
|
||||
|
||||
Синонимов в справочнике нет, поэтому автоматическое сопоставление опирается на
|
||||
код тарификатора (точное совпадение) и на эмбеддинги названий. Этот модуль
|
||||
отвечает только за загрузку и подготовку индексов; само сопоставление — в
|
||||
`etl/normalize`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
# Код тарификатора в формате РК: буква + три группы цифр, например «A02.004.000».
|
||||
TARIFICATOR_RE = re.compile(r"^[A-ZА-Я]\d{2}\.\d{3}\.\d{3}$")
|
||||
|
||||
_WHITESPACE_RE = re.compile(r"\s+")
|
||||
|
||||
|
||||
def normalize_name(text: str | None) -> str:
|
||||
"""Привести название услуги к форме для сравнения.
|
||||
|
||||
Нижний регистр, унификация «ё»→«е», дефисы и кратные пробелы схлопываются в
|
||||
один пробел. Исходное написание при этом нигде не теряется — нормализованная
|
||||
форма используется только как ключ сопоставления.
|
||||
"""
|
||||
s = (text or "").strip().lower().replace("ё", "е")
|
||||
s = s.replace("-", " ")
|
||||
return _WHITESPACE_RE.sub(" ", s).strip()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Service:
|
||||
"""Одна запись эталонного справочника услуг."""
|
||||
|
||||
service_id: str # стабильный идентификатор «<ID>-<Code>»
|
||||
specialty: str # специальность — служит и категорией, и сужением поиска
|
||||
name_ru: str # официальное название
|
||||
tarificator_code: str | None # код тарификатора (если задан)
|
||||
name_norm: str = field(default="") # нормализованная форма name_ru
|
||||
|
||||
|
||||
def load_dictionary(path: str | Path) -> list[Service]:
|
||||
"""Загрузить справочник из xlsx и вернуть список услуг с нормализованными именами.
|
||||
|
||||
Заголовки ищутся по именам колонок, а не по позиции, — так загрузчик переживёт
|
||||
перестановку столбцов. Строки с пустым названием пропускаются.
|
||||
"""
|
||||
workbook = load_workbook(path, read_only=True, data_only=True)
|
||||
sheet = workbook.active
|
||||
rows = sheet.iter_rows(values_only=True)
|
||||
|
||||
header = [str(c).strip() if c is not None else "" for c in next(rows)]
|
||||
col = {name: header.index(name) for name in header if name}
|
||||
name_idx = col.get("Name_ru")
|
||||
if name_idx is None:
|
||||
raise ValueError(f"В справочнике нет колонки Name_ru. Заголовки: {header}")
|
||||
|
||||
services: list[Service] = []
|
||||
seen_ids: dict[str, int] = {}
|
||||
for row in rows:
|
||||
raw_name = row[name_idx]
|
||||
if not raw_name or not str(raw_name).strip():
|
||||
continue
|
||||
|
||||
tar_raw = row[col["TarificatrCode"]] if "TarificatrCode" in col else None
|
||||
tar = str(tar_raw).strip() if tar_raw else None
|
||||
if tar and not TARIFICATOR_RE.match(tar):
|
||||
tar = None # отбрасываем мусорные коды, оставляем только валидный формат
|
||||
|
||||
# «<ID>-<Code>» иногда повторяется в справочнике — гарантируем уникальность.
|
||||
sid = f"{row[col['ID']]}-{row[col['Code']]}"
|
||||
repeat = seen_ids.get(sid, 0)
|
||||
seen_ids[sid] = repeat + 1
|
||||
if repeat:
|
||||
sid = f"{sid}#{repeat}"
|
||||
|
||||
service = Service(
|
||||
service_id=sid,
|
||||
specialty=str(row[col["Специальность"]]).strip() if row[col["Специальность"]] else "",
|
||||
name_ru=str(raw_name).strip(),
|
||||
tarificator_code=tar,
|
||||
)
|
||||
service.name_norm = normalize_name(service.name_ru)
|
||||
services.append(service)
|
||||
|
||||
return services
|
||||
@@ -0,0 +1,25 @@
|
||||
# Fixtures — синтетические прайсы клиник
|
||||
|
||||
Сгенерированы скриптом `scripts/gen_fixtures.py` (seed=42, детерминированно) для отладки pipeline **до** выдачи реальных данных от организатора. Намеренно разнородные и «грязные».
|
||||
|
||||
## `raw/` — входные прайсы (6 форматов)
|
||||
|
||||
| Файл | Формат | Особенности (что тестирует) |
|
||||
|---|---|---|
|
||||
| `clinic_alpha_clean.xlsx` | Excel | Чистая таблица — быстрый путь pandas, без LLM |
|
||||
| `clinic_beta_dirty.xlsx` | Excel | Секции-заголовки, «от 12000», KZ-названия, лишние колонки |
|
||||
| `clinic_gamma.csv` | CSV (`;`) | Смешанные RU/KZ, единицы измерения |
|
||||
| `clinic_delta_messy.txt` | Текст | Прайс абзацем, разные разделители и валюты (тг/тенге/₸/KZT) |
|
||||
| `clinic_epsilon_scan.png` | Картинка | Имитация фото/скана (наклон) → путь LLM-vision / OCR |
|
||||
| `clinic_zeta_prices.pdf` | PDF | Текстовый PDF, RU/EN заголовок |
|
||||
|
||||
Одни и те же услуги названы по-разному между файлами (ОАК / Общий анализ крови / Қанның жалпы анализі) — это материал для канонизации и дедупликации.
|
||||
|
||||
## `normalized/golden.json` — эталон нормализации (52 строки)
|
||||
|
||||
Оракул для TDD ETL: для каждой строки из `raw/` указан `canon_id`, `canonical_ru/kz/en`, `category`, `code`, `price`, `currency`, `unit`, `source_file`. Сгенерирован тем же скриптом, что и raw → гарантированно согласован.
|
||||
|
||||
Справочник канонов: `../contracts/service_canon.json` (15 услуг, синонимы с lang-тегами).
|
||||
|
||||
## TODO (Фаза 0 хакатона)
|
||||
- `api/*.json` — эталонные ответы API (для MSW-моков фронта)
|
||||
@@ -0,0 +1,782 @@
|
||||
[
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "Тиреотропный гормон (ТТГ)",
|
||||
"lang": "ru",
|
||||
"canon_id": "tsh",
|
||||
"canonical_ru": "Тиреотропный гормон (ТТГ)",
|
||||
"canonical_kz": "Тиреотропты гормон (ТТГ)",
|
||||
"canonical_en": "Thyroid-stimulating hormone (TSH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.021",
|
||||
"price": 3600,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "Abdominal US",
|
||||
"lang": "en",
|
||||
"canon_id": "uzi_abd",
|
||||
"canonical_ru": "УЗИ органов брюшной полости",
|
||||
"canonical_kz": "Құрсақ қуысы ағзаларының УДЗ",
|
||||
"canonical_en": "Abdominal ultrasound",
|
||||
"category": "Диагностика/УЗИ",
|
||||
"code": "A04.16.001",
|
||||
"price": 12900,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "Общий анализ крови",
|
||||
"lang": "ru",
|
||||
"canon_id": "cbc",
|
||||
"canonical_ru": "Общий анализ крови",
|
||||
"canonical_kz": "Қанның жалпы анализі",
|
||||
"canonical_en": "Complete blood count (CBC)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.003",
|
||||
"price": 3900,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "Общий анализ мочи",
|
||||
"lang": "ru",
|
||||
"canon_id": "urinalysis",
|
||||
"canonical_ru": "Общий анализ мочи",
|
||||
"canonical_kz": "Зәрдің жалпы анализі",
|
||||
"canonical_en": "Urinalysis",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.010",
|
||||
"price": 900,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "Глюкоза крови",
|
||||
"lang": "ru",
|
||||
"canon_id": "glucose",
|
||||
"canonical_ru": "Глюкоза крови",
|
||||
"canonical_kz": "Қандағы глюкоза",
|
||||
"canonical_en": "Blood glucose",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.006",
|
||||
"price": 1100,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "ЭКГ",
|
||||
"lang": "ru",
|
||||
"canon_id": "ecg",
|
||||
"canonical_ru": "Электрокардиография (ЭКГ)",
|
||||
"canonical_kz": "Электрокардиография (ЭКГ)",
|
||||
"canonical_en": "Electrocardiography (ECG)",
|
||||
"category": "Диагностика",
|
||||
"code": "A05.10.006",
|
||||
"price": 3600,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "Chest X-ray",
|
||||
"lang": "en",
|
||||
"canon_id": "xray_chest",
|
||||
"canonical_ru": "Рентген грудной клетки",
|
||||
"canonical_kz": "Кеуде клеткасының рентгені",
|
||||
"canonical_en": "Chest X-ray",
|
||||
"category": "Диагностика",
|
||||
"code": "A06.09.007",
|
||||
"price": 2600,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "GP visit",
|
||||
"lang": "en",
|
||||
"canon_id": "therapist",
|
||||
"canonical_ru": "Приём врача-терапевта",
|
||||
"canonical_kz": "Терапевт дәрігердің қабылдауы",
|
||||
"canonical_en": "General practitioner consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.047.001",
|
||||
"price": 5200,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_alpha_clean.xlsx",
|
||||
"clinic": "Клиника «Аль-Шифа»",
|
||||
"raw_name": "Кардиолог қабылдауы",
|
||||
"lang": "kz",
|
||||
"canon_id": "cardiologist",
|
||||
"canonical_ru": "Консультация кардиолога",
|
||||
"canonical_kz": "Кардиолог кеңесі",
|
||||
"canonical_en": "Cardiologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.015.001",
|
||||
"price": 7800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Биохимия крови",
|
||||
"lang": "ru",
|
||||
"canon_id": "biochem",
|
||||
"canonical_ru": "Биохимический анализ крови",
|
||||
"canonical_kz": "Қанның биохимиялық анализі",
|
||||
"canonical_en": "Blood chemistry panel",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.004",
|
||||
"price": 8300,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Флюорография",
|
||||
"lang": "ru",
|
||||
"canon_id": "xray_chest",
|
||||
"canonical_ru": "Рентген грудной клетки",
|
||||
"canonical_kz": "Кеуде клеткасының рентгені",
|
||||
"canonical_en": "Chest X-ray",
|
||||
"category": "Диагностика",
|
||||
"code": "A06.09.007",
|
||||
"price": 3100,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Глюкоза крови",
|
||||
"lang": "ru",
|
||||
"canon_id": "glucose",
|
||||
"canonical_ru": "Глюкоза крови",
|
||||
"canonical_kz": "Қандағы глюкоза",
|
||||
"canonical_en": "Blood glucose",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.006",
|
||||
"price": 1400,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Общий анализ крови",
|
||||
"lang": "ru",
|
||||
"canon_id": "cbc",
|
||||
"canonical_ru": "Общий анализ крови",
|
||||
"canonical_kz": "Қанның жалпы анализі",
|
||||
"canonical_en": "Complete blood count (CBC)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.003",
|
||||
"price": 3400,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Консультация терапевта первичная",
|
||||
"lang": "ru",
|
||||
"canon_id": "therapist",
|
||||
"canonical_ru": "Приём врача-терапевта",
|
||||
"canonical_kz": "Терапевт дәрігердің қабылдауы",
|
||||
"canonical_en": "General practitioner consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.047.001",
|
||||
"price": 7800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Магнитно-резонансная томография головного мозга",
|
||||
"lang": "ru",
|
||||
"canon_id": "mri_brain",
|
||||
"canonical_ru": "МРТ головного мозга",
|
||||
"canonical_kz": "Бас миының МРТ",
|
||||
"canonical_en": "Brain MRI",
|
||||
"category": "Диагностика/МРТ",
|
||||
"code": "A05.23.009",
|
||||
"price": 19100,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Кардиолог қабылдауы",
|
||||
"lang": "kz",
|
||||
"canon_id": "cardiologist",
|
||||
"canonical_ru": "Консультация кардиолога",
|
||||
"canonical_kz": "Кардиолог кеңесі",
|
||||
"canonical_en": "Cardiologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.015.001",
|
||||
"price": 11800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Приём гинеколога",
|
||||
"lang": "ru",
|
||||
"canon_id": "gyn",
|
||||
"canonical_ru": "Приём гинеколога",
|
||||
"canonical_kz": "Гинеколог қабылдауы",
|
||||
"canonical_en": "Gynecologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.001.001",
|
||||
"price": 10900,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_beta_dirty.xlsx",
|
||||
"clinic": "Медцентр «Сункар»",
|
||||
"raw_name": "Ультразвуковое исследование брюшной полости",
|
||||
"lang": "ru",
|
||||
"canon_id": "uzi_abd",
|
||||
"canonical_ru": "УЗИ органов брюшной полости",
|
||||
"canonical_kz": "Құрсақ қуысы ағзаларының УДЗ",
|
||||
"canonical_en": "Abdominal ultrasound",
|
||||
"category": "Диагностика/УЗИ",
|
||||
"code": "A04.16.001",
|
||||
"price": 7000,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_gamma.csv",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "УЗИ ОБП",
|
||||
"lang": "ru",
|
||||
"canon_id": "uzi_abd",
|
||||
"canonical_ru": "УЗИ органов брюшной полости",
|
||||
"canonical_kz": "Құрсақ қуысы ағзаларының УДЗ",
|
||||
"canonical_en": "Abdominal ultrasound",
|
||||
"category": "Диагностика/УЗИ",
|
||||
"code": "A04.16.001",
|
||||
"price": 10700,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_gamma.csv",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "ПЦР-тест на коронавирус",
|
||||
"lang": "ru",
|
||||
"canon_id": "covid_pcr",
|
||||
"canonical_ru": "ПЦР на COVID-19",
|
||||
"canonical_kz": "COVID-19 ПТР",
|
||||
"canonical_en": "COVID-19 PCR test",
|
||||
"category": "Лаборатория",
|
||||
"code": "A26.08.027",
|
||||
"price": 9600,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_gamma.csv",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "ЭКГ + описание",
|
||||
"lang": "ru",
|
||||
"canon_id": "ecg",
|
||||
"canonical_ru": "Электрокардиография (ЭКГ)",
|
||||
"canonical_kz": "Электрокардиография (ЭКГ)",
|
||||
"canonical_en": "Electrocardiography (ECG)",
|
||||
"category": "Диагностика",
|
||||
"code": "A05.10.006",
|
||||
"price": 2800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_gamma.csv",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Витамин D (25-OH)",
|
||||
"lang": "ru",
|
||||
"canon_id": "vit_d",
|
||||
"canonical_ru": "Витамин D (25-OH)",
|
||||
"canonical_kz": "D дәрумені (25-OH)",
|
||||
"canonical_en": "Vitamin D (25-OH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.115",
|
||||
"price": 9800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_gamma.csv",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "МРТ г/м",
|
||||
"lang": "ru",
|
||||
"canon_id": "mri_brain",
|
||||
"canonical_ru": "МРТ головного мозга",
|
||||
"canonical_kz": "Бас миының МРТ",
|
||||
"canonical_en": "Brain MRI",
|
||||
"category": "Диагностика/МРТ",
|
||||
"code": "A05.23.009",
|
||||
"price": 31600,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_gamma.csv",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Глюкоза",
|
||||
"lang": "ru",
|
||||
"canon_id": "glucose",
|
||||
"canonical_ru": "Глюкоза крови",
|
||||
"canonical_kz": "Қандағы глюкоза",
|
||||
"canonical_en": "Blood glucose",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.006",
|
||||
"price": 1000,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_gamma.csv",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Биохимический анализ крови (базовый)",
|
||||
"lang": "ru",
|
||||
"canon_id": "biochem",
|
||||
"canonical_ru": "Биохимический анализ крови",
|
||||
"canonical_kz": "Қанның биохимиялық анализі",
|
||||
"canonical_en": "Blood chemistry panel",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.004",
|
||||
"price": 5900,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_gamma.csv",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Кардиолог консультация первичная",
|
||||
"lang": "ru",
|
||||
"canon_id": "cardiologist",
|
||||
"canonical_ru": "Консультация кардиолога",
|
||||
"canonical_kz": "Кардиолог кеңесі",
|
||||
"canonical_en": "Cardiologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.015.001",
|
||||
"price": 7800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_delta_messy.txt",
|
||||
"clinic": "Медцентр «Тенгри»",
|
||||
"raw_name": "TSH",
|
||||
"lang": "en",
|
||||
"canon_id": "tsh",
|
||||
"canonical_ru": "Тиреотропный гормон (ТТГ)",
|
||||
"canonical_kz": "Тиреотропты гормон (ТТГ)",
|
||||
"canonical_en": "Thyroid-stimulating hormone (TSH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.021",
|
||||
"price": 2300,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_delta_messy.txt",
|
||||
"clinic": "Медцентр «Тенгри»",
|
||||
"raw_name": "Консультация кардиолога",
|
||||
"lang": "ru",
|
||||
"canon_id": "cardiologist",
|
||||
"canonical_ru": "Консультация кардиолога",
|
||||
"canonical_kz": "Кардиолог кеңесі",
|
||||
"canonical_en": "Cardiologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.015.001",
|
||||
"price": 7700,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_delta_messy.txt",
|
||||
"clinic": "Медцентр «Тенгри»",
|
||||
"raw_name": "Консультация гинеколога первичная",
|
||||
"lang": "ru",
|
||||
"canon_id": "gyn",
|
||||
"canonical_ru": "Приём гинеколога",
|
||||
"canonical_kz": "Гинеколог қабылдауы",
|
||||
"canonical_en": "Gynecologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.001.001",
|
||||
"price": 6300,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_delta_messy.txt",
|
||||
"clinic": "Медцентр «Тенгри»",
|
||||
"raw_name": "CBC",
|
||||
"lang": "en",
|
||||
"canon_id": "cbc",
|
||||
"canonical_ru": "Общий анализ крови",
|
||||
"canonical_kz": "Қанның жалпы анализі",
|
||||
"canonical_en": "Complete blood count (CBC)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.003",
|
||||
"price": 4300,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_delta_messy.txt",
|
||||
"clinic": "Медцентр «Тенгри»",
|
||||
"raw_name": "Электрокардиография",
|
||||
"lang": "ru",
|
||||
"canon_id": "ecg",
|
||||
"canonical_ru": "Электрокардиография (ЭКГ)",
|
||||
"canonical_kz": "Электрокардиография (ЭКГ)",
|
||||
"canonical_en": "Electrocardiography (ECG)",
|
||||
"category": "Диагностика",
|
||||
"code": "A05.10.006",
|
||||
"price": 4800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_delta_messy.txt",
|
||||
"clinic": "Медцентр «Тенгри»",
|
||||
"raw_name": "Зәр анализі",
|
||||
"lang": "kz",
|
||||
"canon_id": "urinalysis",
|
||||
"canonical_ru": "Общий анализ мочи",
|
||||
"canonical_kz": "Зәрдің жалпы анализі",
|
||||
"canonical_en": "Urinalysis",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.010",
|
||||
"price": 1300,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_delta_messy.txt",
|
||||
"clinic": "Медцентр «Тенгри»",
|
||||
"raw_name": "ПЦР-тест на коронавирус",
|
||||
"lang": "ru",
|
||||
"canon_id": "covid_pcr",
|
||||
"canonical_ru": "ПЦР на COVID-19",
|
||||
"canonical_kz": "COVID-19 ПТР",
|
||||
"canonical_en": "COVID-19 PCR test",
|
||||
"category": "Лаборатория",
|
||||
"code": "A26.08.027",
|
||||
"price": 8700,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "Рентген грудной клетки",
|
||||
"lang": "ru",
|
||||
"canon_id": "xray_chest",
|
||||
"canonical_ru": "Рентген грудной клетки",
|
||||
"canonical_kz": "Кеуде клеткасының рентгені",
|
||||
"canonical_en": "Chest X-ray",
|
||||
"category": "Диагностика",
|
||||
"code": "A06.09.007",
|
||||
"price": 3200,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "Abdominal US",
|
||||
"lang": "en",
|
||||
"canon_id": "uzi_abd",
|
||||
"canonical_ru": "УЗИ органов брюшной полости",
|
||||
"canonical_kz": "Құрсақ қуысы ағзаларының УДЗ",
|
||||
"canonical_en": "Abdominal ultrasound",
|
||||
"category": "Диагностика/УЗИ",
|
||||
"code": "A04.16.001",
|
||||
"price": 12800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "Магнитно-резонансная томография головного мозга",
|
||||
"lang": "ru",
|
||||
"canon_id": "mri_brain",
|
||||
"canonical_ru": "МРТ головного мозга",
|
||||
"canonical_kz": "Бас миының МРТ",
|
||||
"canonical_en": "Brain MRI",
|
||||
"category": "Диагностика/МРТ",
|
||||
"code": "A05.23.009",
|
||||
"price": 34400,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "Консультация гинеколога первичная",
|
||||
"lang": "ru",
|
||||
"canon_id": "gyn",
|
||||
"canonical_ru": "Приём гинеколога",
|
||||
"canonical_kz": "Гинеколог қабылдауы",
|
||||
"canonical_en": "Gynecologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.001.001",
|
||||
"price": 5700,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "Витамин Д",
|
||||
"lang": "ru",
|
||||
"canon_id": "vit_d",
|
||||
"canonical_ru": "Витамин D (25-OH)",
|
||||
"canonical_kz": "D дәрумені (25-OH)",
|
||||
"canonical_en": "Vitamin D (25-OH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.115",
|
||||
"price": 8700,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "Биохимия крови",
|
||||
"lang": "ru",
|
||||
"canon_id": "biochem",
|
||||
"canonical_ru": "Биохимический анализ крови",
|
||||
"canonical_kz": "Қанның биохимиялық анализі",
|
||||
"canonical_en": "Blood chemistry panel",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.004",
|
||||
"price": 6400,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "Удаление зуба",
|
||||
"lang": "ru",
|
||||
"canon_id": "tooth_ext",
|
||||
"canonical_ru": "Удаление зуба",
|
||||
"canonical_kz": "Тісті жұлу",
|
||||
"canonical_en": "Tooth extraction",
|
||||
"category": "Стоматология",
|
||||
"code": "A16.07.001",
|
||||
"price": 9300,
|
||||
"currency": "KZT",
|
||||
"unit": "1 зуб"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "Glucose",
|
||||
"lang": "en",
|
||||
"canon_id": "glucose",
|
||||
"canonical_ru": "Глюкоза крови",
|
||||
"canonical_kz": "Қандағы глюкоза",
|
||||
"canonical_en": "Blood glucose",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.006",
|
||||
"price": 2000,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_epsilon_scan.png",
|
||||
"clinic": "Клиника «Береке»",
|
||||
"raw_name": "ТТГ",
|
||||
"lang": "ru",
|
||||
"canon_id": "tsh",
|
||||
"canonical_ru": "Тиреотропный гормон (ТТГ)",
|
||||
"canonical_kz": "Тиреотропты гормон (ТТГ)",
|
||||
"canonical_en": "Thyroid-stimulating hormone (TSH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.021",
|
||||
"price": 3100,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "D витамині",
|
||||
"lang": "kz",
|
||||
"canon_id": "vit_d",
|
||||
"canonical_ru": "Витамин D (25-OH)",
|
||||
"canonical_kz": "D дәрумені (25-OH)",
|
||||
"canonical_en": "Vitamin D (25-OH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.115",
|
||||
"price": 10900,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Құрсақ қуысы УДЗ",
|
||||
"lang": "kz",
|
||||
"canon_id": "uzi_abd",
|
||||
"canonical_ru": "УЗИ органов брюшной полости",
|
||||
"canonical_kz": "Құрсақ қуысы ағзаларының УДЗ",
|
||||
"canonical_en": "Abdominal ultrasound",
|
||||
"category": "Диагностика/УЗИ",
|
||||
"code": "A04.16.001",
|
||||
"price": 6000,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "ТТГ гормоны",
|
||||
"lang": "kz",
|
||||
"canon_id": "tsh",
|
||||
"canonical_ru": "Тиреотропный гормон (ТТГ)",
|
||||
"canonical_kz": "Тиреотропты гормон (ТТГ)",
|
||||
"canonical_en": "Thyroid-stimulating hormone (TSH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.021",
|
||||
"price": 2500,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Қандағы глюкоза",
|
||||
"lang": "kz",
|
||||
"canon_id": "glucose",
|
||||
"canonical_ru": "Глюкоза крови",
|
||||
"canonical_kz": "Қандағы глюкоза",
|
||||
"canonical_en": "Blood glucose",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.006",
|
||||
"price": 800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Приём гинеколога",
|
||||
"lang": "ru",
|
||||
"canon_id": "gyn",
|
||||
"canonical_ru": "Приём гинеколога",
|
||||
"canonical_kz": "Гинеколог қабылдауы",
|
||||
"canonical_en": "Gynecologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.001.001",
|
||||
"price": 10900,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Экстракция зуба",
|
||||
"lang": "ru",
|
||||
"canon_id": "tooth_ext",
|
||||
"canonical_ru": "Удаление зуба",
|
||||
"canonical_kz": "Тісті жұлу",
|
||||
"canonical_en": "Tooth extraction",
|
||||
"category": "Стоматология",
|
||||
"code": "A16.07.001",
|
||||
"price": 9900,
|
||||
"currency": "KZT",
|
||||
"unit": "1 зуб"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "ЭКГ",
|
||||
"lang": "ru",
|
||||
"canon_id": "ecg",
|
||||
"canonical_ru": "Электрокардиография (ЭКГ)",
|
||||
"canonical_kz": "Электрокардиография (ЭКГ)",
|
||||
"canonical_en": "Electrocardiography (ECG)",
|
||||
"category": "Диагностика",
|
||||
"code": "A05.10.006",
|
||||
"price": 2100,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Прием терапевта",
|
||||
"lang": "ru",
|
||||
"canon_id": "therapist",
|
||||
"canonical_ru": "Приём врача-терапевта",
|
||||
"canonical_kz": "Терапевт дәрігердің қабылдауы",
|
||||
"canonical_en": "General practitioner consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.047.001",
|
||||
"price": 7600,
|
||||
"currency": "KZT",
|
||||
"unit": "1 приём"
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "Биохимический анализ крови",
|
||||
"lang": "ru",
|
||||
"canon_id": "biochem",
|
||||
"canonical_ru": "Биохимический анализ крови",
|
||||
"canonical_kz": "Қанның биохимиялық анализі",
|
||||
"canonical_en": "Blood chemistry panel",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.004",
|
||||
"price": 4000,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
},
|
||||
{
|
||||
"source_file": "clinic_zeta_prices.pdf",
|
||||
"clinic": "Diagnostic Lab Astana",
|
||||
"raw_name": "COVID PCR",
|
||||
"lang": "en",
|
||||
"canon_id": "covid_pcr",
|
||||
"canonical_ru": "ПЦР на COVID-19",
|
||||
"canonical_kz": "COVID-19 ПТР",
|
||||
"canonical_en": "COVID-19 PCR test",
|
||||
"category": "Лаборатория",
|
||||
"code": "A26.08.027",
|
||||
"price": 7800,
|
||||
"currency": "KZT",
|
||||
"unit": "1 иссл."
|
||||
}
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
МЕДЦЕНТР «ТЕНГРИ» — действующие цены
|
||||
|
||||
TSH ... 2300 ₸ (1 иссл.)
|
||||
Консультация кардиолога 7700 KZT (1 приём)
|
||||
Консультация гинеколога первичная: 6300 тенге (1 приём)
|
||||
CBC ... 4300 KZT (1 иссл.)
|
||||
Электрокардиография — 4800 тг (1 иссл.)
|
||||
Зәр анализі — 1300 тенге (1 иссл.)
|
||||
ПЦР-тест на коронавирус ... 8700 KZT (1 иссл.)
|
||||
|
||||
* Цены справочные, уточняйте в регистратуре. Бағалар анықтамалық.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,9 @@
|
||||
service;price_kzt;unit
|
||||
УЗИ ОБП;10700;1 иссл.
|
||||
ПЦР-тест на коронавирус;9600;1 иссл.
|
||||
ЭКГ + описание;2800;1 иссл.
|
||||
Витамин D (25-OH);9800;1 иссл.
|
||||
МРТ г/м;31600;1 иссл.
|
||||
Глюкоза;1000;1 иссл.
|
||||
Биохимический анализ крови (базовый);5900;1 иссл.
|
||||
Кардиолог консультация первичная;7800;1 приём
|
||||
|
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[project]
|
||||
name = "medarchive"
|
||||
version = "0.1.0"
|
||||
description = "Автоматическая обработка архива прайсов клиник-партнёров и база услуг с ценами"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi>=0.115",
|
||||
"uvicorn[standard]>=0.32",
|
||||
"pydantic>=2.9",
|
||||
"psycopg[binary]>=3.2",
|
||||
"python-multipart>=0.0.12", # загрузка ZIP-архива
|
||||
"openpyxl>=3.1", # xlsx
|
||||
"xlrd>=2.0", # старый xls
|
||||
"python-docx>=1.1", # docx (+ принятие правок)
|
||||
"pdfplumber>=0.11", # PDF с текстовым слоем
|
||||
"pymupdf>=1.24", # рендер страниц для Vision
|
||||
"rapidfuzz>=3.10", # нечёткое сопоставление
|
||||
"numpy>=2.0", # векторные операции при сопоставлении
|
||||
"google-genai>=1.0", # Gemini Vision + эмбеддинги (API, без локального torch)
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B"] # стиль, ошибки, импорты, апгрейды, баги
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
# Вызовы-маркеры FastAPI в значениях по умолчанию — идиома фреймворка, не баг B008.
|
||||
extend-immutable-calls = ["fastapi.File", "fastapi.Query", "fastapi.Depends"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
# Разведочные скрипты этапа разбора форматов — черновики, не часть продукта.
|
||||
"scripts/proof/*" = ["E402", "E501", "E741", "B007"]
|
||||
"scripts/gen_fixtures.py" = ["E402", "E501", "E741"]
|
||||
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Генератор синтетических прайсов клиник (RU/KZ/EN) + эталонная нормализация + справочник канонов.
|
||||
|
||||
Выдаёт:
|
||||
fixtures/raw/* — 6 форматов «сырых» прайсов (Excel/CSV/txt/PNG/PDF)
|
||||
fixtures/normalized/golden.json — эталон: каждая строка → канон (оракул для TDD ETL)
|
||||
contracts/service_canon.json — справочник канонических услуг (seed для нормализации)
|
||||
Детерминированно (seed=42).
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||
RAW = os.path.join(ROOT, "fixtures/raw")
|
||||
NORM = os.path.join(ROOT, "fixtures/normalized")
|
||||
CONTRACTS = os.path.join(ROOT, "contracts")
|
||||
for d in (RAW, NORM, CONTRACTS):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
random.seed(42)
|
||||
|
||||
|
||||
def ensure(pkg, mod=None):
|
||||
try:
|
||||
__import__(mod or pkg)
|
||||
except ImportError:
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", pkg])
|
||||
|
||||
|
||||
# Справочник канонических медуслуг. lang-тегированные синонимы — материал для нормализации.
|
||||
CANONS = [
|
||||
{
|
||||
"id": "cbc",
|
||||
"ru": "Общий анализ крови",
|
||||
"kz": "Қанның жалпы анализі",
|
||||
"en": "Complete blood count (CBC)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.003",
|
||||
"unit": "1 иссл.",
|
||||
"price": (1200, 4500),
|
||||
"syn": [
|
||||
("ОАК", "ru"),
|
||||
("Общий анализ крови (развернутый)", "ru"),
|
||||
("Клинический анализ крови", "ru"),
|
||||
("КАК", "ru"),
|
||||
("CBC", "en"),
|
||||
("Қанның жалпы талдауы", "kz"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "uzi_abd",
|
||||
"ru": "УЗИ органов брюшной полости",
|
||||
"kz": "Құрсақ қуысы ағзаларының УДЗ",
|
||||
"en": "Abdominal ultrasound",
|
||||
"category": "Диагностика/УЗИ",
|
||||
"code": "A04.16.001",
|
||||
"unit": "1 иссл.",
|
||||
"price": (6000, 13000),
|
||||
"syn": [
|
||||
("УЗИ ОБП", "ru"),
|
||||
("УЗИ брюшной полости", "ru"),
|
||||
("Ультразвуковое исследование брюшной полости", "ru"),
|
||||
("Құрсақ қуысы УДЗ", "kz"),
|
||||
("Abdominal US", "en"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "therapist",
|
||||
"ru": "Приём врача-терапевта",
|
||||
"kz": "Терапевт дәрігердің қабылдауы",
|
||||
"en": "General practitioner consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.047.001",
|
||||
"unit": "1 приём",
|
||||
"price": (4000, 9000),
|
||||
"syn": [
|
||||
("Прием терапевта", "ru"),
|
||||
("Консультация терапевта первичная", "ru"),
|
||||
("Терапевт қабылдауы", "kz"),
|
||||
("GP visit", "en"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "ecg",
|
||||
"ru": "Электрокардиография (ЭКГ)",
|
||||
"kz": "Электрокардиография (ЭКГ)",
|
||||
"en": "Electrocardiography (ECG)",
|
||||
"category": "Диагностика",
|
||||
"code": "A05.10.006",
|
||||
"unit": "1 иссл.",
|
||||
"price": (2000, 5000),
|
||||
"syn": [
|
||||
("ЭКГ", "ru"),
|
||||
("ЭКГ с расшифровкой", "ru"),
|
||||
("Электрокардиография", "ru"),
|
||||
("ECG", "en"),
|
||||
("ЭКГ + описание", "ru"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "glucose",
|
||||
"ru": "Глюкоза крови",
|
||||
"kz": "Қандағы глюкоза",
|
||||
"en": "Blood glucose",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.006",
|
||||
"unit": "1 иссл.",
|
||||
"price": (800, 2200),
|
||||
"syn": [
|
||||
("Глюкоза", "ru"),
|
||||
("Сахар крови", "ru"),
|
||||
("Қандағы глюкоза", "kz"),
|
||||
("Glucose", "en"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "cardiologist",
|
||||
"ru": "Консультация кардиолога",
|
||||
"kz": "Кардиолог кеңесі",
|
||||
"en": "Cardiologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.015.001",
|
||||
"unit": "1 приём",
|
||||
"price": (5000, 12000),
|
||||
"syn": [
|
||||
("Прием кардиолога", "ru"),
|
||||
("Кардиолог консультация первичная", "ru"),
|
||||
("Кардиолог қабылдауы", "kz"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "mri_brain",
|
||||
"ru": "МРТ головного мозга",
|
||||
"kz": "Бас миының МРТ",
|
||||
"en": "Brain MRI",
|
||||
"category": "Диагностика/МРТ",
|
||||
"code": "A05.23.009",
|
||||
"unit": "1 иссл.",
|
||||
"price": (18000, 35000),
|
||||
"syn": [
|
||||
("МРТ г/м", "ru"),
|
||||
("Магнитно-резонансная томография головного мозга", "ru"),
|
||||
("Бас миы МРТ", "kz"),
|
||||
("Brain MRI", "en"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "biochem",
|
||||
"ru": "Биохимический анализ крови",
|
||||
"kz": "Қанның биохимиялық анализі",
|
||||
"en": "Blood chemistry panel",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.004",
|
||||
"unit": "1 иссл.",
|
||||
"price": (3500, 9000),
|
||||
"syn": [
|
||||
("Биохимия крови", "ru"),
|
||||
("Б/х крови", "ru"),
|
||||
("Биохимический анализ крови (базовый)", "ru"),
|
||||
("Қан биохимиясы", "kz"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tooth_ext",
|
||||
"ru": "Удаление зуба",
|
||||
"kz": "Тісті жұлу",
|
||||
"en": "Tooth extraction",
|
||||
"category": "Стоматология",
|
||||
"code": "A16.07.001",
|
||||
"unit": "1 зуб",
|
||||
"price": (6000, 15000),
|
||||
"syn": [
|
||||
("Удаление зуба простое", "ru"),
|
||||
("Экстракция зуба", "ru"),
|
||||
("Тіс жұлу", "kz"),
|
||||
("Tooth removal", "en"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "xray_chest",
|
||||
"ru": "Рентген грудной клетки",
|
||||
"kz": "Кеуде клеткасының рентгені",
|
||||
"en": "Chest X-ray",
|
||||
"category": "Диагностика",
|
||||
"code": "A06.09.007",
|
||||
"unit": "1 иссл.",
|
||||
"price": (2500, 6000),
|
||||
"syn": [
|
||||
("Рентгенография ОГК", "ru"),
|
||||
("Флюорография", "ru"),
|
||||
("Кеуде рентгені", "kz"),
|
||||
("Chest X-ray", "en"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tsh",
|
||||
"ru": "Тиреотропный гормон (ТТГ)",
|
||||
"kz": "Тиреотропты гормон (ТТГ)",
|
||||
"en": "Thyroid-stimulating hormone (TSH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.021",
|
||||
"unit": "1 иссл.",
|
||||
"price": (1500, 4000),
|
||||
"syn": [("ТТГ", "ru"), ("Тиреотропный гормон", "ru"), ("TSH", "en"), ("ТТГ гормоны", "kz")],
|
||||
},
|
||||
{
|
||||
"id": "urinalysis",
|
||||
"ru": "Общий анализ мочи",
|
||||
"kz": "Зәрдің жалпы анализі",
|
||||
"en": "Urinalysis",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.010",
|
||||
"unit": "1 иссл.",
|
||||
"price": (900, 2500),
|
||||
"syn": [
|
||||
("ОАМ", "ru"),
|
||||
("Общий анализ мочи", "ru"),
|
||||
("Зәр анализі", "kz"),
|
||||
("Urine test", "en"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "gyn",
|
||||
"ru": "Приём гинеколога",
|
||||
"kz": "Гинеколог қабылдауы",
|
||||
"en": "Gynecologist consultation",
|
||||
"category": "Приём специалистов",
|
||||
"code": "B01.001.001",
|
||||
"unit": "1 приём",
|
||||
"price": (5000, 11000),
|
||||
"syn": [
|
||||
("Прием гинеколога", "ru"),
|
||||
("Консультация гинеколога первичная", "ru"),
|
||||
("Гинеколог кеңесі", "kz"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "covid_pcr",
|
||||
"ru": "ПЦР на COVID-19",
|
||||
"kz": "COVID-19 ПТР",
|
||||
"en": "COVID-19 PCR test",
|
||||
"category": "Лаборатория",
|
||||
"code": "A26.08.027",
|
||||
"unit": "1 иссл.",
|
||||
"price": (7000, 16000),
|
||||
"syn": [
|
||||
("ПЦР COVID-19", "ru"),
|
||||
("ПЦР-тест на коронавирус", "ru"),
|
||||
("COVID PCR", "en"),
|
||||
("COVID ПТР", "kz"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "vit_d",
|
||||
"ru": "Витамин D (25-OH)",
|
||||
"kz": "D дәрумені (25-OH)",
|
||||
"en": "Vitamin D (25-OH)",
|
||||
"category": "Лаборатория",
|
||||
"code": "B03.016.115",
|
||||
"unit": "1 иссл.",
|
||||
"price": (6000, 12000),
|
||||
"syn": [
|
||||
("25-OH витамин D", "ru"),
|
||||
("Витамин Д", "ru"),
|
||||
("Vitamin D", "en"),
|
||||
("D витамині", "kz"),
|
||||
],
|
||||
},
|
||||
]
|
||||
CLINICS = [
|
||||
"Клиника «Аль-Шифа»",
|
||||
"Медцентр «Сункар»",
|
||||
"Diagnostic Lab Astana",
|
||||
"Клиника «Береке»",
|
||||
"Медцентр «Тенгри»",
|
||||
]
|
||||
|
||||
|
||||
def price(c):
|
||||
return random.randint(c["price"][0] // 100, c["price"][1] // 100) * 100
|
||||
|
||||
|
||||
def variant(c):
|
||||
return random.choice([(c["ru"], "ru")] + c["syn"])
|
||||
|
||||
|
||||
GOLDEN = []
|
||||
|
||||
|
||||
def build(file_name, clinic, n):
|
||||
rows = []
|
||||
for c in random.sample(CANONS, k=min(n, len(CANONS))):
|
||||
raw_name, lang = variant(c)
|
||||
p = price(c)
|
||||
rows.append({"raw_name": raw_name, "price": p, "unit": c["unit"]})
|
||||
GOLDEN.append(
|
||||
{
|
||||
"source_file": file_name,
|
||||
"clinic": clinic,
|
||||
"raw_name": raw_name,
|
||||
"lang": lang,
|
||||
"canon_id": c["id"],
|
||||
"canonical_ru": c["ru"],
|
||||
"canonical_kz": c["kz"],
|
||||
"canonical_en": c["en"],
|
||||
"category": c["category"],
|
||||
"code": c["code"],
|
||||
"price": p,
|
||||
"currency": "KZT",
|
||||
"unit": c["unit"],
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
ensure("openpyxl")
|
||||
from openpyxl import Workbook
|
||||
|
||||
# 1) чистый Excel
|
||||
rows = build("clinic_alpha_clean.xlsx", CLINICS[0], 9)
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Прайс"
|
||||
ws.append(["Наименование услуги", "Цена, тг"])
|
||||
for r in rows:
|
||||
ws.append([r["raw_name"], r["price"]])
|
||||
wb.save(f"{RAW}/clinic_alpha_clean.xlsx")
|
||||
|
||||
# 2) грязный Excel
|
||||
rows = build("clinic_beta_dirty.xlsx", CLINICS[1], 9)
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "PRICE 2026"
|
||||
ws.append(["Прайс-лист медцентра «Сункар» (с 01.06.2026)"])
|
||||
ws.append([])
|
||||
ws.append(["№", "Услуга / Қызмет", "Стоимость (тенге)", "Ед."])
|
||||
secs = ["— Лаборатория / Зертхана —", "— Диагностика —", "— Приём специалистов —"]
|
||||
i = 1
|
||||
for k, r in enumerate(rows):
|
||||
if k % 3 == 0 and secs:
|
||||
ws.append([secs.pop(0), None, None, None])
|
||||
val = f"от {r['price']}" if random.random() < 0.3 else r["price"]
|
||||
ws.append([i, r["raw_name"], val, r["unit"]])
|
||||
i += 1
|
||||
wb.save(f"{RAW}/clinic_beta_dirty.xlsx")
|
||||
|
||||
# 3) CSV ;
|
||||
rows = build("clinic_gamma.csv", CLINICS[2], 8)
|
||||
with open(f"{RAW}/clinic_gamma.csv", "w", encoding="utf-8", newline="") as f:
|
||||
w = csv.writer(f, delimiter=";")
|
||||
w.writerow(["service", "price_kzt", "unit"])
|
||||
for r in rows:
|
||||
w.writerow([r["raw_name"], r["price"], r["unit"]])
|
||||
|
||||
# 4) грязный текст
|
||||
rows = build("clinic_delta_messy.txt", CLINICS[4], 7)
|
||||
with open(f"{RAW}/clinic_delta_messy.txt", "w", encoding="utf-8") as f:
|
||||
f.write("МЕДЦЕНТР «ТЕНГРИ» — действующие цены\n\n")
|
||||
for r in rows:
|
||||
sep = random.choice([" — ", " ... ", ": ", " "])
|
||||
cur = random.choice(["тг", "тенге", "₸", "KZT"])
|
||||
f.write(f"{r['raw_name']}{sep}{r['price']} {cur} ({r['unit']})\n")
|
||||
f.write("\n* Цены справочные, уточняйте в регистратуре. Бағалар анықтамалық.\n")
|
||||
|
||||
# 5) PNG-скан
|
||||
rows = build("clinic_epsilon_scan.png", CLINICS[3], 9)
|
||||
ensure("Pillow", "PIL")
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
def font(sz):
|
||||
for p in [
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/Library/Fonts/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
]:
|
||||
if os.path.exists(p):
|
||||
return ImageFont.truetype(p, sz)
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
img = Image.new("RGB", (900, 720), (250, 248, 244))
|
||||
d = ImageDraw.Draw(img)
|
||||
d.text((40, 30), "Клиника «Береке» — прайс-лист", fill=(20, 20, 20), font=font(34))
|
||||
d.line((40, 85, 860, 85), fill=(180, 180, 180), width=2)
|
||||
y = 110
|
||||
for r in rows:
|
||||
d.text((50, y), r["raw_name"][:42], fill=(30, 30, 30), font=font(23))
|
||||
d.text((720, y), f"{r['price']} тг", fill=(30, 30, 30), font=font(23))
|
||||
y += 62
|
||||
img = img.rotate(-1.4, expand=False, fillcolor=(250, 248, 244))
|
||||
img.save(f"{RAW}/clinic_epsilon_scan.png")
|
||||
|
||||
# 6) PDF
|
||||
rows = build("clinic_zeta_prices.pdf", CLINICS[2], 10)
|
||||
pdf = "skip"
|
||||
try:
|
||||
ensure("reportlab")
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
fp = next(
|
||||
(
|
||||
p
|
||||
for p in [
|
||||
"/Library/Fonts/Arial.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
]
|
||||
if os.path.exists(p)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if fp:
|
||||
pdfmetrics.registerFont(TTFont("U", fp))
|
||||
c = canvas.Canvas(f"{RAW}/clinic_zeta_prices.pdf", pagesize=A4)
|
||||
c.setFont("U", 18)
|
||||
c.drawString(60, 790, "Diagnostic Lab Astana — Price list / Прайс")
|
||||
c.setFont("U", 12)
|
||||
y = 750
|
||||
for r in rows:
|
||||
c.drawString(60, y, r["raw_name"][:55])
|
||||
c.drawRightString(540, y, f"{r['price']} KZT")
|
||||
y -= 28
|
||||
c.save()
|
||||
pdf = "ok"
|
||||
except Exception as e:
|
||||
pdf = f"skip ({e})"
|
||||
|
||||
# эталон нормализации + справочник
|
||||
with open(f"{NORM}/golden.json", "w", encoding="utf-8") as f:
|
||||
json.dump(GOLDEN, f, ensure_ascii=False, indent=2)
|
||||
canon_out = [
|
||||
{
|
||||
"canon_id": c["id"],
|
||||
"canonical_ru": c["ru"],
|
||||
"canonical_kz": c["kz"],
|
||||
"canonical_en": c["en"],
|
||||
"category": c["category"],
|
||||
"code": c["code"],
|
||||
"unit": c["unit"],
|
||||
"synonyms": [{"text": t, "lang": l} for t, l in c["syn"]],
|
||||
}
|
||||
for c in CANONS
|
||||
]
|
||||
with open(f"{CONTRACTS}/service_canon.json", "w", encoding="utf-8") as f:
|
||||
json.dump(canon_out, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("PDF:", pdf)
|
||||
print("raw:", sorted(os.listdir(RAW)))
|
||||
print("golden rows:", len(GOLDEN), "| canons:", len(CANONS))
|
||||
Reference in New Issue
Block a user