Eine Stufenfunktion, kein glatter Trend
Vierter Eintrag in dieser Serie über den Prozess hinter einer Spannungsüberwachungs-Pipeline. Dies ist der Beitrag, in dem die widerrufene 43/43-Behauptung aus einem früheren Eintrag ihren echten Ersatz erhält – und in dem sich herausstellte, dass das ehrliche Ergebnis interessanter war als das ursprünglich erhoffte.
Was nötig war
Zwei Probleme von früher in dieser Serie liefen hier zusammen. Erstens, echte SimBench-Lastdaten produzieren auf ihrer nativen Skala null Verletzungen, daher gibt es keine Verletzungspopulation, gegen die man eine zustandsabhängige Überwachungsregel unter realistischen Bedingungen testen könnte. Zweitens, der ursprüngliche zustandsabhängige Schwellenwert war zirkulär – angesetzt am exakten Minimum der Stichprobe, an der er validiert wurde.
Beides gleichzeitig zu beheben bedeutete, ein echtes Schweregrad-Experiment (Severity-Sweep) aufzubauen: Eine echte Lastform (keine synthetische Tabelle), skaliert mit einem expliziten, vorab deklarierten Multiplikator, der über einen im Voraus festgelegten Bereich gestrichen wird – nicht erst ausgewählt, nachdem man nachgesehen hat, welche Werte ein günstiges Ergebnis liefern – und eine Überwachungsregel, die an jedem Punkt dieses Durchlaufs mit echter Train/Test-Disziplin getestet wird.
Den Beginn (Onset) finden
Ein ScaledLoadProvider umschließt ein echtes, zeitlich geordnetes SimBench-Lastprofil und wendet einen Multiplikator gleichmäßig auf den gesamten Ereignis-Stream an. Das Durchlaufen dieses Multiplikators von 1,00× bis 2,00× der netzeigenen dokumentierten Spitzenlast erzeugt eine saubere, monotone Dosis-Wirkungs-Beziehung auf der Population des kritischen Pfades:
| Schweregrad | Verletzungen (von 4.055 kritischen Ereignissen) | |---|---| | 1,00×–1,30× | 0 | | 1,35× | 1 | | 1,40× | 2 | | 1,50× | 14 | | 1,75× | 131 | | 2,00× | 372 |
Verletzungen treten erstmals beim 1,35-fachen der dokumentierten Spitzenlast auf – höher als das eigene 1,26-fache der synthetischen Standardtabelle, und lokalisiert, indem von der Spezifikation des Netzes aus nach außen getestet wurde, anstatt von der Kuratierung eines Dritten entlehnt zu sein. Dies ist an sich schon eine zweite, unabhängige Bestätigung des „Last dominiert Spannung“-Mechanismus aus dem ersten Beitrag in dieser Serie, zu der man auf einem völlig anderen Weg als bei der ursprünglichen Regression gelangte.
Das Recall-Audit, diesmal richtig gemacht
Mit einem gefundenen echten Beginn wurden drei Schweregrade für ein vollständiges Recall-Audit ausgewählt: 1,26× (die Standardtabelle) und zwei Punkte jenseits des echten Beginns, 1,50× und 1,75×. An jedem Punkt wurde die lineare Spannungssensitivitätsregression von Grund auf neu angepasst, und zwar an einem frischen 70/30-Split der eigenen Daten des kritischen Pfades dieses Schweregrads, und dann out-of-sample auf eine Recall-Audit-Population angewendet, die sie während der Anpassung nie berührt hatte – genau die Disziplin, die den ursprünglichen zirkulären Schwellenwert aufgedeckt hätte, diesmal richtig angewendet. Und wie im Beitrag zur Stichprobengröße dargelegt, wurde jeder Schweregrad gegen die gesamte Population von 16.531 unkritischen Ereignissen geprüft, nicht gegen eine Stichprobe.
| Schweregrad | FN-Rate (exakt) | Trefferquote (zustandsabh.) | 95% KI | |---|---|---|---| | 1,26× (Standardtabelle) | 7,9 % | 20,5 % | [18,4 %, 22,7 %] | | 1,50× (SimBench, skaliert) | 0,4 % | 26,0 % | [17,3 %, 37,1 %] | | 1,75× (SimBench, skaliert) | 3,9 % | 74,3 % | [70,8 %, 77,5 %] |
Das Ergebnis, nicht das, was ich erwartet hatte
Zu Beginn hoffte ich auf ein sauberes, monotones Ergebnis: Das zustandsabhängige Screening wird kontinuierlich besser, je drastischer die Bedingungen werden. Das ist aber nicht das, was die Daten zeigen.
Zwischen 1,26× und 1,50× ist die Trefferquote statistisch nicht zu unterscheiden – 20,5 % gegenüber 26,0 %, mit stark überlappenden Konfidenzintervallen. Dann, bei 1,75×, springt sie auf 74,3 %, mit einem Konfidenzintervall, das keines der beiden früheren überhaupt überlappt – die Obergrenze von 1,50× (37,1 %) liegt deutlich unter der Untergrenze von 1,75× (70,8 %).
Das ist eine echte Stufenfunktion, kein glatter Trend: Zustandsabhängiges Screening bietet nahe dem Beginn der Verletzungen wenig Vorteil und einen großen, robusten Vorteil, sobald die Betriebsbedingungen wesentlich darüber hinaus verschoben werden. Das ist eine spezifischere und falsifizierbarere Behauptung als „zustandsabhängiges Screening hilft“ – es besagt, wo der Wert sichtbar wird, und das ist genau die Art von Ergebnis, die tatsächlich nützlich ist, um zu entscheiden, wann ein solches Screening den Einsatz wert ist und wann nicht.
Warum der flache Teil genauso wichtig ist wie der Sprung
Es wäre ein Leichtes gewesen, nur das 1,75×-Ergebnis zu berichten und den flachen Bereich unerwähnt zu lassen – das Papier hätte sauberer ausgesehen. Aber der flache Bereich ist ein echtes, eigenständiges Ergebnis: Er besagt, dass der Mechanismus, der das zustandsabhängige Screening wertvoll macht (Hintergrundlast dominiert Spannung), ausreichend Abstand zu den Nennbedingungen benötigt, bevor eine angepasste Regel ihn zuverlässig vom Rauschen unterscheiden kann. Hätte man nur das starke Ergebnis berichtet, wäre genau die Randbedingung verborgen geblieben, die ein Übertragungs- oder Verteilnetzbetreiber kennen müsste, der diesen Ansatz evaluiert, bevor er ihm operativ vertraut.
Reproduzierbarkeit: der Schweregrad-Durchlauf und das Recall-Audit der Vollerhebung
from engine import (
GridSimulator, LocalCsvIngestionLayer,
ScaledLoadProvider, TimeSeriesLoadProvider, load_simbench_profile,
)
simulator = GridSimulator()
stream = LocalCsvIngestionLayer().fetch_stream()
simbench_vals = load_simbench_profile()
base_provider = TimeSeriesLoadProvider(simbench_vals)
# Den Beginn lokalisieren (Schweregrade vor dem Ausführen deklariert)
for severity in [1.00, 1.05, 1.10, 1.15, 1.20, 1.25, 1.30, 1.35, 1.40, 1.50, 1.75, 2.00]:
provider = ScaledLoadProvider(base_provider, severity)
cycle_df = simulator.run_streaming_pipeline(stream, load_provider=provider)
critical = cycle_df[cycle_df["critical_event"] == True].dropna(subset=["raw_vm_ref_pu"])
n_violations = (critical["raw_vm_ref_pu"] < 0.90).sum()
print(f"Schweregrad={severity:.2f}: {n_violations} Verletzungen")
# Vollständiger Durchlauf run_severity_recall_audit.py <severity> 16531 für jeweils 1.50, 1.75
# reproduziert die Recall-Tabelle exakt (frische Regression, Vollerhebung).



