mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-11-13 09:13:30 +01:00
138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
"""Group/horizon selection on genuinely issued, independent-day predictions."""
|
|||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pandas as pd
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from akkudoktoreos.prediction.pvakkuadaptquality import (
|
||
|
|
choose_correction,
|
||
|
|
select_corrections,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def archive():
|
||
|
|
rows, index = [], []
|
||
|
|
for day in pd.date_range("2026-09-01T08:00Z", periods=4, freq="D"):
|
||
|
|
for slot in range(12):
|
||
|
|
time = day + pd.Timedelta(minutes=slot * 15)
|
||
|
|
index.append(time)
|
||
|
|
for lead in (7, 52):
|
||
|
|
rows.append(
|
||
|
|
dict(
|
||
|
|
target_ms=int(time.timestamp() * 1000),
|
||
|
|
issue_ms=int((time - pd.Timedelta(minutes=lead)).timestamp() * 1000),
|
||
|
|
lead_minutes=lead,
|
||
|
|
groups={
|
||
|
|
"roof": {
|
||
|
|
"calibrated": 1000,
|
||
|
|
"local": 800 if lead == 7 else 1100,
|
||
|
|
"trend": 700 if lead == 7 else 1200,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
)
|
||
|
|
)
|
||
|
|
observed = pd.DataFrame(
|
||
|
|
dict(
|
||
|
|
renamed_sensor=700,
|
||
|
|
renamed_sensor_eligible=True,
|
||
|
|
renamed_sensor_available=pd.DatetimeIndex(index) + pd.Timedelta(minutes=20),
|
||
|
|
),
|
||
|
|
index=pd.DatetimeIndex(index),
|
||
|
|
)
|
||
|
|
return rows, observed
|
||
|
|
|
||
|
|
|
||
|
|
def test_selects_per_group_and_horizon_with_no_site_names():
|
||
|
|
rows, observed = archive()
|
||
|
|
result = select_corrections(
|
||
|
|
rows, observed, {"roof": ["renamed_sensor"]}, pd.Timestamp("2026-09-06T00:00Z"), "UTC"
|
||
|
|
)
|
||
|
|
assert result["roof"]["15"]["method"] == "trend"
|
||
|
|
assert result["roof"]["60"]["method"] == "calibrated"
|
||
|
|
assert result["roof"]["15"]["days"] == 4
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("problem", ["future_labels", "curtailed", "missing", "today"])
|
||
|
|
def test_missing_censored_or_unavailable_truth_cannot_select_complex_model(problem):
|
||
|
|
rows, observed = archive()
|
||
|
|
cutoff = pd.Timestamp("2026-09-06T00:00Z")
|
||
|
|
if problem == "future_labels":
|
||
|
|
observed.renamed_sensor_available = cutoff + pd.Timedelta(minutes=1)
|
||
|
|
if problem == "curtailed":
|
||
|
|
observed.renamed_sensor_eligible = False
|
||
|
|
if problem == "missing":
|
||
|
|
observed.renamed_sensor = np.nan
|
||
|
|
if problem == "today":
|
||
|
|
cutoff = pd.Timestamp("2026-09-01T12:00Z")
|
||
|
|
assert (
|
||
|
|
select_corrections(rows, observed, {"roof": ["renamed_sensor"]}, cutoff, "UTC")["roof"]
|
||
|
|
== {}
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_repeated_polling_cannot_manufacture_sample_size():
|
||
|
|
rows, observed = archive()
|
||
|
|
rows = [r for r in rows if r["target_ms"] == rows[0]["target_ms"]] * 1000
|
||
|
|
result = select_corrections(
|
||
|
|
rows, observed, {"roof": ["renamed_sensor"]}, pd.Timestamp("2026-09-06T00:00Z"), "UTC"
|
||
|
|
)
|
||
|
|
assert result["roof"]["15"]["samples"] == 1
|
||
|
|
assert result["roof"]["15"]["method"] == "calibrated"
|
||
|
|
|
||
|
|
|
||
|
|
def test_unknown_group_and_insufficient_history_preserve_calibration():
|
||
|
|
rows, observed = archive()
|
||
|
|
result = select_corrections(
|
||
|
|
rows,
|
||
|
|
observed,
|
||
|
|
{"different_roof": ["renamed_sensor"]},
|
||
|
|
pd.Timestamp("2026-09-06T00:00Z"),
|
||
|
|
"UTC",
|
||
|
|
)
|
||
|
|
assert result["different_roof"] == {}
|
||
|
|
|
||
|
|
|
||
|
|
def test_choice_expires_after_two_hours_and_cold_start_is_simple():
|
||
|
|
issue = pd.Timestamp("2026-09-06T08:00Z")
|
||
|
|
idx = pd.date_range(issue + pd.Timedelta(minutes=15), periods=12, freq="15min")
|
||
|
|
base = pd.Series(1000.0, index=idx)
|
||
|
|
local = base * 0.8
|
||
|
|
trend = base * 0.7
|
||
|
|
selected = choose_correction(
|
||
|
|
base, local, trend, issue, {"15": {"method": "trend"}, "60": {"method": "local"}}
|
||
|
|
)
|
||
|
|
assert selected.iloc[0] == 700
|
||
|
|
assert selected.iloc[3] == 800
|
||
|
|
assert selected.iloc[-1] == 1000
|
||
|
|
pd.testing.assert_series_equal(choose_correction(base, local, trend, issue, {}), base)
|
||
|
|
|
||
|
|
|
||
|
|
def test_legacy_archive_can_only_validate_explicit_reference_sum():
|
||
|
|
rows, observed = archive()
|
||
|
|
for r in rows:
|
||
|
|
r.pop("groups")
|
||
|
|
r.update(reference_calibrated_w=1000, reference_local_w=800, reference_adaptive_w=700)
|
||
|
|
result = select_corrections(
|
||
|
|
rows,
|
||
|
|
observed,
|
||
|
|
{"roof": ["renamed_sensor"], "other": ["renamed_sensor"]},
|
||
|
|
pd.Timestamp("2026-09-06T00:00Z"),
|
||
|
|
"UTC",
|
||
|
|
reference_group="roof",
|
||
|
|
)
|
||
|
|
assert result["roof"]["15"]["method"] == "trend"
|
||
|
|
assert result["other"] == {}
|
||
|
|
|
||
|
|
|
||
|
|
def test_disabled_trend_candidate_cannot_be_selected():
|
||
|
|
rows, observed = archive()
|
||
|
|
result = select_corrections(
|
||
|
|
rows,
|
||
|
|
observed,
|
||
|
|
{"roof": ["renamed_sensor"]},
|
||
|
|
pd.Timestamp("2026-09-06T00:00Z"),
|
||
|
|
"UTC",
|
||
|
|
enabled_methods=("calibrated", "local"),
|
||
|
|
)
|
||
|
|
assert result["roof"]["15"]["method"] == "local"
|