Files

458 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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))