Problem.
Die manuelle Triage von pharmazeutischen Nebenwirkungs-Berichten ist im großen Maßstab langsam, inkonsistent und teuer. Pharmakovigilanz-Teams, die diese Berichte gegen standardisierte Taxonomien klassifizieren, sind durch das Volumen ausgebremst, und die Klassifikationsqualität schwankt von Analyst zu Analyst.
Die Frage hinter diesem Projekt: Kann ein Multi-Class-Classifier die Qualität menschlicher Triage genau genug treffen, um als Decision-Support-Tool sinnvoll deployt zu werden? Und kann in einer regulierten Domäne wie der Pharmakovigilanz die erklärbare Baseline nahe genug an das komplexere Modell herankommen, dass man die erklärbare Variante ausliefert?
Mein Beitrag.
Eigenständig, end-to-end:
- Die Feature-Pipeline gebaut. TF-IDF-Vektorisierung von Medikamentenname, Zusammensetzung, Indikationen und Nebenwirkungs-Text, dazu Ordinal-Encoding der Hersteller (mit sicherer Behandlung unbekannter Hersteller zur Inferenz-Zeit) und Review-Prozent-Features, alles hinter einem einzigen sklearn
ColumnTransformer+FeatureUnion. - Zwei komplementäre Modelle auf 11.825 marktverfügbaren Medikamenten trainiert, die einer MedDRA-orientierten Taxonomie aus zehn klinischen Kategorien zugeordnet sind. Random Forest als primäres Modell, Logistic Regression als interpretierbarer Vergleich.
- Beide in einer serialisierbaren sklearn
Pipelineverpackt, sodassjoblib.dumpein einziges End-to-End-Artefakt erzeugt und die Inferenz keine manuelle Feature-Vorbereitung mehr braucht. - Jeden Hyperparameter in eine YAML-Config ausgelagert, geladen in eingefrorene Dataclasses. Keine Magic Numbers im Code.
- Das CLI (
train,predict) und die pytest-Suite geschrieben (synthetische 60-Zeilen-Fixture, damit Tests nicht von der 4-MB-xls abhängen). - Die GitHub-Actions-Matrix-CI auf Python 3.10, 3.11 und 3.12 aufgesetzt, mit ruff- und black-Gating.
Architektur.
+------------------+ +-----------------------+ +------------------+
| Medicine_Details | -> | Category mapping | -> | Stratified split |
| (.xls, 11,825) | | (10 MedDRA-aligned | | (80/20, seed 42) |
+------------------+ | categories) | +------------------+
+-----------------------+ |
v
+----------------------------+
| sklearn Pipeline |
| ColumnTransformer |
| TF-IDF (name, comp, |
| uses, side fx) |
| OrdinalEncoder (mfr) |
| StandardScaler (rev %) |
| Classifier |
| RF or LR (registry) |
+----------------------------+
|
v
joblib artifact + metrics + plots
Ergebnisse.
Die Trainings-Pipeline gibt Accuracy, Macro F1, Weighted F1, Precision und Recall pro Klasse sowie eine Confusion-Matrix-Grafik aus. Beide Modelle liefern auf dem stratifizierten 20-Prozent-Hold-out gute Ergebnisse; die interpretierbare Logistic-Regression-Baseline kommt nahe genug an den Random Forest heran, dass der Vergleich selbst zum Befund wird.
Konkrete Metrik-Werte sind in diesem Text bewusst nicht festgenagelt, weil das Repo so konfiguriert ist, dass es sie über make train deterministisch neu erzeugt. Wer das Repo klont, führt einen Befehl aus und bekommt die aktuellen Zahlen aus dem aktuellen Code, nicht aus einem veralteten Screenshot.
Erkenntnisse.
Die nützlichste Erkenntnis war nicht die Headline-Accuracy. Sie war, dass das deutlich einfachere Modell nur wenige Prozentpunkte hinter dem komplexeren lag. Das sagt etwas Spezifisches über die Daten aus: Das kategoriale Signal ist stark genug, dass man keine tiefe Architektur braucht, um es zu extrahieren, und ein flaches, interpretierbares Modell ist tatsächlich konkurrenzfähig. In einer regulierten Domäne wie der Pharmakovigilanz, in der jede Modellentscheidung erklärbar sein muss, verändert das die Deployment-Rechnung. Man liefert das erklärbare Modell aus, es sei denn, man braucht die marginalen Extra-Punkte unbedingt.
Die zweite Erkenntnis betraf Scope-Disziplin. Eine erste Version hatte eine Streamlit-Demo, eine HuggingFace-Transformer-Baseline und einen Deployment-Workflow. Nichts davon war fertig. Ich habe alle drei gestrichen und den produktionsreifen Kern ausgeliefert: Feature-Pipeline, zwei Modelle, Evaluation, Inference-CLI, Tests, CI-Matrix. Weniger Oberfläche, mehr Tiefe.
Die dritte Erkenntnis betraf Leaks. Der ursprüngliche Prototyp hatte den Hersteller mit einem einmaligen LabelEncoder außerhalb der Pipeline kodiert, was zur Inferenz-Zeit bei jedem unbekannten Hersteller abgestürzt wäre. Das Encoding in die Pipeline zu verschieben als OrdinalEncoder(handle_unknown="use_encoded_value") ist die Art Fix, die in keiner Metriken-Tabelle auftaucht, aber den Unterschied macht zwischen einem Modell, das ausgeliefert wird, und einem, das beim ersten Kontakt mit Produktivdaten bricht.
Stack.
Python · scikit-learn · pandas · numpy · TF-IDF · Random Forest · Logistic Regression · joblib · pytest · ruff · black · GitHub Actions