Datenbereinigung und -abgleich
Standardwerkzeuge: DuckDB 2.0 SQL oder Polars 2.x Lazy API (scan_csv, scan_parquet) für lokale Dateien. Pandas nur, wenn eine Bibliothek es erzwingt. Python über uv, Linting mit ruff. Überprüfe alle API-Details, die sich in Polars 2.0 oder DuckDB 2.0 geändert haben, anhand der aktuellen Dokumentation, bevor du dich darauf verlässt (siehe references/landscape-2026-11.md).
Niemals stillschweigend korrigieren
Jede Konvertierung, Löschung, Standardzuweisung oder Zusammenführung wird gezählt und gemeldet. Ein Bereinigungsschritt ist (rows_in, rows_out, rows_changed, reason). Fehlgeschlagene Typumwandlungen werden zu NULL plus einer Zählung, niemals zu einer gelöschten Zeile und niemals zu einem geschätzten Wert. Lasse die Rohdaten unberührt; schreibe die bereinigte Ausgabe in einen neuen Pfad. Wenn die Zählungen eines Schritts überraschen, stoppe und melde, anstatt fortzufahren.
Vorgehen
- Profilieren (schreibgeschützt). 2. Schlüssel, Körnung und Typen festlegen. 3. Bereinigen in einer Pipeline aus kleinen, gezählten Schritten. 4. Abgleichen, wenn zwei Quellen vorliegen. 5. Ausnahmen plus Zusammenfassung mit allen Zählungen ausgeben.
1. Vor dem Bearbeiten profilieren
Lese jede Spalte zuerst als Text (all_varchar=true in DuckDB, infer_schema=False in Polars), da die Inferenz anhand einer Stichprobe stillschweigend 00123 in 123 verwandelt und gemischte Spalten in den Typ der ersten Zeile umwandelt.
Berechne dann pro Spalte: Zeilenanzahl, Null/Leerstringrate, Anzahl unterschiedlicher Werte, Min/Max (als Text und als geparster Wert), kürzeste/längste Länge und Anteil der Werte, die einem Typmuster entsprechen (Integer, Dezimalzahl, ISO-Datum, Sonstiges).
DuckDB SUMMARIZE erledigt das meiste in einem Aufruf.
Halte vor der Bereinigung fest: Gesamtzeilen, Anzahl unterschiedlicher Kandidatenschlüssel, Zeilen pro Schlüssel (Maximum) und die exakt vorgefundenen Kopfzeilen (nachgestellte Leerzeichen, BOM, doppelte Namen). Ein Kandidatenschlüssel ist nur ein Schlüssel, wenn die Anzahl unterschiedlicher Werte nach Normalisierung der Zeilenanzahl entspricht; teste dies, nimm es nicht an.
Prüfe auf: zeilenweise Duplikate, wiederholte Kopfzeilen in der Mitte der Datei, Fußzeilen/Summenzeilen innerhalb der Daten, Platzhalterwerte ("N/A", "-", "0000-00-00", 9999-12-31), gemischte Kodierungen und numerische Spalten, die eigentlich IDs sind.
2. Typen und Parsing
- IDs, PLZ/Postleitzahlen, Kontonummern und Telefonnummern sind immer Zeichenketten. Speichere eine Auffüllung auf bekannte Breite nur dann als Regel (
lpadauf die bekannte Breite), wenn die Breite dokumentiert ist; ansonsten belasse sie wie gelesen. - Datumsangaben: Parse mit einem expliziten Format pro Spalte (
try_strptime/str.to_date(format, strict=False)). Lasse den Motor niemals die Reihenfolge von Tag/Monat erraten. Mehrdeutige Werte (03/04/2026) ohne dokumentierte Reihenfolge sind eine Ausnahme, kein Münzwurf. Lege die Zeitzonenentscheidung explizit fest. - Geldbeträge: Parse in ganzzahlige Untereinheiten oder
DECIMAL(p,s), niemals in Floats. Entferne Währungssymbole und Tausendertrennzeichen nach Gebietsschemaregel:1.234,56und1,234.56sind unterschiedliche Gebietsschemata – entscheide also pro Quelle und stelle sicher, dass jeder Wert dem gewählten Muster entspricht. Klammern und nachgestelltes Minus sind negativ. Halte die Währung pro Zeile fest; summiere nicht über verschiedene Währungen hinweg. - Booleans und Enums: Kartiere von einer expliziten Tabelle; nicht kartierte Werte gehen in die Ausnahmen.
- Zähle nach jeder Umwandlung
non-null beforeminusnon-null after. Diese Zahl ist die Anzahl der fehlgeschlagenen Konvertierungen und wird im Bericht ausgewiesen.
3. Normalisierung
Wende die Normalisierung auf einen abgeleiteten Match-Schlüssel an, nicht auf die ursprüngliche Spalte, sodass der Rohwert erhalten bleibt:
Unicode NFKC, Trimmen, Zusammenziehen interner Leerzeichen, Fallnormalisierung, Entfernen von Nullbreiten- und geschützten Leerzeichen.
NFKC ist verlustbehaftet (Ligaturen, Vollbreitenformen, hochgestellte Zeichen) – das ist für den Abgleich beabsichtigt und für die Anzeige falsch.
Entferne bei IDs auch Trennzeichen (-, /, Leerzeichen) und lege die Behandlung von O/0 und I/1 explizit fest.
NFKC ist im DuckDB-Kern-SQL nicht enthalten; führe es in Polars/Python aus oder registriere eine UDF (überprüfe die aktuelle Unterstützung).
4. Deduplizierung
- Exakte Duplikate: Entfernen mit Zählung; behalte die Zeilennummer der überlebenden Zeile aus der Quelldatei.
- Duplikate nach Schlüssel: Entscheide zuerst, ob es sich um echte Wiederholungen oder getrennte Ereignisse handelt (gleiche Rechnungsnummer, unterschiedlicher Betrag ist ein Konflikt, kein Duplikat).
- Die Auswahl des Überlebenden muss deterministisch sein und angegeben werden:
ROW_NUMBER() OVER (PARTITION BY key ORDER BY updated_at DESC NULLS LAST, completeness DESC, source_row DESC) = 1(DuckDBQUALIFY). Beende die ORDER BY-Klausel immer mit einem eindeutigen Tiebreaker wie der Quellzeilennummer, da Gleichstände sonst durch eine willkürliche Scan-Reihenfolge aufgelöst werden. Verwende in Polarssort(...)dannunique(subset, keep="first", maintain_order=True). - Fuzzy-Deduplizierung ist ein Kandidatengenerator, niemals eine automatische Zusammenführung: Blocke auf einem billigen Schlüssel (normalisierte PLZ, erste 3 Buchstaben, Jahr), bewerte innerhalb der Blöcke, akzeptiere automatisch nur oberhalb einer hohen Schwelle mit einem exakten Sekundärfeld, das übereinstimmt, und sende das mittlere Band an einen Menschen zum Bearbeiten. Melde die Schwelle und die Anzahl.
5. Reconciliations-Audit
Quelle der Wahrheit = A, abgeleitet = B (z. B. Systemexport vs. OCR-gescannte Dokumente). Sage niemals „B ist falsch“, bevor du überprüft hast, ob der Fehler in der Extraktion oder im Schlüssel liegt.
- Kontrollsummen zuerst. Zeilenanzahl und Summe jeder Geld-/Mengenspalte in A und B, plus Anzahl unterschiedlicher Schlüssel. Eine Abweichung zeigt an, ob fehlende Zeilen, zusätzliche Zeilen oder Betragsdrift zu erwarten sind. Wenn Summen übereinstimmen, aber Zeilen abweichen, bedeutet dies sich ausgleichende Fehler; fahre fort.
- Schlüsselabgleich. Baue denselben normalisierten Schlüssel auf beiden Seiten (Abschnitt 3). Gleiche zuerst exakt ab, dann in expliziten Durchgängen mit abnehmender Strenge (z. B. nur Schlüssel, dann Schlüssel plus Zeitfenster), wobei jeder Treffer mit dem Durchgang markiert wird, der ihn erzeugt hat. Mische niemals Durchgänge ohne Markierung.
- Anti-Joins in beide Richtungen. A-nicht-in-B (fehlt in den abgeleiteten Daten) und B-nicht-in-A (unerwartet in den abgeleiteten Daten, oft OCR-Schlüsseltippfehler oder Duplikate). Melde beide; das Lesen nur einer Richtung verbirgt die Hälfte der Fehler. Bevor du eine Zeile als fehlend bezeichnest, überprüfe, ob ihr Schlüssel mit Duplikaten auf einer der Seiten kollidiert (Viele-zu-Viele-Joins erhöhen die Summen).
- Feldvergleich bei gepaarten Treffern. Vergleiche pro Feld nach derselben Normalisierung.
Geldtoleranz: Vergleiche in Untereinheiten,
abs(a - b) <= tol, wobei tol im Voraus festgelegt wird (0 für Hauptbücher; eine Rundungstoleranz wie 1 Untereinheit pro Zeile nur, wenn die Quellen unterschiedlich runden, und niemals ein Prozentsatz bei kleinen Beträgen). Daten: Exakt oder ein angegebenes Tagesfenster. Text: Normalisierte Gleichheit, dann ein Ähnlichkeitsscore, der als Score gemeldet wird, nicht als bestanden/nicht bestanden. - Klassifiziere, unterscheide nicht nur. Jede Ausnahme erhält einen Grundcode:
MISSING_IN_B,EXTRA_IN_B,DUP_KEY_A,DUP_KEY_B,AMOUNT_MISMATCH,FIELD_MISMATCH,UNPARSEABLE_A,UNPARSEABLE_B,AMBIGUOUS_MATCH. Nicht-parsbare Zeilen sind Ausnahmen und werden niemals stillschweigend von Summen ausgeschlossen. - Schließe den Kreislauf. Die endgültige Identität muss gelten:
|A| = matched + missing_in_B + dup_excess_A + unparseable_A(und das Pendant für B). Wenn sie nicht aufgeht, ist das Audit nicht abgeschlossen.
SQL-Muster für Durchgänge, Anti-Joins, Toleranz und die Saldenprüfung befinden sich in references/reconciliation-recipes.md.
Ausnahmebericht
Eine Zeile pro Ausnahme, lesbar ohne das Tooling:
reason_code, key, A-Quelldatei und Zeilennummer, B-Quelldatei und Zeilennummer (und Seite bei gescannten Dokumenten), field, a_value, b_value, delta, suggested_action.
Sortiere absteigend nach monetärer Auswirkung, dann nach Grund.
Stelle eine Zusammenfassung voran: Eingaben mit Zeilenanzahlen und Prüfsummen, jeder Bereinigungsschritt mit Zählungen, Kontrollsummen beider Seiten, die Saldenidentität, verwendete Toleranzen.
Schreibe CSV für die Maschine und eine kurze Markdown- oder XLSX-Ansicht für den Menschen; gib nicht Tausende von Zeilen in den Chat aus, fasse die Anzahl nach Grund zusammen und zeige die wichtigsten Beispiele.
Absicherungen
- Überprüfe, schätze nicht: Zeilenanzahlen nach Joins, Schlüsseleindeutigkeit nach Deduplizierung, keine Float-Spalten für Geldbeträge.
- Left Joins auf nicht-eindeutigen Schlüsseln multiplizieren Zeilen; überprüfe die Anzahl vor und nach jedem Join.
- Überschreibe A nicht. Korrekturen an der Quelle der Wahrheit sind eine menschliche Entscheidung, die im Bericht aufgeführt wird.
- Datenbankgeheimnisse werden aus
/run/secrets/<name>gelesen, nicht aus Umgebungsvariablen oder der Kommandozeile. - Deklarative Prüfungen (soda-core, great_expectations) sind sinnvoll, sobald die Regeln stabil sind; das Profil und die Zählungen oben kommen zuerst.
Lessons learned aus der Praxis sammeln sich in references/gotchas.md.