From 80c7376ad3a8e1b34156c0742b334f7c1442a5ed Mon Sep 17 00:00:00 2001 From: Vladimir Buzalka Date: Wed, 8 Apr 2026 20:56:26 +0200 Subject: [PATCH] notebook --- PSA/01 PSA.md | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++ PSA/01 PSA.py | 79 ++++++++++++++++++---- 2 files changed, 242 insertions(+), 13 deletions(-) create mode 100644 PSA/01 PSA.md diff --git a/PSA/01 PSA.md b/PSA/01 PSA.md new file mode 100644 index 0000000..e6f3f0a --- /dev/null +++ b/PSA/01 PSA.md @@ -0,0 +1,176 @@ +# 01 PSA.py — Dokumentace + +## Účel + +Skript generuje report výsledků PSA (prostatický specifický antigen) z databáze Medicus (Firebird) a exportuje ho do formátovaného Excel souboru. Výsledky jsou barevně označeny podle poměru naměřené hodnoty k horní hranici normy. Skript zároveň sleduje historii vykázání PSA pojišťovně a počítá, kdy by měl být výkon vykázán příště. + +--- + +## Databázové připojení + +| Parametr | Hodnota | +|------------|--------------------------------------| +| Host | `localhost` | +| Port | `3050` | +| Databáze | `c:\Medicus 3\data\MEDICUS.FDB` | +| Uživatel | `SYSDBA` | +| Heslo | `masterkey` | +| Kódování | `WIN1250` | + +> Jde o **lokální testovací instanci** Medicus. Produkční verze (`Reporter PSA.py`) se připojuje na `192.168.1.10`, databáze `m:\Medicus\data\MEDICUS.FDB`. + +--- + +## Výstup + +Soubor se ukládá do: +``` +u:\Dropbox\!!!Days\Downloads Z230\ +``` +Název souboru: `YYYY-MM-DD HH-MM-SS PSA report.xlsx` + +Před každým spuštěním jsou **automaticky smazány** všechny starší soubory v této složce, jejichž název končí na `PSA report.xlsx`. + +--- + +## SQL dotaz — co se táhne z databáze + +Dotaz vytáhne všechna PSA měření bez datumového omezení, seřazená od nejnovějšího. + +### Tabulky + +| Tabulka | Alias | Popis | +|-------------|-------|------------------------------------| +| `labvh` | `vh` | Hlavičky laboratorních vyšetření | +| `labvd` | `vd` | Výsledky laboratorních vyšetření | +| `kar` | | Kartotéka pacientů | +| `labmetod` | `lm` | Laboratorní metody (název, kód) | +| `labjedn` | `lj` | Jednotky výsledků | +| `labskaly` | `ls` | Referenční rozmezí (normy) | +| `dokladd` | `dd` | Vykázané výkony pojišťovně | + +### Filtr + +```sql +WHERE lm.nazev CONTAINING 'PSA' +``` +Bez datumového omezení — bere všechna PSA z celé databáze. + +--- + +## Sloupce ve výsledném Excel souboru + +| Sloupec | Zdroj / Popis | +|---------------|---------------| +| `IDPACIENT` | ID pacienta z `labvh` | +| `PRIJMENI` | Příjmení z kartotéky | +| `JMENO` | Jméno z kartotéky | +| `RODCIS` | Rodné číslo | +| `DATUM` | Datum odběru PSA | +| `MINULE` | Datum posledního vykázání kódu `01130` **před** tímto PSA + vypočtený termín příštího vykázání dle kódů z té návštěvy (viz logika níže) | +| `VYKODOVANO` | Seznam výkonů vykázaných pojišťovně v okně ±7 dní od data PSA (kódy 01130–01134), formát: `"YYYY-MM-DD kod, ..."` | +| `DALŠÍ` | Vypočtený termín příštího vykázání PSA dle kódů v `VYKODOVANO` (viz logika níže) | +| `KODTEXT` | Kód laboratorní metody | +| `NAZEV` | Název laboratorní metody | +| `VYSL` | Naměřená hodnota (text, např. `"5,6"`, `"<0.1"`) | +| `JEDN` | Jednotka výsledku | +| `NORMDOL` | Dolní hranice normy | +| `NORMHOR` | Horní hranice normy | +| `VYSL_NUM` | Numerická hodnota z `VYSL` (pomocný sloupec pro výpočty) | +| `NORMHOR_NUM` | Numerická hodnota z `NORMHOR` (pomocný sloupec) | +| `RATIO` | `VYSL_NUM / NORMHOR_NUM` — poměr hodnoty k horní normě (základ pro barevné zvýraznění) | + +--- + +## Logika sloupců MINULE a DALŠÍ + +Kódy pojišťovny relevantní pro PSA: + +| Kód | Význam | Interval opakování | +|---------|---------------------------------------------|--------------------| +| `01130` | Základní vykázání PSA | — | +| `01131` | PSA s rozšířeným sledováním (rizikový) | za **4 roky** | +| `01132` | PSA se středním sledováním | za **2 roky** | +| `01133` | PSA — jednorázový výkon (bez opakování) | **NIKDY** | +| `01134` | Ostatní / doplňkový kód | — | + +### Sloupec DALŠÍ + +Vypočítá se z kódů nalezených v `VYKODOVANO` (okno ±7 dní od data PSA): + +``` +01133 → "NIKDY" +01131 → datum PSA + 4 roky +01132 → datum PSA + 2 roky +jinak → prázdné +``` + +### Sloupec MINULE + +1. SQL subquery najde `MAX(datose)` z `dokladd` kde `kod = '01130'` a `datose < datum PSA` — tedy **poslední vykázání PSA před tímto odběrem**. +2. Python pak k tomuto datu dohledá kódy `01131`, `01132`, `01133` vykázané v okně ±7 dní a aplikuje stejnou logiku jako DALŠÍ. + +Výsledný formát: `"2024-03-15, další 2028-03-15"` nebo `"2024-03-15, další NIKDY"` nebo jen `"2024-03-15"` (pokud nebyl nalezen žádný rozhodující kód). + +--- + +## Parsování výsledků (funkce `to_num`) + +Hodnoty PSA přicházejí jako text. Funkce `to_num` extrahuje číslo i z nestandardních zápisů: + +| Vstup | Výstup | Poznámka | +|-------------|------------------|---------------------------------------| +| `"5,6"` | `5.6` | česká desetinná čárka | +| `"<0.1"` | `0.05` | pod mezí detekce → polovina hodnoty | +| `">100"` | `100.0` | nad rozsahem → použije se hodnota | +| `"3.2 ng/mL"` | `3.2` | jednotka se ignoruje | +| `None` / `""` | `NaN` | prázdné nebo chybějící | + +--- + +## Barevné podmíněné formátování (sloupec VYSL) + +Barva se odvíjí od sloupce `RATIO` (`VYSL_NUM / NORMHOR_NUM`): + +| Barva | Podmínka | Význam | +|-------------|------------------|-------------------------| +| 🟢 Zelená | `RATIO ≤ 0.80` | V normě | +| 🟡 Žlutá | `0.80 < RATIO < 1.00` | Hraniční hodnota | +| 🔴 Červená | `RATIO ≥ 1.00` | Nad horní hranicí normy | + +--- + +## Formátování Excel souboru + +- Automatická šířka sloupců (max. 50 znaků) +- Tenké ohraničení všech buněk +- Sloupce A, B, E zarovnány na střed +- Zmražení prvního řádku (záhlaví) +- Autofiltr na celý rozsah dat + +--- + +## Závislosti (Python balíčky) + +``` +firebirdsql +pandas +numpy +openpyxl +``` + +--- + +## Rozdíl oproti `Reporter PSA.py` + +| Vlastnost | `01 PSA.py` (tento) | `Reporter PSA.py` | +|------------------------|--------------------------------------------|------------------------------------------| +| Host / DB | `localhost` / `c:\Medicus 3\data\MEDICUS.FDB` | `192.168.1.10` / `m:\Medicus\data\MEDICUS.FDB` | +| Výstupní složka | `u:\Dropbox\!!!Days\Downloads Z230` | `z:\Dropbox\Ordinace\Reporty` | +| Okno pro VYKODOVANO | ±7 dní | ±7 dní | +| Sloupce MINULE / DALŠÍ | ✅ Ano | ❌ Ne | +| Datumové omezení | Žádné (celá databáze) | Žádné (celá databáze) | + +--- + +*Poslední aktualizace dokumentace: 2026-04-08* diff --git a/PSA/01 PSA.py b/PSA/01 PSA.py index 3fe5f33..978ee45 100644 --- a/PSA/01 PSA.py +++ b/PSA/01 PSA.py @@ -3,9 +3,9 @@ import pandas as pd # TCP to the Firebird 2.5 server. Use the DB path as seen by the *server* (Windows path). conn = fb.connect( - host="192.168.1.10", + host="localhost", port=3050, - database=r"m:\Medicus\data\MEDICUS.FDB", # raw string for backslashes + database=r"c:\Medicus 3\data\MEDICUS.FDB", # local test Medicus user="SYSDBA", password="masterkey", charset="WIN1250", # adjust if needed @@ -29,9 +29,6 @@ print(df) from datetime import datetime -start = datetime(2025, 1, 1) -end = datetime(2026, 1, 1) - sql = """ SELECT /*vh.idvh,*/ @@ -43,13 +40,19 @@ SELECT /*vh.idhodn,*/ /*vd.poradi,*/ /*vd.idmetod,*/ -/* NEW: list of matching dokladd entries within ±7 days, one cell */ + ( + SELECT MAX(dd.datose) + FROM dokladd dd + WHERE dd.rodcis = kar.rodcis + AND dd.kod = '01130' + AND dd.datose < vh.datum + ) AS minule, ( SELECT LIST(CAST(dd.datose AS VARCHAR(10)) || ' ' || dd.kod, ', ') FROM dokladd dd WHERE dd.rodcis = kar.rodcis - AND (dd.kod = '01130' or dd.kod = '01131' OR dd.kod = '01132' OR dd.kod = '01133' OR dd.kod = '01134') - AND dd.datose BETWEEN vh.datum - 365 AND vh.datum + 365 + AND (dd.kod = '01130' OR dd.kod = '01131' OR dd.kod = '01132' OR dd.kod = '01133' OR dd.kod = '01134') + AND dd.datose BETWEEN vh.datum - 7 AND vh.datum + 7 ) AS vykodovano, lm.kodtext, lm.nazev, @@ -63,18 +66,68 @@ JOIN kar ON kar.idpac = vh.idpacient JOIN labmetod lm ON lm.idmetod = vd.idmetod JOIN labjedn lj ON lj.idjedn = vd.idjedn JOIN labskaly ls ON ls.idskaly = vd.idskaly -WHERE vh.datum >= ? - AND vh.datum < ? - AND lm.nazev CONTAINING 'PSA' +WHERE lm.nazev CONTAINING 'PSA' /*ORDER BY kar.idpac, vh.datum, vd.poradi;*/ ORDER BY vh.datum desc; """ -df_direct = query_df(sql, (start, end)) +df_direct = query_df(sql) import re import numpy as np +# --- MINULE: expand with ", další XXXXX" based on codes billed around that date --- +df_dokladd = query_df(""" + SELECT rodcis, datose, kod FROM dokladd + WHERE kod = '01131' OR kod = '01132' OR kod = '01133' +""") +df_dokladd['DATOSE'] = pd.to_datetime(df_dokladd['DATOSE']) + +def compute_minule_str(row): + minule = row['MINULE'] + if minule is None or (isinstance(minule, float) and np.isnan(minule)): + return None + minule_ts = pd.Timestamp(minule) + rodcis = row['RODCIS'] + mask = ( + (df_dokladd['RODCIS'] == rodcis) & + (df_dokladd['DATOSE'] >= minule_ts - pd.Timedelta(days=7)) & + (df_dokladd['DATOSE'] <= minule_ts + pd.Timedelta(days=7)) + ) + codes = df_dokladd.loc[mask, 'KOD'].tolist() + if '01133' in codes: + dalsi_str = 'NIKDY' + elif '01131' in codes: + dalsi_str = (minule_ts + pd.DateOffset(years=4)).strftime('%Y-%m-%d') + elif '01132' in codes: + dalsi_str = (minule_ts + pd.DateOffset(years=2)).strftime('%Y-%m-%d') + else: + dalsi_str = '' + date_str = minule_ts.strftime('%Y-%m-%d') + return f"{date_str}, další {dalsi_str}" if dalsi_str else date_str + +df_direct['MINULE'] = df_direct.apply(compute_minule_str, axis=1) + +# --- DALŠÍ: next PSA billing date based on codes in VYKODOVANO --- +def compute_dalsi(row): + vykod = str(row['VYKODOVANO'] or '') + datum = row['DATUM'] + if '01133' in vykod: + return 'NIKDY' + if '01131' in vykod: + return (pd.Timestamp(datum) + pd.DateOffset(years=4)).strftime('%Y-%m-%d') + if '01132' in vykod: + return (pd.Timestamp(datum) + pd.DateOffset(years=2)).strftime('%Y-%m-%d') + return None + +df_direct['DALŠÍ'] = df_direct.apply(compute_dalsi, axis=1) + +# Reorder: DALŠÍ immediately after VYKODOVANO +cols = list(df_direct.columns) +cols.remove('DALŠÍ') +cols.insert(cols.index('VYKODOVANO') + 1, 'DALŠÍ') +df_direct = df_direct[cols] + # --- 0) Helper: parse numeric value from string like "5,6", "<0.1", "3.2 mmol/L" --- num_re = re.compile(r'[-+]?\d+(?:[.,]\d+)?(?:[eE][-+]?\d+)?') @@ -120,7 +173,7 @@ from openpyxl.styles import PatternFill from openpyxl.formatting.rule import FormulaRule -base_path = Path(r"z:\Dropbox\Ordinace\Reporty") +base_path = Path(r"u:\Dropbox\!!!Days\Downloads Z230") base_path.mkdir(parents=True, exist_ok=True) # ================= DELETE OLD PSA REPORTS ==================