| """ |
| Extended Privacy Detection Tests for SAAP Evaluation |
| Includes: Edge Cases, False Positive Tests, False Negative Tests, Adversarial Cases |
| |
| Purpose: Address professor's feedback about trivial tests |
| """ |
|
|
| import sys |
| import json |
| from pathlib import Path |
| from typing import List, Dict, Tuple |
| from dataclasses import dataclass |
| from datetime import datetime |
|
|
| |
| sys.path.insert(0, str(Path(__file__).parent.parent)) |
|
|
| from services.privacy_detector import PrivacyDetector, PrivacyLevel, detect_privacy_level |
|
|
| @dataclass |
| class TestCase: |
| """Single test case for privacy detection""" |
| id: str |
| category: str |
| input_text: str |
| expected_level: str |
| description: str |
| difficulty: str |
|
|
| def create_extended_test_cases() -> List[TestCase]: |
| """ |
| Create comprehensive test cases including: |
| - Easy cases (baseline) |
| - Medium cases (real-world variations) |
| - Hard cases (edge cases, partial matches) |
| - Adversarial cases (designed to trick the detector) |
| """ |
| |
| test_cases = [] |
| |
| |
| |
| |
| |
| |
| test_cases.extend([ |
| TestCase("TP-E-01", "IBAN", "Meine IBAN ist DE89370400440532013000", "private", "Standard deutsche IBAN", "easy"), |
| TestCase("TP-E-02", "Kreditkarte", "Meine Kreditkartennummer ist 4111111111111111", "private", "Standard Visa-Testformat", "easy"), |
| TestCase("TP-E-03", "Medizin", "Der Patient hat Diabetes Typ 2 diagnostiziert", "private", "Explizite Diagnose", "easy"), |
| TestCase("TP-E-04", "Gehalt", "Mein Gehalt betrΓ€gt 75.000 EUR jΓ€hrlich", "private", "Explizite Gehaltsangabe", "easy"), |
| ]) |
| |
| |
| test_cases.extend([ |
| TestCase("TP-M-01", "IBAN-Text", "Bitte ΓΌberweisen Sie auf DE44500105175407324931 bis Freitag", "private", "IBAN in Satzkontext", "medium"), |
| TestCase("TP-M-02", "Medizin-Verlauf", "Seit der Behandlung mit Metformin sind die Blutzuckerwerte stabil", "private", "Medikament mit Kontext", "medium"), |
| TestCase("TP-M-03", "Kreditkarte-Spaces", "4111 1111 1111 1111 ist meine Kartennummer", "private", "Kreditkarte mit Leerzeichen", "medium"), |
| TestCase("TP-M-04", "Personal-Kombi", "Meine Adresse ist HauptstraΓe 15, 12345 Berlin, geboren am 15.03.1985", "private", "Kombination persΓΆnlicher Daten", "medium"), |
| TestCase("TP-M-05", "SSN-US", "My social security number is 123-45-6789", "private", "US SSN Format", "medium"), |
| TestCase("TP-M-06", "Medizin-Symptom", "Ich habe seit Wochen starke Kopfschmerzen und Γbelkeit, mein Arzt vermutet MigrΓ€ne", "private", "Symptombeschreibung mit Arzt", "medium"), |
| ]) |
| |
| |
| test_cases.extend([ |
| TestCase("TP-H-01", "IBAN-Keine-Prefix", "DE89370400440532013000 - das ist die Kontoverbindung", "private", "IBAN ohne vorherige ErklΓ€rung", "hard"), |
| TestCase("TP-H-02", "Medizin-Indirekt", "Nach meinem Krankenhausaufenthalt wegen der Herzoperation brauche ich Reha", "private", "Indirekter medizinischer Kontext", "hard"), |
| TestCase("TP-H-03", "Multi-Pattern", "Kontakt: +49 176 12345678, email@test.de, geboren 01.01.1990", "confidential", "Mehrere Muster in einem Text", "hard"), |
| TestCase("TP-H-04", "Kontext-Medizin", "Ich bin Patient in der Klinik und nehme tΓ€glich meine Medikamente", "private", "Impliziter medizinischer Status", "hard"), |
| TestCase("TP-H-05", "Gehalts-Indirekt", "Mein monatliches Nettoeinkommen nach Steuern liegt bei etwa 3.500 Euro", "private", "Gehalt ohne direktes Keyword", "hard"), |
| ]) |
| |
| |
| |
| |
| |
| |
| test_cases.extend([ |
| TestCase("TN-E-01", "Tech", "Python ist eine interpretierte Programmiersprache", "public", "Technische Info", "easy"), |
| TestCase("TN-E-02", "Wetter", "Das Wetter in Berlin ist heute sonnig mit 22 Grad", "public", "Wetterbericht", "easy"), |
| TestCase("TN-E-03", "Frage", "Was ist der Unterschied zwischen REST und GraphQL?", "public", "Technische Frage", "easy"), |
| TestCase("TN-E-04", "Allgemein", "Wie funktioniert Machine Learning?", "public", "Allgemeine Wissensfrage", "easy"), |
| ]) |
| |
| |
| test_cases.extend([ |
| TestCase("TN-M-01", "Fiktion-Medizin", "In der Serie 'House' diagnostiziert der Arzt komplexe Krankheiten", "public", "Fiktionaler medizinischer Kontext", "medium"), |
| TestCase("TN-M-02", "Beispiel-IBAN", "Das IBAN-Format fΓΌr Deutschland ist DE + 2 PrΓΌfziffern + 8 BLZ + 10 Kontonummer", "public", "ErklΓ€rung ohne echte IBAN", "medium"), |
| TestCase("TN-M-03", "Code-Beispiel", "const password = 'hash123'; // Beispiel fΓΌr Passwortspeicherung", "public", "Code-Snippet mit 'password' String", "medium"), |
| TestCase("TN-M-04", "Bildung", "Im Medizinstudium lernt man ΓΌber Anatomie und Physiologie", "public", "Bildungskontext, nicht Patient", "medium"), |
| ]) |
| |
| |
| test_cases.extend([ |
| TestCase("TN-H-01", "Partiell-IBAN", "DE123 ist keine gΓΌltige IBAN - sie muss 22 Zeichen haben", "public", "UngΓΌltiges IBAN-Format", "hard"), |
| TestCase("TN-H-02", "Zahlenfolge", "Die Telefonnummer 12345678901234567890 ist zu lang", "public", "Lange Zahl, aber keine Kreditkarte", "hard"), |
| TestCase("TN-H-03", "Fiktion-Gehalt", "In dem Roman verdient der Protagonist 100.000 EUR Gehalt", "public", "Fiktionales Gehalt", "hard"), |
| TestCase("TN-H-04", "Generisch-Patient", "Der Patient in Zimmer 5 ist ein NPC im Videospiel", "public", "Gaming-Kontext mit 'Patient'", "hard"), |
| TestCase("TN-H-05", "Tech-Health", "Das System health check zeigt CPU: healthy, Memory: healthy", "public", "Technischer 'health' Begriff", "hard"), |
| TestCase("TN-H-06", "English-Blood", "The new album 'Blood on the Dance Floor' is available now", "public", "Nicht-medizinischer Blood-Kontext", "hard"), |
| ]) |
| |
| |
| |
| |
| |
| test_cases.extend([ |
| |
| TestCase("ADV-01", "Obfusc-IBAN", "D E 8 9 3 7 0 4 0 0 4 4 0 5 3 2 0 1 3 0 0 0", "public", "IBAN mit Leerzeichen (Obfuscation)", "adversarial"), |
| TestCase("ADV-02", "Leet-Password", "My p@ssw0rd is s3cur3123", "public", "Leet-Speak Passwort-Obfuscation", "adversarial"), |
| |
| |
| TestCase("ADV-03", "Kontext-Dual", "Ich behandle den Bug im Code, nicht den Patienten", "public", "Dual-Kontext (Code vs Medizin)", "adversarial"), |
| TestCase("ADV-04", "Historisch", "Im Jahr 1985 kostete die Diagnose von Computerviren viel Zeit", "public", "Historischer Tech-Kontext mit Medizin-Wort", "adversarial"), |
| |
| |
| TestCase("ADV-05", "Fast-IBAN", "DE8937040044053201300 ist falsch formatiert", "public", "UngΓΌltige IBAN (21 statt 22 Zeichen)", "adversarial"), |
| TestCase("ADV-06", "Fast-CC", "411111111111111 ist keine Kreditkarte", "public", "15-stellige Zahl (nicht CC)", "adversarial"), |
| |
| |
| TestCase("ADV-07", "Mixed-Lang", "The patient wurde behandelt successfully", "private", "Deutsch-Englisch Mix mit Patient", "adversarial"), |
| |
| |
| TestCase("ADV-08", "Noise-Medical", "Lorem ipsum dolor sit amet Diabetes consectetur adipiscing elit", "private", "Medizinisches Keyword in Lorem Ipsum", "adversarial"), |
| ]) |
| |
| |
| |
| |
| |
| test_cases.extend([ |
| |
| TestCase("BND-01", "Single-Health", "health", "public", "Nur 'health' ohne Kontext", "medium"), |
| TestCase("BND-02", "Single-Patient", "patient", "private", "Nur 'patient' als Keyword", "medium"), |
| TestCase("BND-03", "Single-Gehalt", "gehalt", "private", "Nur 'Gehalt' als Keyword", "medium"), |
| |
| |
| TestCase("BND-04", "Word-Boundary", "unhealthy Wealthiness", "public", "EnthΓ€lt 'health' als Substring, sollte NICHT triggern", "hard"), |
| TestCase("BND-05", "Compound-DE", "Gehaltstabelle und Einkommensteuer im Γberblick", "private", "Deutsche Komposita mit Gehalt", "hard"), |
| ]) |
| |
| return test_cases |
|
|
|
|
| def run_extended_tests() -> Dict: |
| """Run all extended test cases and collect results""" |
| |
| test_cases = create_extended_test_cases() |
| detector = PrivacyDetector() |
| |
| results = { |
| "timestamp": datetime.now().isoformat(), |
| "total_tests": len(test_cases), |
| "summary": { |
| "TP": 0, |
| "TN": 0, |
| "FP": 0, |
| "FN": 0 |
| }, |
| "by_difficulty": { |
| "easy": {"correct": 0, "total": 0}, |
| "medium": {"correct": 0, "total": 0}, |
| "hard": {"correct": 0, "total": 0}, |
| "adversarial": {"correct": 0, "total": 0} |
| }, |
| "test_details": [] |
| } |
| |
| for tc in test_cases: |
| |
| detected_level, details = detect_privacy_level(tc.input_text, agent_id=None) |
| detected_value = detected_level.value |
| |
| |
| expected_sensitive = tc.expected_level in ["private", "confidential"] |
| detected_sensitive = detected_value in ["private", "confidential"] |
| |
| is_correct = False |
| confusion_type = "" |
| |
| if expected_sensitive and detected_sensitive: |
| results["summary"]["TP"] += 1 |
| is_correct = True |
| confusion_type = "TP" |
| elif not expected_sensitive and not detected_sensitive: |
| results["summary"]["TN"] += 1 |
| is_correct = True |
| confusion_type = "TN" |
| elif not expected_sensitive and detected_sensitive: |
| results["summary"]["FP"] += 1 |
| confusion_type = "FP" |
| else: |
| results["summary"]["FN"] += 1 |
| confusion_type = "FN" |
| |
| |
| results["by_difficulty"][tc.difficulty]["total"] += 1 |
| if is_correct: |
| results["by_difficulty"][tc.difficulty]["correct"] += 1 |
| |
| |
| results["test_details"].append({ |
| "id": tc.id, |
| "category": tc.category, |
| "difficulty": tc.difficulty, |
| "input": tc.input_text[:80] + ("..." if len(tc.input_text) > 80 else ""), |
| "expected": tc.expected_level, |
| "detected": detected_value, |
| "correct": is_correct, |
| "confusion_type": confusion_type, |
| "detection_reason": details.get("reason", "unknown"), |
| "keyword_matches": len(details.get("keyword_matches", [])), |
| "pattern_matches": len(details.get("pattern_matches", [])) |
| }) |
| |
| |
| s = results["summary"] |
| total = s["TP"] + s["TN"] + s["FP"] + s["FN"] |
| |
| |
| results["metrics"] = { |
| "precision": s["TP"] / (s["TP"] + s["FP"]) if (s["TP"] + s["FP"]) > 0 else 0, |
| "recall": s["TP"] / (s["TP"] + s["FN"]) if (s["TP"] + s["FN"]) > 0 else 0, |
| "accuracy": (s["TP"] + s["TN"]) / total if total > 0 else 0 |
| } |
| |
| |
| p, r = results["metrics"]["precision"], results["metrics"]["recall"] |
| results["metrics"]["f1_score"] = 2 * p * r / (p + r) if (p + r) > 0 else 0 |
| |
| return results |
|
|
|
|
| def print_results(results: Dict): |
| """Print results in formatted output""" |
| |
| print("\n" + "="*70) |
| print(" EXTENDED PRIVACY DETECTION TEST RESULTS") |
| print("="*70) |
| print(f"\nTimestamp: {results['timestamp']}") |
| print(f"Total Tests: {results['total_tests']}\n") |
| |
| |
| s = results["summary"] |
| print("KONFUSIONSMATRIX:") |
| print("-"*50) |
| print(f" β Predicted PRIVATE β Predicted PUBLIC") |
| print(f"ββββββββββββββββββββββββΌββββββββββββββββββββΌββββββββββββββββββ") |
| print(f" Actual PRIVATE β {s['TP']:>8} (TP) β {s['FN']:>8} (FN)") |
| print(f" Actual PUBLIC β {s['FP']:>8} (FP) β {s['TN']:>8} (TN)") |
| print("-"*50) |
| |
| |
| m = results["metrics"] |
| print(f"\nMETRIKEN:") |
| print(f" Precision: {m['precision']:.4f} ({m['precision']*100:.2f}%)") |
| print(f" Recall: {m['recall']:.4f} ({m['recall']*100:.2f}%)") |
| print(f" F1-Score: {m['f1_score']:.4f} ({m['f1_score']*100:.2f}%)") |
| print(f" Accuracy: {m['accuracy']:.4f} ({m['accuracy']*100:.2f}%)") |
| |
| |
| print(f"\nERGEBNISSE NACH SCHWIERIGKEITSGRAD:") |
| for diff, data in results["by_difficulty"].items(): |
| if data["total"] > 0: |
| rate = data["correct"] / data["total"] * 100 |
| print(f" {diff.capitalize():12}: {data['correct']}/{data['total']} ({rate:.1f}%)") |
| |
| |
| failed = [t for t in results["test_details"] if not t["correct"]] |
| if failed: |
| print(f"\nFEHLGESCHLAGENE TESTS ({len(failed)}):") |
| print("-"*70) |
| for t in failed: |
| print(f" [{t['id']}] {t['confusion_type']}") |
| print(f" Input: {t['input']}") |
| print(f" Expected: {t['expected']} | Detected: {t['detected']}") |
| print(f" Reason: {t['detection_reason']}") |
| print() |
| else: |
| print("\nβ Alle Tests bestanden!") |
| |
| print("="*70) |
|
|
|
|
| def generate_markdown_table(results: Dict) -> str: |
| """Generate markdown table for thesis documentation""" |
| |
| lines = [] |
| lines.append("### Erweiterte Konfusionsmatrix (n={})".format(results["total_tests"])) |
| lines.append("") |
| lines.append("| | Predicted PRIVATE/CONF | Predicted PUBLIC/INT |") |
| lines.append("|----------------|------------------------|----------------------|") |
| lines.append(f"| **Actual PRIVATE/CONF** | {results['summary']['TP']} (TP) | {results['summary']['FN']} (FN) |") |
| lines.append(f"| **Actual PUBLIC/INT** | {results['summary']['FP']} (FP) | {results['summary']['TN']} (TN) |") |
| lines.append("") |
| |
| m = results["metrics"] |
| lines.append("### Berechnete Metriken") |
| lines.append("") |
| lines.append("```") |
| lines.append(f"Precision = TP / (TP + FP) = {results['summary']['TP']} / ({results['summary']['TP']} + {results['summary']['FP']}) = {m['precision']:.4f} ({m['precision']*100:.2f}%)") |
| lines.append(f"Recall = TP / (TP + FN) = {results['summary']['TP']} / ({results['summary']['TP']} + {results['summary']['FN']}) = {m['recall']:.4f} ({m['recall']*100:.2f}%)") |
| lines.append(f"F1-Score = 2 * (P * R) / (P + R) = {m['f1_score']:.4f} ({m['f1_score']*100:.2f}%)") |
| lines.append(f"Accuracy = (TP + TN) / Total = {m['accuracy']:.4f} ({m['accuracy']*100:.2f}%)") |
| lines.append("```") |
| lines.append("") |
| |
| |
| lines.append("### Ergebnisse nach Schwierigkeitsgrad") |
| lines.append("") |
| lines.append("| Schwierigkeit | Korrekt | Gesamt | Rate |") |
| lines.append("|---------------|---------|--------|------|") |
| for diff, data in results["by_difficulty"].items(): |
| if data["total"] > 0: |
| rate = data["correct"] / data["total"] * 100 |
| lines.append(f"| {diff.capitalize()} | {data['correct']} | {data['total']} | {rate:.1f}% |") |
| lines.append("") |
| |
| |
| failed = [t for t in results["test_details"] if not t["correct"]] |
| if failed: |
| lines.append("### Fehlgeschlagene TestfΓ€lle (Detail)") |
| lines.append("") |
| lines.append("| Test-ID | Typ | Input (gekΓΌrzt) | Erwartet | Erkannt | Grund |") |
| lines.append("|---------|-----|-----------------|----------|---------|-------|") |
| for t in failed: |
| input_short = t['input'][:40] + "..." if len(t['input']) > 40 else t['input'] |
| lines.append(f"| {t['id']} | {t['confusion_type']} | {input_short} | {t['expected']} | {t['detected']} | {t['detection_reason']} |") |
| |
| return "\n".join(lines) |
|
|
|
|
| if __name__ == "__main__": |
| print("Running Extended Privacy Detection Tests...") |
| results = run_extended_tests() |
| print_results(results) |
| |
| |
| md_output = generate_markdown_table(results) |
| |
| |
| output_path = Path(__file__).parent.parent.parent / "docs" / "extended_privacy_test_results.json" |
| with open(output_path, "w", encoding="utf-8") as f: |
| json.dump(results, f, indent=2, ensure_ascii=False) |
| |
| md_path = Path(__file__).parent.parent.parent / "docs" / "extended_privacy_test_results.md" |
| with open(md_path, "w", encoding="utf-8") as f: |
| f.write(md_output) |
| |
| print(f"\nResults saved to:") |
| print(f" - {output_path}") |
| print(f" - {md_path}") |
|
|