Skip to content

Commit a2960a0

Browse files
CIclaude
andcommitted
fix(solar): use a true median for the Open-Meteo ensemble ratios
With an even number of usable members the nearest-rank pick took the lower of the two middle values, skewing both the P10 and P90 ratios. Take the mean of the two instead, and cover the even-member case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent cc6f580 commit a2960a0

2 files changed

Lines changed: 13 additions & 5 deletions

File tree

‎apps/predbat/solcast.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ async def download_open_meteo_ensemble_data(self, lat, lon, tilt, az):
306306
continue
307307
values.sort()
308308
gti_p10 = values[max(0, math.ceil(len(values) * 0.10) - 1)]
309-
gti_p50 = values[max(0, math.ceil(len(values) * 0.50) - 1)]
309+
# A true median: with an even number of usable members there is no middle one, so
310+
# take the mean of the two either side rather than the lower of them
311+
middle = len(values) // 2
312+
gti_p50 = values[middle] if len(values) % 2 else 0.5 * (values[middle - 1] + values[middle])
310313
gti_p90 = values[max(0, math.ceil(len(values) * 0.90) - 1)]
311314
if gti_p50 <= 0:
312315
continue

‎apps/predbat/tests/test_open_meteo.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ def test_ensemble_returns_band_ratios(my_predbat):
6969
try:
7070
test_api.solar.open_meteo_forecast_max_age = 1.0
7171
ensemble_data = _make_ensemble_response(
72-
times=["2025-06-15T04:00", "2025-06-15T05:00", "2025-06-15T12:00"],
72+
times=["2025-06-15T04:00", "2025-06-15T05:00", "2025-06-15T12:00", "2025-06-15T13:00"],
7373
members={
74-
"global_tilted_irradiance_member01": [0.0, 1.0, 400.0],
75-
"global_tilted_irradiance_member02": [0.0, 2.0, 450.0],
76-
"global_tilted_irradiance_member03": [5.0, 40.0, 480.0],
74+
"global_tilted_irradiance_member01": [0.0, 1.0, 400.0, 300.0],
75+
"global_tilted_irradiance_member02": [0.0, 2.0, 450.0, None],
76+
"global_tilted_irradiance_member03": [5.0, 40.0, 480.0, 500.0],
7777
},
7878
)
7979
test_api.set_mock_response("ensemble-api.open-meteo.com", ensemble_data)
@@ -101,6 +101,11 @@ def create_mock_session(*args, **kwargs):
101101
if result.get("2025-06-15T05:00") != (0.5, 2.0):
102102
print(f"ERROR: ensemble ratios at 05:00: expected (0.5, 2.0) with the P90 ratio capped, got {result.get('2025-06-15T05:00')}")
103103
failed = True
104+
# 13:00 -> [300, 500] with one member missing: an even count has no middle member, so the
105+
# median is the mean of the two middle values (400), not the lower of them
106+
if result.get("2025-06-15T13:00") != (0.75, 1.25):
107+
print(f"ERROR: ensemble ratios at 13:00: expected (0.75, 1.25) against a true median of 400, got {result.get('2025-06-15T13:00')}")
108+
failed = True
104109
finally:
105110
test_api.cleanup()
106111

0 commit comments

Comments
 (0)