notebook
This commit is contained in:
+176
@@ -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*
|
||||||
+66
-13
@@ -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).
|
# TCP to the Firebird 2.5 server. Use the DB path as seen by the *server* (Windows path).
|
||||||
conn = fb.connect(
|
conn = fb.connect(
|
||||||
host="192.168.1.10",
|
host="localhost",
|
||||||
port=3050,
|
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",
|
user="SYSDBA",
|
||||||
password="masterkey",
|
password="masterkey",
|
||||||
charset="WIN1250", # adjust if needed
|
charset="WIN1250", # adjust if needed
|
||||||
@@ -29,9 +29,6 @@ print(df)
|
|||||||
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
start = datetime(2025, 1, 1)
|
|
||||||
end = datetime(2026, 1, 1)
|
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
SELECT
|
SELECT
|
||||||
/*vh.idvh,*/
|
/*vh.idvh,*/
|
||||||
@@ -43,13 +40,19 @@ SELECT
|
|||||||
/*vh.idhodn,*/
|
/*vh.idhodn,*/
|
||||||
/*vd.poradi,*/
|
/*vd.poradi,*/
|
||||||
/*vd.idmetod,*/
|
/*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, ', ')
|
SELECT LIST(CAST(dd.datose AS VARCHAR(10)) || ' ' || dd.kod, ', ')
|
||||||
FROM dokladd dd
|
FROM dokladd dd
|
||||||
WHERE dd.rodcis = kar.rodcis
|
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.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.datose BETWEEN vh.datum - 7 AND vh.datum + 7
|
||||||
) AS vykodovano,
|
) AS vykodovano,
|
||||||
lm.kodtext,
|
lm.kodtext,
|
||||||
lm.nazev,
|
lm.nazev,
|
||||||
@@ -63,18 +66,68 @@ JOIN kar ON kar.idpac = vh.idpacient
|
|||||||
JOIN labmetod lm ON lm.idmetod = vd.idmetod
|
JOIN labmetod lm ON lm.idmetod = vd.idmetod
|
||||||
JOIN labjedn lj ON lj.idjedn = vd.idjedn
|
JOIN labjedn lj ON lj.idjedn = vd.idjedn
|
||||||
JOIN labskaly ls ON ls.idskaly = vd.idskaly
|
JOIN labskaly ls ON ls.idskaly = vd.idskaly
|
||||||
WHERE vh.datum >= ?
|
WHERE lm.nazev CONTAINING 'PSA'
|
||||||
AND vh.datum < ?
|
|
||||||
AND lm.nazev CONTAINING 'PSA'
|
|
||||||
/*ORDER BY kar.idpac, vh.datum, vd.poradi;*/
|
/*ORDER BY kar.idpac, vh.datum, vd.poradi;*/
|
||||||
ORDER BY vh.datum desc;
|
ORDER BY vh.datum desc;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
df_direct = query_df(sql, (start, end))
|
df_direct = query_df(sql)
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import numpy as np
|
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" ---
|
# --- 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+)?')
|
num_re = re.compile(r'[-+]?\d+(?:[.,]\d+)?(?:[eE][-+]?\d+)?')
|
||||||
|
|
||||||
@@ -120,7 +173,7 @@ from openpyxl.styles import PatternFill
|
|||||||
from openpyxl.formatting.rule import FormulaRule
|
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)
|
base_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# ================= DELETE OLD PSA REPORTS ==================
|
# ================= DELETE OLD PSA REPORTS ==================
|
||||||
|
|||||||
Reference in New Issue
Block a user