gridshedwatch 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gridshedwatch-1.1.1/LICENSE +21 -0
- gridshedwatch-1.1.1/PKG-INFO +170 -0
- gridshedwatch-1.1.1/README.md +147 -0
- gridshedwatch-1.1.1/gridshedwatch/__init__.py +0 -0
- gridshedwatch-1.1.1/gridshedwatch/alerts.py +77 -0
- gridshedwatch-1.1.1/gridshedwatch/baselines/__init__.py +14 -0
- gridshedwatch-1.1.1/gridshedwatch/baselines/chronos.py +39 -0
- gridshedwatch-1.1.1/gridshedwatch/baselines/lstm.py +88 -0
- gridshedwatch-1.1.1/gridshedwatch/baselines/oprule.py +32 -0
- gridshedwatch-1.1.1/gridshedwatch/baselines/zscore.py +24 -0
- gridshedwatch-1.1.1/gridshedwatch/benchmark.py +205 -0
- gridshedwatch-1.1.1/gridshedwatch/cli.py +129 -0
- gridshedwatch-1.1.1/gridshedwatch/config.py +55 -0
- gridshedwatch-1.1.1/gridshedwatch/data.py +131 -0
- gridshedwatch-1.1.1/gridshedwatch/events.yaml +105 -0
- gridshedwatch-1.1.1/gridshedwatch/operator_notices.yaml +124 -0
- gridshedwatch-1.1.1/gridshedwatch/rules.py +121 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/SHA256SUMS +8 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/eia930/CISO.csv.gz +0 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/eia930/CPLE.csv.gz +0 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/eia930/DUK.csv.gz +0 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/eia930/ERCO.csv.gz +0 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/eia930/LGEE.csv.gz +0 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/eia930/MISO.csv.gz +0 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/eia930/SWPP.csv.gz +0 -0
- gridshedwatch-1.1.1/gridshedwatch/snapshot_v1.0/eia930/TVA.csv.gz +0 -0
- gridshedwatch-1.1.1/gridshedwatch/submission.py +68 -0
- gridshedwatch-1.1.1/gridshedwatch.egg-info/PKG-INFO +170 -0
- gridshedwatch-1.1.1/gridshedwatch.egg-info/SOURCES.txt +38 -0
- gridshedwatch-1.1.1/gridshedwatch.egg-info/dependency_links.txt +1 -0
- gridshedwatch-1.1.1/gridshedwatch.egg-info/entry_points.txt +2 -0
- gridshedwatch-1.1.1/gridshedwatch.egg-info/requires.txt +13 -0
- gridshedwatch-1.1.1/gridshedwatch.egg-info/top_level.txt +1 -0
- gridshedwatch-1.1.1/pyproject.toml +35 -0
- gridshedwatch-1.1.1/setup.cfg +4 -0
- gridshedwatch-1.1.1/tests/test_alerts.py +74 -0
- gridshedwatch-1.1.1/tests/test_data.py +52 -0
- gridshedwatch-1.1.1/tests/test_regression.py +48 -0
- gridshedwatch-1.1.1/tests/test_rules.py +67 -0
- gridshedwatch-1.1.1/tests/test_scoring.py +39 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sam Heiserman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gridshedwatch
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: A benchmark for early warning of grid load sheds, scored the way forecasters score weather warnings
|
|
5
|
+
Author: Sam Heiserman
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gotham29.github.io/gridshedwatch
|
|
8
|
+
Project-URL: Repository, https://github.com/gotham29/gridshedwatch
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy>=1.22
|
|
13
|
+
Requires-Dist: pandas>=1.5
|
|
14
|
+
Requires-Dist: pyyaml>=6
|
|
15
|
+
Provides-Extra: lstm
|
|
16
|
+
Requires-Dist: torch>=2.0; extra == "lstm"
|
|
17
|
+
Provides-Extra: chronos
|
|
18
|
+
Requires-Dist: torch>=2.0; extra == "chronos"
|
|
19
|
+
Requires-Dist: chronos-forecasting>=1.4; extra == "chronos"
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# GridShedWatch
|
|
25
|
+
|
|
26
|
+
[](https://doi.org/10.5281/zenodo.23626223) [](https://github.com/gotham29/gridshedwatch/actions/workflows/tests.yml)
|
|
27
|
+
|
|
28
|
+
**A benchmark for early warning of grid load sheds, scored the way forecasters score weather warnings.**
|
|
29
|
+
|
|
30
|
+
Website: **https://gotham29.github.io/gridshedwatch** (built from this repo by `scripts/build_site.py`).
|
|
31
|
+
|
|
32
|
+
Since 2020, US grid operators have had to cut power for lack of supply eight times. The public
|
|
33
|
+
was warned beforehand in four of them, and in none of the four during Winter Storm Elliott.
|
|
34
|
+
Plenty of methods claim to detect grid trouble early, but there has been no shared test of
|
|
35
|
+
whether a warning would actually have come in time. GridShedWatch is that test: every one of
|
|
36
|
+
those eight load sheds, public hourly data, and one fixed scoring procedure that any method can
|
|
37
|
+
be run through.
|
|
38
|
+
|
|
39
|
+
The National Weather Service scores every warning it issues on three numbers read together:
|
|
40
|
+
how many events were warned in advance, how much lead time people got, and how often warnings
|
|
41
|
+
were false alarms. GridShedWatch asks the same of grid warnings, at two levels: a **watch**
|
|
42
|
+
(about one a month per grid: something unusual is building) and a **warning** (two a year: act).
|
|
43
|
+
|
|
44
|
+
## What's in it
|
|
45
|
+
|
|
46
|
+
| | |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **Events** | 8 supply-shortfall load sheds of at least 250 MW, Jan 2020 to Sep 2026, chosen by a stated rule from NERC's annual lists ([docs/events.md](https://github.com/gotham29/gridshedwatch/blob/main/docs/events.md)) |
|
|
49
|
+
| **Data** | A frozen snapshot of EIA-930 hourly demand, day-ahead forecast and net generation for the 8 grids, 2019-2026, and the five signals built from it ([gridshedwatch/snapshot_v1.0/](https://github.com/gotham29/gridshedwatch/tree/main/gridshedwatch/snapshot_v1.0/), [gridshedwatch/data.py](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/data.py)) |
|
|
50
|
+
| **Rules** | One shared rule that turns per-signal scores into watches and warnings, so methods differ only in how they score each hour ([gridshedwatch/rules.py](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/rules.py)) |
|
|
51
|
+
| **Scoring** | Same alert budget for every method, chosen without looking at the events; events caught, lead time and false alarm ratio, with bootstrap intervals and a chance baseline ([docs/scoring.md](https://github.com/gotham29/gridshedwatch/blob/main/docs/scoring.md)) |
|
|
52
|
+
| **Status quo** | Every operator notice in the week before each event, with times and sources ([gridshedwatch/operator_notices.yaml](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/operator_notices.yaml)) |
|
|
53
|
+
| **Reference methods** | An operator-style rule, a seasonal z-score, an LSTM forecaster, Chronos-Bolt (zero-shot), and HTM ([gridshedwatch/baselines/](https://github.com/gotham29/gridshedwatch/tree/main/gridshedwatch/baselines/)) |
|
|
54
|
+
| **Datasheet** | Motivation, composition, collection, intended uses, known biases and maintenance ([docs/datasheet.md](https://github.com/gotham29/gridshedwatch/blob/main/docs/datasheet.md)) |
|
|
55
|
+
|
|
56
|
+
| Event | Grid | Shed at its worst | Split |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| Heat wave, Aug 2020 | California (CAISO) | about 1,000 MW | development |
|
|
59
|
+
| Winter Storm Uri, Feb 2021 | Texas (ERCOT) | 20,000 MW | development |
|
|
60
|
+
| Uri | Southwest Power Pool | 2,700 MW | held-out |
|
|
61
|
+
| Uri | MISO (MISO South) | 700 MW | held-out |
|
|
62
|
+
| Winter Storm Elliott, Dec 2022 | Tennessee Valley Authority | 3,000 MW | development |
|
|
63
|
+
| Elliott | Duke Energy Carolinas | up to 1,000 MW | held-out |
|
|
64
|
+
| Elliott | Duke Energy Progress | about 960 MW | held-out |
|
|
65
|
+
| Elliott | LG&E and KU (Kentucky) | about 300 MW | held-out |
|
|
66
|
+
|
|
67
|
+
## Results, v1.0
|
|
68
|
+
|
|
69
|
+
<!-- leaderboard:start -->
|
|
70
|
+
| Method | Watches: caught (of 8) | held-out (of 5) | by chance | median lead (h) | Warnings: caught (of 8) | held-out (of 5) | by chance | False alarm ratio (watch / warning) |
|
|
71
|
+
|---|---|---|---|---|---|---|---|---|
|
|
72
|
+
| Operators (public notices) | 4 | 2 | - | 20 | - | - | - | - |
|
|
73
|
+
| Operator-style rule | 5 [3.0-7.0] | 4 | 1.2 | 85 | 2 [0.0-4.0] | 1 | 0.2 | 98% / 94% |
|
|
74
|
+
| Seasonal z-score | 5 [3.0-7.0] | 4 | 1.1 | 105 | 6 [4.0-8.0] | 4 | 0.2 | 98% / 92% |
|
|
75
|
+
| Chronos-Bolt (zero-shot) | 1 [0.0-3.0] | 1 | 0.9 | 9 | 3 [1.0-5.0] | 1 | 0.3 | 100% / 96% |
|
|
76
|
+
| LSTM forecaster | 6.4 [4.6-8.0] | 4.4 | 1.3 | 8 | 4.8 [2.8-6.8] | 2.8 | 0.3 | 99% / 94% |
|
|
77
|
+
| HTM (htm-monitor) | 6 [4.6-7.2] | 4 | 1.4 | 56 | 5.6 [3.8-7.2] | 3.6 | 0.3 | 98% / 93% |
|
|
78
|
+
|
|
79
|
+
All five reference methods are in the scores track; no alerts-track entries yet. Mean over 5 seeds for the LSTM and HTM. [a-b]: 5th-95th percentile with events resampled. Operators: public notices in the 7 days before onset, scored as watches.
|
|
80
|
+
<!-- leaderboard:end -->
|
|
81
|
+
|
|
82
|
+
Every method except Chronos's watches catches far more than chance, and none leads at both
|
|
83
|
+
levels. The differences are by kind of emergency: the simple rules caught all three Uri events
|
|
84
|
+
days ahead, the LSTM and HTM caught the Elliott events hours ahead (when the public got no
|
|
85
|
+
warning), and only the operators warned California. With eight events, differences of about one
|
|
86
|
+
event are not meaningful. Per-event results: `results/v1.0/<method>_events.csv`.
|
|
87
|
+
|
|
88
|
+
## Quick start
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install gridshedwatch # or, to work on the code:
|
|
92
|
+
git clone https://github.com/gotham29/gridshedwatch && cd gridshedwatch
|
|
93
|
+
pip install -e ".[test]"
|
|
94
|
+
gridshedwatch info # the eight events and their timelines
|
|
95
|
+
gridshedwatch score --oprule # score the built-in operator-style rule
|
|
96
|
+
python examples/my_method.py # scores track template: writes my_scores/
|
|
97
|
+
gridshedwatch score my_scores --name "My method"
|
|
98
|
+
python examples/my_alerts.py # alerts track template: writes my_alerts.csv
|
|
99
|
+
gridshedwatch score-alerts my_alerts.csv --name "My rule"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Add your method
|
|
103
|
+
|
|
104
|
+
There are two ways in.
|
|
105
|
+
|
|
106
|
+
**Scores track** (the controlled comparison). Your method says, for every grid, every hour and
|
|
107
|
+
each of the five signals, how unusual that signal is now, as a score in [0, 1], using data up to
|
|
108
|
+
that hour only. Write the scores in the submission layout (`my_scores/s<seed>/<grid>.csv`, see
|
|
109
|
+
[gridshedwatch/submission.py](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/submission.py)) and run `gridshedwatch score`. The
|
|
110
|
+
shared rules, the label-free budget step and the scoring are done for you, so your method
|
|
111
|
+
cannot be tuned toward the eight events. Template: [examples/my_method.py](https://github.com/gotham29/gridshedwatch/blob/main/examples/my_method.py).
|
|
112
|
+
|
|
113
|
+
**Alerts track** (any method). If your method decides on its own when to raise a watch or a
|
|
114
|
+
warning (a rule, a classifier, a weather-based model, a human forecaster), list when each alert
|
|
115
|
+
started:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
grid,level,start_utc,seed
|
|
119
|
+
tx,watch,2021-02-11 03:00,0
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
and run `gridshedwatch score-alerts alerts.csv --name "My method"`. It is scored exactly like
|
|
123
|
+
the scores track, at the same budget (median grid at most 12 watches and 2 warnings a year, no
|
|
124
|
+
grid above 3 times that). Format: [gridshedwatch/alerts.py](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/alerts.py); template:
|
|
125
|
+
[examples/my_alerts.py](https://github.com/gotham29/gridshedwatch/blob/main/examples/my_alerts.py); worked examples: the five reference methods'
|
|
126
|
+
alerts in [results/v1.0/alerts/](https://github.com/gotham29/gridshedwatch/tree/main/results/v1.0/alerts/), which reproduce the table above.
|
|
127
|
+
|
|
128
|
+
- Scores track: use the five signals provided (and 2019 for calibration). Alerts track: any data
|
|
129
|
+
available at the time of each alert. Methods that use data beyond the five signals are listed
|
|
130
|
+
separately.
|
|
131
|
+
- If your method is random, submit several seeds; results are averaged.
|
|
132
|
+
- To add a result to the table, open a pull request with your scores or alerts (or a link to
|
|
133
|
+
them), the code that produced them, the output of `--json`, and short answers on the data used
|
|
134
|
+
and, for the alerts track, which events you looked at while developing
|
|
135
|
+
([docs/scoring.md](https://github.com/gotham29/gridshedwatch/blob/main/docs/scoring.md#listing-a-result)). Every result is added, including ones
|
|
136
|
+
that beat the reference methods.
|
|
137
|
+
|
|
138
|
+
## Reproducing v1.0
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
python -m pytest -q # rebuilds the rule and z-score results from the data
|
|
142
|
+
python scripts/fetch_reference.py # reference scores for LSTM, Chronos, HTM (about 100 MB)
|
|
143
|
+
python -m pytest -q # now also checks those three against results/v1.0/
|
|
144
|
+
python scripts/reproduce_v1.py # writes results/v1.0/ from scratch
|
|
145
|
+
docker build -t gridshedwatch . && docker run --rm gridshedwatch
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The LSTM and Chronos can be rerun with `gridshedwatch baseline lstm|chronos -o DIR` (needs
|
|
149
|
+
`pip install -e ".[lstm]"` or `".[chronos]"`). Neural-network training is not bit-identical
|
|
150
|
+
across platforms and library versions, so a rerun gives close but not identical scores; the
|
|
151
|
+
reference scores are the official v1.0 results. HTM's scores come from
|
|
152
|
+
[htm-monitor](https://github.com/gotham29/htm-monitor), scored here like any other submission.
|
|
153
|
+
|
|
154
|
+
## Limits
|
|
155
|
+
|
|
156
|
+
- Eight events, seven from two storms (Uri and Elliott). One storm of a new kind could change
|
|
157
|
+
the picture.
|
|
158
|
+
- Three events were used to design the shared rules; five were held out and scored once.
|
|
159
|
+
- Reference methods see only five signals from EIA-930: no weather, outage or market data. The
|
|
160
|
+
alerts track is open to methods that use more.
|
|
161
|
+
- The status quo row covers operators' public notices; their notices to the industry and their
|
|
162
|
+
false alarms in ordinary years are not yet compiled.
|
|
163
|
+
|
|
164
|
+
GridShedWatch grows as new events happen: each load shed that meets the rule is added in a new
|
|
165
|
+
version and scored with the rules frozen in v1.0.
|
|
166
|
+
|
|
167
|
+
## Citation and license
|
|
168
|
+
|
|
169
|
+
DOI: [10.5281/zenodo.23626223](https://doi.org/10.5281/zenodo.23626223) (archived on Zenodo with every release). See [CITATION.cff](https://github.com/gotham29/gridshedwatch/blob/main/CITATION.cff). Code: MIT. Event list, timelines, documentation and results:
|
|
170
|
+
CC BY 4.0. EIA-930 data: U.S. public domain ([DATA_LICENSE.md](https://github.com/gotham29/gridshedwatch/blob/main/DATA_LICENSE.md)).
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# GridShedWatch
|
|
2
|
+
|
|
3
|
+
[](https://doi.org/10.5281/zenodo.23626223) [](https://github.com/gotham29/gridshedwatch/actions/workflows/tests.yml)
|
|
4
|
+
|
|
5
|
+
**A benchmark for early warning of grid load sheds, scored the way forecasters score weather warnings.**
|
|
6
|
+
|
|
7
|
+
Website: **https://gotham29.github.io/gridshedwatch** (built from this repo by `scripts/build_site.py`).
|
|
8
|
+
|
|
9
|
+
Since 2020, US grid operators have had to cut power for lack of supply eight times. The public
|
|
10
|
+
was warned beforehand in four of them, and in none of the four during Winter Storm Elliott.
|
|
11
|
+
Plenty of methods claim to detect grid trouble early, but there has been no shared test of
|
|
12
|
+
whether a warning would actually have come in time. GridShedWatch is that test: every one of
|
|
13
|
+
those eight load sheds, public hourly data, and one fixed scoring procedure that any method can
|
|
14
|
+
be run through.
|
|
15
|
+
|
|
16
|
+
The National Weather Service scores every warning it issues on three numbers read together:
|
|
17
|
+
how many events were warned in advance, how much lead time people got, and how often warnings
|
|
18
|
+
were false alarms. GridShedWatch asks the same of grid warnings, at two levels: a **watch**
|
|
19
|
+
(about one a month per grid: something unusual is building) and a **warning** (two a year: act).
|
|
20
|
+
|
|
21
|
+
## What's in it
|
|
22
|
+
|
|
23
|
+
| | |
|
|
24
|
+
|---|---|
|
|
25
|
+
| **Events** | 8 supply-shortfall load sheds of at least 250 MW, Jan 2020 to Sep 2026, chosen by a stated rule from NERC's annual lists ([docs/events.md](https://github.com/gotham29/gridshedwatch/blob/main/docs/events.md)) |
|
|
26
|
+
| **Data** | A frozen snapshot of EIA-930 hourly demand, day-ahead forecast and net generation for the 8 grids, 2019-2026, and the five signals built from it ([gridshedwatch/snapshot_v1.0/](https://github.com/gotham29/gridshedwatch/tree/main/gridshedwatch/snapshot_v1.0/), [gridshedwatch/data.py](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/data.py)) |
|
|
27
|
+
| **Rules** | One shared rule that turns per-signal scores into watches and warnings, so methods differ only in how they score each hour ([gridshedwatch/rules.py](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/rules.py)) |
|
|
28
|
+
| **Scoring** | Same alert budget for every method, chosen without looking at the events; events caught, lead time and false alarm ratio, with bootstrap intervals and a chance baseline ([docs/scoring.md](https://github.com/gotham29/gridshedwatch/blob/main/docs/scoring.md)) |
|
|
29
|
+
| **Status quo** | Every operator notice in the week before each event, with times and sources ([gridshedwatch/operator_notices.yaml](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/operator_notices.yaml)) |
|
|
30
|
+
| **Reference methods** | An operator-style rule, a seasonal z-score, an LSTM forecaster, Chronos-Bolt (zero-shot), and HTM ([gridshedwatch/baselines/](https://github.com/gotham29/gridshedwatch/tree/main/gridshedwatch/baselines/)) |
|
|
31
|
+
| **Datasheet** | Motivation, composition, collection, intended uses, known biases and maintenance ([docs/datasheet.md](https://github.com/gotham29/gridshedwatch/blob/main/docs/datasheet.md)) |
|
|
32
|
+
|
|
33
|
+
| Event | Grid | Shed at its worst | Split |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| Heat wave, Aug 2020 | California (CAISO) | about 1,000 MW | development |
|
|
36
|
+
| Winter Storm Uri, Feb 2021 | Texas (ERCOT) | 20,000 MW | development |
|
|
37
|
+
| Uri | Southwest Power Pool | 2,700 MW | held-out |
|
|
38
|
+
| Uri | MISO (MISO South) | 700 MW | held-out |
|
|
39
|
+
| Winter Storm Elliott, Dec 2022 | Tennessee Valley Authority | 3,000 MW | development |
|
|
40
|
+
| Elliott | Duke Energy Carolinas | up to 1,000 MW | held-out |
|
|
41
|
+
| Elliott | Duke Energy Progress | about 960 MW | held-out |
|
|
42
|
+
| Elliott | LG&E and KU (Kentucky) | about 300 MW | held-out |
|
|
43
|
+
|
|
44
|
+
## Results, v1.0
|
|
45
|
+
|
|
46
|
+
<!-- leaderboard:start -->
|
|
47
|
+
| Method | Watches: caught (of 8) | held-out (of 5) | by chance | median lead (h) | Warnings: caught (of 8) | held-out (of 5) | by chance | False alarm ratio (watch / warning) |
|
|
48
|
+
|---|---|---|---|---|---|---|---|---|
|
|
49
|
+
| Operators (public notices) | 4 | 2 | - | 20 | - | - | - | - |
|
|
50
|
+
| Operator-style rule | 5 [3.0-7.0] | 4 | 1.2 | 85 | 2 [0.0-4.0] | 1 | 0.2 | 98% / 94% |
|
|
51
|
+
| Seasonal z-score | 5 [3.0-7.0] | 4 | 1.1 | 105 | 6 [4.0-8.0] | 4 | 0.2 | 98% / 92% |
|
|
52
|
+
| Chronos-Bolt (zero-shot) | 1 [0.0-3.0] | 1 | 0.9 | 9 | 3 [1.0-5.0] | 1 | 0.3 | 100% / 96% |
|
|
53
|
+
| LSTM forecaster | 6.4 [4.6-8.0] | 4.4 | 1.3 | 8 | 4.8 [2.8-6.8] | 2.8 | 0.3 | 99% / 94% |
|
|
54
|
+
| HTM (htm-monitor) | 6 [4.6-7.2] | 4 | 1.4 | 56 | 5.6 [3.8-7.2] | 3.6 | 0.3 | 98% / 93% |
|
|
55
|
+
|
|
56
|
+
All five reference methods are in the scores track; no alerts-track entries yet. Mean over 5 seeds for the LSTM and HTM. [a-b]: 5th-95th percentile with events resampled. Operators: public notices in the 7 days before onset, scored as watches.
|
|
57
|
+
<!-- leaderboard:end -->
|
|
58
|
+
|
|
59
|
+
Every method except Chronos's watches catches far more than chance, and none leads at both
|
|
60
|
+
levels. The differences are by kind of emergency: the simple rules caught all three Uri events
|
|
61
|
+
days ahead, the LSTM and HTM caught the Elliott events hours ahead (when the public got no
|
|
62
|
+
warning), and only the operators warned California. With eight events, differences of about one
|
|
63
|
+
event are not meaningful. Per-event results: `results/v1.0/<method>_events.csv`.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install gridshedwatch # or, to work on the code:
|
|
69
|
+
git clone https://github.com/gotham29/gridshedwatch && cd gridshedwatch
|
|
70
|
+
pip install -e ".[test]"
|
|
71
|
+
gridshedwatch info # the eight events and their timelines
|
|
72
|
+
gridshedwatch score --oprule # score the built-in operator-style rule
|
|
73
|
+
python examples/my_method.py # scores track template: writes my_scores/
|
|
74
|
+
gridshedwatch score my_scores --name "My method"
|
|
75
|
+
python examples/my_alerts.py # alerts track template: writes my_alerts.csv
|
|
76
|
+
gridshedwatch score-alerts my_alerts.csv --name "My rule"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Add your method
|
|
80
|
+
|
|
81
|
+
There are two ways in.
|
|
82
|
+
|
|
83
|
+
**Scores track** (the controlled comparison). Your method says, for every grid, every hour and
|
|
84
|
+
each of the five signals, how unusual that signal is now, as a score in [0, 1], using data up to
|
|
85
|
+
that hour only. Write the scores in the submission layout (`my_scores/s<seed>/<grid>.csv`, see
|
|
86
|
+
[gridshedwatch/submission.py](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/submission.py)) and run `gridshedwatch score`. The
|
|
87
|
+
shared rules, the label-free budget step and the scoring are done for you, so your method
|
|
88
|
+
cannot be tuned toward the eight events. Template: [examples/my_method.py](https://github.com/gotham29/gridshedwatch/blob/main/examples/my_method.py).
|
|
89
|
+
|
|
90
|
+
**Alerts track** (any method). If your method decides on its own when to raise a watch or a
|
|
91
|
+
warning (a rule, a classifier, a weather-based model, a human forecaster), list when each alert
|
|
92
|
+
started:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
grid,level,start_utc,seed
|
|
96
|
+
tx,watch,2021-02-11 03:00,0
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
and run `gridshedwatch score-alerts alerts.csv --name "My method"`. It is scored exactly like
|
|
100
|
+
the scores track, at the same budget (median grid at most 12 watches and 2 warnings a year, no
|
|
101
|
+
grid above 3 times that). Format: [gridshedwatch/alerts.py](https://github.com/gotham29/gridshedwatch/blob/main/gridshedwatch/alerts.py); template:
|
|
102
|
+
[examples/my_alerts.py](https://github.com/gotham29/gridshedwatch/blob/main/examples/my_alerts.py); worked examples: the five reference methods'
|
|
103
|
+
alerts in [results/v1.0/alerts/](https://github.com/gotham29/gridshedwatch/tree/main/results/v1.0/alerts/), which reproduce the table above.
|
|
104
|
+
|
|
105
|
+
- Scores track: use the five signals provided (and 2019 for calibration). Alerts track: any data
|
|
106
|
+
available at the time of each alert. Methods that use data beyond the five signals are listed
|
|
107
|
+
separately.
|
|
108
|
+
- If your method is random, submit several seeds; results are averaged.
|
|
109
|
+
- To add a result to the table, open a pull request with your scores or alerts (or a link to
|
|
110
|
+
them), the code that produced them, the output of `--json`, and short answers on the data used
|
|
111
|
+
and, for the alerts track, which events you looked at while developing
|
|
112
|
+
([docs/scoring.md](https://github.com/gotham29/gridshedwatch/blob/main/docs/scoring.md#listing-a-result)). Every result is added, including ones
|
|
113
|
+
that beat the reference methods.
|
|
114
|
+
|
|
115
|
+
## Reproducing v1.0
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python -m pytest -q # rebuilds the rule and z-score results from the data
|
|
119
|
+
python scripts/fetch_reference.py # reference scores for LSTM, Chronos, HTM (about 100 MB)
|
|
120
|
+
python -m pytest -q # now also checks those three against results/v1.0/
|
|
121
|
+
python scripts/reproduce_v1.py # writes results/v1.0/ from scratch
|
|
122
|
+
docker build -t gridshedwatch . && docker run --rm gridshedwatch
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The LSTM and Chronos can be rerun with `gridshedwatch baseline lstm|chronos -o DIR` (needs
|
|
126
|
+
`pip install -e ".[lstm]"` or `".[chronos]"`). Neural-network training is not bit-identical
|
|
127
|
+
across platforms and library versions, so a rerun gives close but not identical scores; the
|
|
128
|
+
reference scores are the official v1.0 results. HTM's scores come from
|
|
129
|
+
[htm-monitor](https://github.com/gotham29/htm-monitor), scored here like any other submission.
|
|
130
|
+
|
|
131
|
+
## Limits
|
|
132
|
+
|
|
133
|
+
- Eight events, seven from two storms (Uri and Elliott). One storm of a new kind could change
|
|
134
|
+
the picture.
|
|
135
|
+
- Three events were used to design the shared rules; five were held out and scored once.
|
|
136
|
+
- Reference methods see only five signals from EIA-930: no weather, outage or market data. The
|
|
137
|
+
alerts track is open to methods that use more.
|
|
138
|
+
- The status quo row covers operators' public notices; their notices to the industry and their
|
|
139
|
+
false alarms in ordinary years are not yet compiled.
|
|
140
|
+
|
|
141
|
+
GridShedWatch grows as new events happen: each load shed that meets the rule is added in a new
|
|
142
|
+
version and scored with the rules frozen in v1.0.
|
|
143
|
+
|
|
144
|
+
## Citation and license
|
|
145
|
+
|
|
146
|
+
DOI: [10.5281/zenodo.23626223](https://doi.org/10.5281/zenodo.23626223) (archived on Zenodo with every release). See [CITATION.cff](https://github.com/gotham29/gridshedwatch/blob/main/CITATION.cff). Code: MIT. Event list, timelines, documentation and results:
|
|
147
|
+
CC BY 4.0. EIA-930 data: U.S. public domain ([DATA_LICENSE.md](https://github.com/gotham29/gridshedwatch/blob/main/DATA_LICENSE.md)).
|
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Alerts track: read, check and write a method's watch and warning start times.
|
|
2
|
+
|
|
3
|
+
File format (CSV), one row per alert episode:
|
|
4
|
+
grid,level,start_utc[,seed]
|
|
5
|
+
tx,watch,2021-02-11 03:00,0
|
|
6
|
+
grid: one of gridshedwatch.data.grids(); level: watch or warning; start_utc: when the alert
|
|
7
|
+
episode began, UTC (ISO format; minutes allowed); seed: optional, for methods with randomness
|
|
8
|
+
(default 0). Starts must fall in the scored period: after the first week of the run (warm-up)
|
|
9
|
+
and before the end of the run. A grid with no rows has no alerts.
|
|
10
|
+
|
|
11
|
+
An episode is one alert, however long it lasts: if your method raises an alert and keeps it on,
|
|
12
|
+
list the time it came on. Alerts must be issued using data up to that time only.
|
|
13
|
+
"""
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
from . import data
|
|
19
|
+
|
|
20
|
+
LEVELS = ("watch", "warning")
|
|
21
|
+
COLUMNS = ["grid", "level", "start_utc"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def scored_period(grid: str) -> tuple:
|
|
25
|
+
tb = data.timebase(grid)
|
|
26
|
+
s = tb.loc[tb.in_warmup == 0, "timestamp"]
|
|
27
|
+
return s.iloc[0], s.iloc[-1]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check(df: pd.DataFrame) -> pd.DataFrame:
|
|
31
|
+
missing = [c for c in COLUMNS if c not in df]
|
|
32
|
+
if missing:
|
|
33
|
+
raise ValueError(f"missing columns {missing}; need grid, level, start_utc (and optionally seed)")
|
|
34
|
+
df = df.copy()
|
|
35
|
+
if "seed" not in df:
|
|
36
|
+
df["seed"] = 0
|
|
37
|
+
bad = sorted(set(df.grid) - set(data.grids()))
|
|
38
|
+
if bad:
|
|
39
|
+
raise ValueError(f"unknown grids {bad}; use {data.grids()}")
|
|
40
|
+
bad = sorted(set(df.level) - set(LEVELS))
|
|
41
|
+
if bad:
|
|
42
|
+
raise ValueError(f"unknown levels {bad}; use watch or warning")
|
|
43
|
+
try:
|
|
44
|
+
df["start_utc"] = pd.to_datetime(df.start_utc, utc=True).dt.tz_localize(None)
|
|
45
|
+
except (ValueError, TypeError) as e:
|
|
46
|
+
raise ValueError(f"start_utc must be UTC timestamps: {e}") from None
|
|
47
|
+
df["seed"] = df.seed.astype(int)
|
|
48
|
+
for g, x in df.groupby("grid"):
|
|
49
|
+
lo, hi = scored_period(g)
|
|
50
|
+
out = x[(x.start_utc < lo) | (x.start_utc > hi)]
|
|
51
|
+
if len(out):
|
|
52
|
+
raise ValueError(f"{g}: {len(out)} starts outside the scored period {lo} to {hi}, e.g. {out.start_utc.iloc[0]}")
|
|
53
|
+
dup = df.duplicated(["seed", "grid", "level", "start_utc"])
|
|
54
|
+
if dup.any():
|
|
55
|
+
raise ValueError(f"{dup.sum()} duplicate rows (same seed, grid, level and start)")
|
|
56
|
+
return df
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load(path) -> dict:
|
|
60
|
+
"""{seed: {grid: {level: [Timestamp, ...]}}}"""
|
|
61
|
+
df = check(pd.read_csv(path))
|
|
62
|
+
out = {}
|
|
63
|
+
for (k, g, lv), x in df.groupby(["seed", "grid", "level"]):
|
|
64
|
+
out.setdefault(int(k), {}).setdefault(g, {})[lv] = sorted(x.start_utc)
|
|
65
|
+
return out or {0: {}}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def from_rows(rows: pd.DataFrame) -> pd.DataFrame:
|
|
69
|
+
"""Alert file rows from a scored Result's rows (to export a scores-track method's alerts)."""
|
|
70
|
+
recs = [dict(grid=r.grid, level=r.level, start_utc=pd.Timestamp(t).strftime("%Y-%m-%d %H:%M"), seed=int(r.seed))
|
|
71
|
+
for r in rows.itertuples() for t in str(r.starts).split("|") if t and t != "nan"]
|
|
72
|
+
return pd.DataFrame(recs, columns=COLUMNS + ["seed"]).sort_values(["seed", "grid", "level", "start_utc"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write(df: pd.DataFrame, path) -> None:
|
|
76
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
df.to_csv(path, index=False)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Reference methods. Each turns the five signals into per-signal scores p in [0, 1], except the
|
|
2
|
+
operator-style rule, which raises alerts directly from demand and its day-ahead forecast."""
|
|
3
|
+
import bisect
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_p(score: np.ndarray) -> np.ndarray:
|
|
9
|
+
"""Causal empirical CDF: share of strictly earlier scores that are <= the current one."""
|
|
10
|
+
seen, p = [], np.zeros(len(score))
|
|
11
|
+
for t, s in enumerate(score):
|
|
12
|
+
p[t] = bisect.bisect_right(seen, s) / len(seen) if seen else 0.0
|
|
13
|
+
bisect.insort(seen, s)
|
|
14
|
+
return p
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Chronos-Bolt, zero-shot (Ansari et al., 2024). Needs: pip install gridshedwatch[chronos].
|
|
2
|
+
|
|
3
|
+
Per grid and signal, the pretrained model forecasts the next hour from the previous 512 (the 2019
|
|
4
|
+
tail supplies the context for the first run hours); score = |actual - median forecast|. No
|
|
5
|
+
training or tuning, so it is deterministic. Caveat: the pretraining corpus includes public
|
|
6
|
+
electricity data, so overlap with 2020-2026 EIA-930 cannot be ruled out.
|
|
7
|
+
"""
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from .. import data
|
|
12
|
+
from ..config import SIGNALS
|
|
13
|
+
from . import to_p
|
|
14
|
+
|
|
15
|
+
CTX = 512
|
|
16
|
+
MODEL = "amazon/chronos-bolt-small"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def scores(grid: str, model: str = MODEL, device: str = "cpu", batch: int = 1024) -> pd.DataFrame:
|
|
20
|
+
try:
|
|
21
|
+
import torch
|
|
22
|
+
from chronos import BaseChronosPipeline
|
|
23
|
+
except ImportError as e: # pragma: no cover
|
|
24
|
+
raise ImportError("the Chronos baseline needs: pip install gridshedwatch[chronos]") from e
|
|
25
|
+
pipe = BaseChronosPipeline.from_pretrained(model, device_map=device, torch_dtype=torch.float32)
|
|
26
|
+
X = data.signals(grid)
|
|
27
|
+
cal_all = data.calibration(grid)
|
|
28
|
+
out = {}
|
|
29
|
+
for f in SIGNALS:
|
|
30
|
+
cal = cal_all[f].dropna().to_numpy(float)
|
|
31
|
+
run = np.nan_to_num(X[f].to_numpy(float), nan=float(np.nanmedian(cal)))
|
|
32
|
+
full = np.concatenate([cal[-CTX:], run]).astype(np.float32)
|
|
33
|
+
W = np.lib.stride_tricks.sliding_window_view(full, CTX + 1)
|
|
34
|
+
raw = np.empty(len(W), dtype=np.float32)
|
|
35
|
+
for a in range(0, len(W), batch):
|
|
36
|
+
q, _ = pipe.predict_quantiles(torch.tensor(W[a:a + batch, :CTX]), prediction_length=1, quantile_levels=[0.5])
|
|
37
|
+
raw[a:a + batch] = np.abs(W[a:a + batch, CTX] - q[:, 0, 0].numpy())
|
|
38
|
+
out[f"p_{f}"] = np.round(to_p(raw), 6)
|
|
39
|
+
return pd.DataFrame(out)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""LSTM forecaster (LSTM-AD, Malhotra et al., ESANN 2015). Needs: pip install gridshedwatch[lstm].
|
|
2
|
+
|
|
3
|
+
Per grid and signal, a 2-layer, 64-unit LSTM reads the previous 24 hours and predicts the next;
|
|
4
|
+
the score is the absolute error. Signals are scaled by their 2019 0.1-99.9th percentile range.
|
|
5
|
+
Trained on 2019 only (Adam 1e-3, batch 64, up to 50 epochs, early stopping on the last 10% of
|
|
6
|
+
2019), then frozen. Seed k uses torch seed 42 + 1000 k. Settings are common defaults, not tuned
|
|
7
|
+
on events. Expect about an hour per seed on a laptop CPU.
|
|
8
|
+
"""
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from .. import data
|
|
13
|
+
from ..config import SIGNALS
|
|
14
|
+
from . import to_p
|
|
15
|
+
|
|
16
|
+
L = 24
|
|
17
|
+
SEED_BASE = 42
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _torch():
|
|
21
|
+
try:
|
|
22
|
+
import torch
|
|
23
|
+
from torch import nn
|
|
24
|
+
except ImportError as e: # pragma: no cover
|
|
25
|
+
raise ImportError("the LSTM baseline needs torch: pip install gridshedwatch[lstm]") from e
|
|
26
|
+
return torch, nn
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def scores(grid: str, seed: int = 0, threads: int = 1) -> pd.DataFrame:
|
|
30
|
+
torch, nn = _torch()
|
|
31
|
+
torch.set_num_threads(threads)
|
|
32
|
+
|
|
33
|
+
class Net(nn.Module):
|
|
34
|
+
def __init__(self, hidden=64, layers=2):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.lstm = nn.LSTM(1, hidden, num_layers=layers, batch_first=True)
|
|
37
|
+
self.head = nn.Linear(hidden, 1)
|
|
38
|
+
|
|
39
|
+
def forward(self, x):
|
|
40
|
+
out, _ = self.lstm(x)
|
|
41
|
+
return self.head(out[:, -1]).squeeze(-1)
|
|
42
|
+
|
|
43
|
+
def windows(x):
|
|
44
|
+
W = np.lib.stride_tricks.sliding_window_view(x, L + 1)
|
|
45
|
+
return torch.tensor(W[:, :L, None], dtype=torch.float32), torch.tensor(W[:, L], dtype=torch.float32)
|
|
46
|
+
|
|
47
|
+
def fit(model, X, y, s, max_epochs=50, patience=5):
|
|
48
|
+
torch.manual_seed(s)
|
|
49
|
+
n_val = max(1, len(X) // 10)
|
|
50
|
+
Xtr, ytr, Xva, yva = X[:-n_val], y[:-n_val], X[-n_val:], y[-n_val:]
|
|
51
|
+
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
|
|
52
|
+
best, state, bad = np.inf, None, 0
|
|
53
|
+
g = torch.Generator().manual_seed(s)
|
|
54
|
+
for _ in range(max_epochs):
|
|
55
|
+
model.train()
|
|
56
|
+
for idx in torch.randperm(len(Xtr), generator=g).split(64):
|
|
57
|
+
opt.zero_grad()
|
|
58
|
+
nn.functional.mse_loss(model(Xtr[idx]), ytr[idx]).backward()
|
|
59
|
+
opt.step()
|
|
60
|
+
model.eval()
|
|
61
|
+
with torch.no_grad():
|
|
62
|
+
v = nn.functional.mse_loss(model(Xva), yva).item()
|
|
63
|
+
if v < best - 1e-6:
|
|
64
|
+
best, state, bad = v, {k: t.clone() for k, t in model.state_dict().items()}, 0
|
|
65
|
+
else:
|
|
66
|
+
bad += 1
|
|
67
|
+
if bad >= patience:
|
|
68
|
+
break
|
|
69
|
+
model.load_state_dict(state)
|
|
70
|
+
return model
|
|
71
|
+
|
|
72
|
+
X = data.signals(grid)
|
|
73
|
+
cal_all = data.calibration(grid)
|
|
74
|
+
b = data.bounds_2019(grid)
|
|
75
|
+
out = {}
|
|
76
|
+
for f in SIGNALS:
|
|
77
|
+
lo, hi = b[f]
|
|
78
|
+
cal = (cal_all[f].dropna().to_numpy(float) - lo) / (hi - lo)
|
|
79
|
+
run = np.nan_to_num((X[f].to_numpy(float) - lo) / (hi - lo), nan=0.0)
|
|
80
|
+
net = fit(Net(), *windows(cal), SEED_BASE + 1000 * seed)
|
|
81
|
+
full = np.concatenate([cal, run])
|
|
82
|
+
Xw, yw = windows(full[len(cal) - L:])
|
|
83
|
+
net.eval()
|
|
84
|
+
with torch.no_grad():
|
|
85
|
+
pred = torch.cat([net(x) for x in Xw.split(4096)])
|
|
86
|
+
raw = (pred - yw).abs().numpy()
|
|
87
|
+
out[f"p_{f}"] = np.round(to_p(raw), 6)
|
|
88
|
+
return pd.DataFrame(out)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Operator-style rule: no per-signal model and no groups. An hour is hot when demand runs above
|
|
2
|
+
the day-ahead forecast by more than the trailing year's q-th percentile of percent forecast error
|
|
3
|
+
(q = the benchmark threshold); an alert is on after `persistence` hot hours in a row."""
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .. import data
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@lru_cache(maxsize=None)
|
|
13
|
+
def hot(grid: str, q: float) -> np.ndarray:
|
|
14
|
+
w = data.clean_eia930(grid)
|
|
15
|
+
pct = ((w["D"] - w["DF"]) / w["DF"].where(w["DF"] > 0) * 100).asfreq("h")
|
|
16
|
+
thr = pct.shift(1).rolling(8760, min_periods=24 * 90).quantile(q)
|
|
17
|
+
ts = data.timebase(grid)["timestamp"]
|
|
18
|
+
p, th = pct.reindex(ts).to_numpy(), thr.reindex(ts).to_numpy()
|
|
19
|
+
return np.nan_to_num(p, nan=-np.inf) > np.nan_to_num(th, nan=np.inf)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def alerts(grid: str, level: str, threshold: float, persistence: int, seed: int = 0) -> np.ndarray:
|
|
23
|
+
h = hot(grid, threshold)
|
|
24
|
+
warm = data.timebase(grid)["in_warmup"].to_numpy()
|
|
25
|
+
a, run = np.zeros(len(h), dtype=int), 0
|
|
26
|
+
for i, x in enumerate(h):
|
|
27
|
+
if warm[i]:
|
|
28
|
+
run = 0
|
|
29
|
+
continue
|
|
30
|
+
run = run + 1 if x else 0
|
|
31
|
+
a[i] = int(run >= persistence)
|
|
32
|
+
return a
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Seasonal z-score: how far each signal sits from the median of its past 30 days, in robust
|
|
2
|
+
standard deviations (median absolute deviation x 1.4826). Deterministic."""
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from .. import data
|
|
7
|
+
from ..config import SIGNALS
|
|
8
|
+
from . import to_p
|
|
9
|
+
|
|
10
|
+
WINDOW_H = 720
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def robust_z(x: np.ndarray, W: int = WINDOW_H) -> np.ndarray:
|
|
14
|
+
s = pd.Series(x)
|
|
15
|
+
past = s.shift(1)
|
|
16
|
+
med = past.rolling(W, min_periods=24).median()
|
|
17
|
+
mad = (past - med).abs().rolling(W, min_periods=24).median()
|
|
18
|
+
z = (s - med).abs() / (1.4826 * mad.replace(0, np.nan))
|
|
19
|
+
return z.fillna(0).to_numpy()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def scores(grid: str) -> pd.DataFrame:
|
|
23
|
+
X = data.signals(grid)
|
|
24
|
+
return pd.DataFrame({f"p_{f}": to_p(robust_z(X[f].to_numpy(float))) for f in SIGNALS})
|