Files
EDSS-calc/certainty.py
2026-02-12 13:39:36 +01:00

601 lines
22 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.
# %% API call1
#import time
#import json
#import os
#from datetime import datetime
#import pandas as pd
#from openai import OpenAI
#from dotenv import load_dotenv
#
## Load environment variables
#load_dotenv()
#
## === CONFIGURATION ===
#OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
#OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL")
#MODEL_NAME = "GPT-OSS-120B"
#HEALTH_URL = f"{OPENAI_BASE_URL}/health" # Placeholder - actual health check would need to be implemented
#CHAT_URL = f"{OPENAI_BASE_URL}/chat/completions"
#
## File paths
#INPUT_CSV = "/home/shahin/Lab/Doktorarbeit/Barcelona/Data/MS_Briefe_400_with_unique_id_SHA3_explore_cleaned_unique.csv"
#EDSS_INSTRUCTIONS_PATH = "/home/shahin/Lab/Doktorarbeit/Barcelona/attach/Komplett.txt"
##GRAMMAR_FILE = "/home/shahin/Lab/Doktorarbeit/Barcelona/attach/just_edss_schema.gbnf"
#
## Initialize OpenAI client
#client = OpenAI(
# api_key=OPENAI_API_KEY,
# base_url=OPENAI_BASE_URL
#)
#
## Read EDSS instructions from file
#with open(EDSS_INSTRUCTIONS_PATH, 'r') as f:
# EDSS_INSTRUCTIONS = f.read().strip()
## === RUN INFERENCE 2 ===
#def run_inference(patient_text):
# prompt = f'''
# Du bist ein medizinischer Assistent, der spezialisiert darauf ist, EDSS-Scores (Expanded Disability Status Scale) aus klinischen Berichten zu extrahieren.
#### Regeln für die Ausgabe:
#1. **Reason**: Erstelle eine prägnante Zusammenfassung (max. 400 Zeichen) der Befunde auf **DEUTSCH**, die zur Einstufung führen.
#2. **klassifizierbar**:
# - Setze dies auf **true**, wenn ein EDSS-Wert identifiziert, berechnet oder basierend auf den klinischen Hinweisen plausibel geschätzt werden kann.
# - Setze dies auf **false**, NUR wenn die Daten absolut unzureichend oder so widersprüchlich sind, dass keinerlei Einstufung möglich ist.
#3. **EDSS**:
# - Dieses Feld ist **VERPFLICHTEND**, wenn "klassifizierbar" auf true steht.
# - Es muss eine Zahl zwischen 0.0 und 10.0 sein.
# - Versuche stets, den EDSS-Wert so präzise wie möglich zu bestimmen, auch wenn die Datenlage dünn ist (nutze verfügbare Informationen zu Gehstrecke und Funktionssystemen).
# - Dieses Feld **DARF NICHT ERSCHEINEN**, wenn "klassifizierbar" auf false steht.
#
#### Einschränkungen:
#- Erfinde keine Fakten, aber nutze klinische Herleitungen aus dem Bericht, um den EDSS zu bestimmen.
#- Priorisiere die Vergabe eines EDSS-Wertes gegenüber der Markierung als nicht klassifizierbar.
#- Halte dich strikt an die JSON-Struktur.
#
#EDSS-Bewertungsrichtlinien:
#{EDSS_INSTRUCTIONS}
#
#Patientenbericht:
#{patient_text}
#'''
# start_time = time.time()
#
# try:
# # Make API call using OpenAI client
# response = client.chat.completions.create(
# messages=[
# {
# "role": "system",
# "content": "You extract EDSS scores. You prioritize providing a score even if data is partial, by using clinical inference."
# },
# {
# "role": "user",
# "content": prompt
# }
# ],
# model=MODEL_NAME,
# max_tokens=2048,
# temperature=0.0,
# response_format={"type": "json_object"}
# )
#
# # Extract content from response
# content = response.choices[0].message.content
#
# # Parse the JSON response
# parsed = json.loads(content)
#
# inference_time = time.time() - start_time
#
# return {
# "success": True,
# "result": parsed,
# "inference_time_sec": inference_time
# }
#
# except Exception as e:
# print(f"Inference error: {e}")
# return {
# "success": False,
# "error": str(e),
# "inference_time_sec": -1
# }
## === BUILD PATIENT TEXT ===
#def build_patient_text(row):
# return (
# str(row["T_Zusammenfassung"]) + "\n" +
# str(row["Diagnosen"]) + "\n" +
# str(row["T_KlinBef"]) + "\n" +
# str(row["T_Befunde"]) + "\n"
# )
#
#if __name__ == "__main__":
# # Read CSV file ONLY inside main block
# df = pd.read_csv(INPUT_CSV, sep=';')
# results = []
#
# # Process each row
# for idx, row in df.iterrows():
# print(f"Processing row {idx + 1}/{len(df)}")
# try:
# patient_text = build_patient_text(row)
# result = run_inference(patient_text)
#
# # Add unique_id and MedDatum to result for tracking
# result["unique_id"] = row.get("unique_id", f"row_{idx}")
# result["MedDatum"] = row.get("MedDatum", None)
#
# results.append(result)
# print(json.dumps(result, indent=2))
# except Exception as e:
# print(f"Error processing row {idx}: {e}")
# results.append({
# "success": False,
# "error": str(e),
# "unique_id": row.get("unique_id", f"row_{idx}"),
# "MedDatum": row.get("MedDatum", None)
# })
#
# # Save results to a JSON file
# output_json = INPUT_CSV.replace(".csv", "_results_Nisch.json")
# with open(output_json, 'w') as f:
# json.dump(results, f, indent=2)
# print(f"Results saved to {output_json}")
##
# %% API call1 - Enhanced with certainty scoring
#import time
#import json
#import os
#from datetime import datetime
#import pandas as pd
#from openai import OpenAI
#from dotenv import load_dotenv
#
## Load environment variables
#load_dotenv()
#
## === CONFIGURATION ===
#OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
#OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL")
#MODEL_NAME = "GPT-OSS-120B"
#
## File paths
#INPUT_CSV = "/home/shahin/Lab/Doktorarbeit/Barcelona/Data/Test.csv"
#EDSS_INSTRUCTIONS_PATH = "/home/shahin/Lab/Doktorarbeit/Barcelona/attach/Komplett.txt"
#
## Initialize OpenAI client
#client = OpenAI(
# api_key=OPENAI_API_KEY,
# base_url=OPENAI_BASE_URL
#)
#
## Read EDSS instructions from file
#with open(EDSS_INSTRUCTIONS_PATH, 'r') as f:
# EDSS_INSTRUCTIONS = f.read().strip()
#
## === PROMPT WITH CERTAINTY REQUEST ===
#def build_prompt(patient_text):
# return f'''Du bist ein medizinischer Assistent, der spezialisiert darauf ist, EDSS-Scores (Expanded Disability Status Scale), alle Unterkategorien und die Bewertungssicherheit aus klinischen Berichten zu extrahieren.
#
#### Deine Aufgabe:
#1. Analysiere den Patientenbericht und extrahiere:
# - Den Gesamt-EDSS-Score (0.010.0)
# - Alle 8 EDSS-Unterkategorien (mit jeweils eigener Maximalpunktzahl)
#2. Schätze für jede Entscheidung die Sicherheit als Ganzzahl von 0100 % ein.
#
#### Struktur der JSON-Ausgabe (VERPFLICHTEND):
#Gib NUR gültiges JSON zurück — kein Markdown, kein Text davor/dahinter.
#
#{{
# "reason": "Kernaussage zur EDSS-Begründung (max. 400 Zeichen, auf Deutsch).",
# "klassifizierbar": true/false,
# "EDSS": null ODER Zahl zwischen 0.0 und 10.0 (nur wenn klassifizierbar=true)",
# "certainty_percent": 0 ODER Zahl zwischen 0 und 100 (Ganzzahl)",
# "subcategories": {{
# "VISUAL_OPTIC_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
# "BRAINSTEM_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
# "PYRAMIDAL_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
# "CEREBELLAR_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
# "SENSORY_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
# "BOWEL_AND_BLADDER_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
# "CEREBRAL_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
# "AMBULATION": null ODER Zahl zwischen 0.0 und 10.0
# }}
#}}
#
#### Regeln:
#- **reason**: Kurze, prägnante Begründung (auf Deutsch, max. 400 Zeichen), warum du den EDSS-Wert und die Unterkategorien so bewertest.
#- **klassifizierbar**:
# - `true`, wenn EDSS und mindestens die wichtigsten Unterkategorien *eindeutig ableitbar* oder *plausibel inferierbar* sind.
# - `false`, **nur**, wenn keine relevanten Daten vorliegen, oder diese so widersprüchlich/inkonsistent sind, dass keine vernünftige Einschätzung möglich ist.
#- **EDSS**:
# - **VERPFLICHTEND**, wenn `klassifizierbar=true`.
# - Zahl zwischen 0.0 und 10.0 (z.B. 3.0, 5.5). Darf **nicht** erscheinen, wenn `klassifizierbar=false`.
#- **certainty_percent**:
# - **Immer present** — Ganzzahl (0100), basierend auf:
# - Klarheit und Vollständigkeit der Berichtsangaben,
# - Stichhaltigkeit der Schlussfolgerung (inkl. Inferenz),
# - Konsistenz zwischen den Unterkategorien.
#- **subcategories**:
# - **Immer present** — **alle 8 Unterkategorien** müssen enthalten sein.
# - Jeder Wert ist entweder:
# - `null` (wenn keine ausreichende Information vorliegt), **oder**
# - eine Zahl ≤ jeweiliger Obergrenze (z.B. Ambulation ≤ 10.0).
# - Wenn die Unterkategorie plausibel inferiert werden kann (auch indirekt), gib einen sinnvollen Wert ab.
# - Beispiel: Wenn „Gang mit Krückstock auf ebenem Boden bis 200 m“ steht, setze `AMBULATION: 5.5`.
#
#### EDSS-Bewertungsrichtlinien:
#{EDSS_INSTRUCTIONS}
#
#Patientenbericht:
#{patient_text}
#'''
#
## === INFERENCE FUNCTION ===
#def run_inference(patient_text):
# prompt = build_prompt(patient_text)
#
# start_time = time.time()
#
# try:
# response = client.chat.completions.create(
# messages=[
# {"role": "system", "content": "Du gibst EXKLUSIV gültiges JSON zurück — keine weiteren Erklärungen."}
# ] + [
# {"role": "user", "content": prompt}
# ],
# model=MODEL_NAME,
# max_tokens=2048,
# temperature=0.1, # Slightly higher for more natural certainty estimation (still low for reliability)
# response_format={"type": "json_object"}
# )
#
# content = response.choices[0].message.content
#
# # Parse and validate JSON
# try:
# parsed = json.loads(content)
# except json.JSONDecodeError as e:
# print(f"⚠️ JSON parsing failed: {e}")
# print("Raw response:", content[:500])
# raise ValueError("Model did not return valid JSON")
#
# # Enforce required keys
# if "certainty_percent" not in parsed:
# print("⚠️ Missing 'certainty_percent' in output! Force-adding fallback.")
# parsed["certainty_percent"] = 0 # fallback
# elif not isinstance(parsed["certainty_percent"], (int, float)):
# parsed["certainty_percent"] = int(parsed["certainty_percent"])
#
# # Clamp certainty to [0, 100]
# pct = parsed["certainty_percent"]
# parsed["certainty_percent"] =max(0, min(100, int(pct)))
#
# # Enforce EDSS rules: if not classifiable → remove EDSS
# if not parsed.get("klassifizierbar", False):
# if "EDSS" in parsed:
# del parsed["EDSS"] # per spec, must not appear if not classifiable
# else:
# if "EDSS" not in parsed:
# print("⚠️ 'klassifizierbar' is true but EDSS missing — adding fallback.")
# parsed["EDSS"] = 7.0 # last-resort fallback
#
# inference_time = time.time() - start_time
#
# return {
# "success": True,
# "result": parsed,
# "inference_time_sec": inference_time
# }
#
# except Exception as e:
# print(f"❌ Inference error: {e}")
# return {
# "success": False,
# "error": str(e),
# "inference_time_sec": -1,
# "result": None # no structured output
# }
#
## === BUILD PATIENT TEXT ===
#def build_patient_text(row):
# return (
# str(row.get("T_Zusammenfassung", "")) + "\n" +
# str(row.get("Diagnosen", "")) + "\n" +
# str(row.get("T_KlinBef", "")) + "\n" +
# str(row.get("T_Befunde", ""))
# )
#
#if __name__ == "__main__":
# # Load data
# df = pd.read_csv(INPUT_CSV, sep=';')
# results = []
#
# # Optional: limit for testing
# # df = df.head(3)
#
# print(f"Processing {len(df)} rows...")
# for idx, row in df.iterrows():
# print(f"\n— Row {idx + 1}/{len(df)} —")
# try:
# patient_text = build_patient_text(row)
# result = run_inference(patient_text)
#
# # Attach metadata
# result["unique_id"] = row.get("unique_id", f"row_{idx}")
# result["MedDatum"] = row.get("MedDatum", None)
#
# results.append(result)
#
# # Print summary
# if result["success"]:
# res = result["result"]
# edss = res.get("EDSS", "N/A") if res.get("klassifizierbar") else "N/A"
# print(f"✅ Result → EDSS={edss}, certainty={res.get('certainty_percent', 'N/A')}%")
# print(f" Reason: {res.get('reason', 'N/A')[:100]}…")
# else:
# print(f"❌ Failed: {result.get('error', 'Unknown error')[:100]}")
#
# except Exception as e:
# print(f"⚠️ Error processing row {idx}: {e}")
# results.append({
# "success": False,
# "error": str(e),
# "unique_id": row.get("unique_id", f"row_{idx}"),
# "MedDatum": row.get("MedDatum", None),
# "result": None
# })
#
# # Save results
# output_json = INPUT_CSV.replace(".csv", "_results_Nisch_certainty.json")
# with open(output_json, 'w', encoding='utf-8') as f:
# json.dump(results, f, indent=2, ensure_ascii=False)
# print(f"\n✅ Saved results to: {output_json}")
#
##
# %% API call - Multi-iteration EDSS + certainty extraction
import time
import json
import os
from datetime import datetime
import pandas as pd
from openai import OpenAI
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# === CONFIGURATION ===
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL")
MODEL_NAME = "GPT-OSS-120B"
# File paths
INPUT_CSV = "/home/shahin/Lab/Doktorarbeit/Barcelona/Data/MS_Briefe_400_with_unique_id_SHA3_explore_cleaned_unique.csv"
EDSS_INSTRUCTIONS_PATH = "/home/shahin/Lab/Doktorarbeit/Barcelona/attach/Komplett.txt"
# Iteration settings
NUM_ITERATIONS = 20
STOP_ON_FIRST_ERROR = False # Set to True for debugging
# Initialize OpenAI client
client = OpenAI(
api_key=OPENAI_API_KEY,
base_url=OPENAI_BASE_URL
)
# Read EDSS instructions from file
with open(EDSS_INSTRUCTIONS_PATH, 'r') as f:
EDSS_INSTRUCTIONS = f.read().strip()
# === PROMPT (unchanged from before) ===
def build_prompt(patient_text):
return f'''Du bist ein medizinischer Assistent, der spezialisiert darauf ist, EDSS-Scores (Expanded Disability Status Scale), alle Unterkategorien und die Bewertungssicherheit aus klinischen Berichten zu extrahieren.
### Deine Aufgabe:
1. Analysiere den Patientenbericht und extrahiere:
- Den Gesamt-EDSS-Score (0.010.0)
- Alle 8 EDSS-Unterkategorien (mit jeweils eigener Maximalpunktzahl)
2. Schätze für jede Entscheidung die Sicherheit als Ganzzahl von 0100 % ein.
### Struktur der JSON-Ausgabe (VERPFLICHTEND):
Gib NUR gültiges JSON zurück — kein Markdown, kein Text davor/dahinter.
{{
"reason": "Kernaussage zur EDSS-Begründung (max. 400 Zeichen, auf Deutsch).",
"klassifizierbar": true/false,
"EDSS": null ODER Zahl zwischen 0.0 und 10.0 (nur wenn klassifizierbar=true)",
"certainty_percent": 0 ODER Zahl zwischen 0 und 100 (Ganzzahl)",
"subcategories": {{
"VISUAL_OPTIC_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
"BRAINSTEM_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
"PYRAMIDAL_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
"CEREBELLAR_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
"SENSORY_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
"BOWEL_AND_BLADDER_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
"CEREBRAL_FUNCTIONS": null ODER Zahl zwischen 0.0 und 6.0,
"AMBULATION": null ODER Zahl zwischen 0.0 und 10.0
}}
}}
### Regeln:
- **reason**: Kurze, prägnante Begründung (auf Deutsch, max. 400 Zeichen), warum du den EDSS-Wert und die Unterkategorien so bewertest.
- **klassifizierbar**:
- `true`, wenn EDSS und mindestens die wichtigsten Unterkategorien *eindeutig ableitbar* oder *plausibel inferierbar* sind.
- `false`, **nur**, wenn keine relevanten Daten vorliegen, oder diese so widersprüchlich/inkonsistent sind, dass keine vernünftige Einschätzung möglich ist.
- **EDSS**:
- **VERPFLICHTEND**, wenn `klassifizierbar=true`.
- Zahl zwischen 0.0 und 10.0 (z.B. 3.0, 5.5). Darf **nicht** erscheinen, wenn `klassifizierbar=false`.
- **certainty_percent**:
- **Immer present** — Ganzzahl (0100), basierend auf:
- Klarheit und Vollständigkeit der Berichtsangaben,
- Stichhaltigkeit der Schlussfolgerung (inkl. Inferenz),
- Konsistenz zwischen den Unterkategorien.
- **subcategories**:
- **Immer present** — **alle 8 Unterkategorien** müssen enthalten sein.
- Jeder Wert ist entweder:
- `null` (wenn keine ausreichende Information vorliegt), **oder**
- eine Zahl ≤ jeweiliger Obergrenze (z.B. Ambulation ≤ 10.0).
- Wenn die Unterkategorie plausibel inferiert werden kann (auch indirekt), gib einen sinnvollen Wert ab.
- Beispiel: Wenn „Gang mit Krückstock auf ebenem Boden bis 200 m“ steht, setze `AMBULATION: 5.5`.
### EDSS-Bewertungsrichtlinien:
{EDSS_INSTRUCTIONS}
Patientenbericht:
{patient_text}
'''
# === INFERENCE FUNCTION (unchanged) ===
def run_inference(patient_text):
prompt = build_prompt(patient_text)
start_time = time.time()
try:
response = client.chat.completions.create(
messages=[
{"role": "system", "content": "Du gibst EXKLUSIV gültiges JSON zurück — keine weiteren Erklärungen."}
] + [
{"role": "user", "content": prompt}
],
model=MODEL_NAME,
max_tokens=2048,
temperature=0.1,
response_format={"type": "json_object"}
)
content = response.choices[0].message.content
# Parse and validate JSON
try:
parsed = json.loads(content)
except json.JSONDecodeError as e:
print(f"⚠️ JSON parsing failed: {e}")
print("Raw response:", content[:500])
raise ValueError("Model did not return valid JSON")
# Enforce required keys
if "certainty_percent" not in parsed:
print("⚠️ Missing 'certainty_percent' in output! Force-adding fallback.")
parsed["certainty_percent"] = 0
elif not isinstance(parsed["certainty_percent"], (int, float)):
parsed["certainty_percent"] = int(parsed["certainty_percent"])
# Clamp certainty to [0, 100]
pct = parsed["certainty_percent"]
parsed["certainty_percent"] = max(0, min(100, int(pct)))
# Enforce EDSS rules
if not parsed.get("klassifizierbar", False):
if "EDSS" in parsed:
del parsed["EDSS"]
else:
if "EDSS" not in parsed:
print("⚠️ 'klassifizierbar' is true but EDSS missing — adding fallback.")
parsed["EDSS"] = 7.0
inference_time = time.time() - start_time
return {
"success": True,
"result": parsed,
"inference_time_sec": inference_time
}
except Exception as e:
print(f"❌ Inference error: {e}")
return {
"success": False,
"error": str(e),
"inference_time_sec": -1,
"result": None
}
# === BUILD PATIENT TEXT ===
def build_patient_text(row):
return (
str(row.get("T_Zusammenfassung", "")) + "\n" +
str(row.get("Diagnosen", "")) + "\n" +
str(row.get("T_KlinBef", "")) + "\n" +
str(row.get("T_Befunde", ""))
)
# === MAIN LOOP (NEW: MULTI-ITERATION) ===
if __name__ == "__main__":
# Load data ONCE (to avoid repeated I/O overhead)
df = pd.read_csv(INPUT_CSV, sep=';')
total_rows = len(df)
print(f"Loaded {total_rows} patient records.")
for iteration in range(1, NUM_ITERATIONS + 1):
print(f"\n{'='*60}")
print(f"🔄 ITERATION {iteration}/{NUM_ITERATIONS}")
print(f"{'='*60}")
iteration_results = []
start_iter = time.time()
for idx, row in df.iterrows():
print(f"\rRow {idx+1}/{total_rows} | Iter {iteration}", end='', flush=True)
try:
patient_text = build_patient_text(row)
result = run_inference(patient_text)
# Attach metadata
if result["success"]:
res = result["result"].copy() # avoid mutation
res["iteration"] = iteration
res["unique_id"] = row.get("unique_id", f"row_{idx}")
res["MedDatum"] = row.get("MedDatum", None)
result["result"] = res
else:
result["iteration"] = iteration
result["unique_id"] = row.get("unique_id", f"row_{idx}")
result["MedDatum"] = row.get("MedDatum", None)
iteration_results.append(result)
if result["success"]:
res = result["result"]
edss = res.get("EDSS", "N/A") if res.get("klassifizierbar") else "N/A"
print(f" ✅ EDSS={edss}, cert={res.get('certainty_percent', '?')}%")
else:
print(f"{result.get('error', 'Unknown')}")
except Exception as e:
print(f"\n⚠️ Row {idx} failed: {e}")
iteration_results.append({
"success": False,
"error": str(e),
"iteration": iteration,
"unique_id": row.get("unique_id", f"row_{idx}"),
"MedDatum": row.get("MedDatum", None),
"result": None
})
if STOP_ON_FIRST_ERROR:
break
# Save per-iteration results
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = INPUT_CSV.replace(".csv", f"_results_iter_{iteration}_{timestamp}.json")
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(iteration_results, f, indent=2, ensure_ascii=False)
print(f"\n✅ Iteration {iteration} complete. Saved to: {output_path}")
elapsed = time.time() - start_iter
print(f"⏱️ Iteration {iteration} took {elapsed:.1f}s ({elapsed/total_rows:.1f}s/row)")
print(f"\n🎉 All {NUM_ITERATIONS} iterations completed!")
##