tune forecast correction parametersw
This commit is contained in:
12
.cursor/rules/postgres-sql-drop-comment.mdc
Normal file
12
.cursor/rules/postgres-sql-drop-comment.mdc
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
description: Postgres DROP/COMMENT ON FUNCTION bez seznamu argumentů (jedna funkce na jméno)
|
||||||
|
globs: db/**/*.sql
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Postgres: `DROP FUNCTION` a `COMMENT ON FUNCTION` bez parametrů
|
||||||
|
|
||||||
|
- U **`DROP FUNCTION`** (včetně schématu, např. `ems.fn_pv_forecast_delta_profile`) **nemusíme** uvádět signaturu argumentů, pokud platí předpoklad: **v DB existuje jen jedna funkce tohoto plného jména** (žádný jiný overload se stejným jménem).
|
||||||
|
- Stejně u **`COMMENT ON FUNCTION`** používej **`COMMENT ON FUNCTION ems.nazev_funkce IS '...'`** bez seznamu typů argumentů — za stejného předpokladu jedné funkce na jméno.
|
||||||
|
|
||||||
|
**Důsledek:** při zavádění overloadů se stejným názvem je nutné signatury zase explicitně rozlišit, nebo přejmenovat / sloučit funkce tak, aby jméno bylo jednoznačné.
|
||||||
@@ -560,6 +560,24 @@ async def get_site_forecast_pv_slots_range_corrected(
|
|||||||
le=10_000,
|
le=10_000,
|
||||||
description="Ignorovat sloty s nízkou výrobou (W) při odhadu profilu",
|
description="Ignorovat sloty s nízkou výrobou (W) při odhadu profilu",
|
||||||
),
|
),
|
||||||
|
top_n_days: int | None = Query(
|
||||||
|
None,
|
||||||
|
ge=1,
|
||||||
|
le=120,
|
||||||
|
description="Jen N nejlepších kalendářních dní (podle clear-ish skóre) pro delta profil; ostatní dny ztlumené",
|
||||||
|
),
|
||||||
|
non_top_day_factor: float | None = Query(
|
||||||
|
None,
|
||||||
|
ge=0.0,
|
||||||
|
le=1.0,
|
||||||
|
description="Násobek váhy pro dny mimo top N (default 0.02)",
|
||||||
|
),
|
||||||
|
day_weight_gamma: float | None = Query(
|
||||||
|
None,
|
||||||
|
ge=0.25,
|
||||||
|
le=8.0,
|
||||||
|
description="Exponent na denní váhu (>1 silněji preferuje jen velmi 'clear' dny)",
|
||||||
|
),
|
||||||
) -> dict[str, list[dict[str, Any]]]:
|
) -> dict[str, list[dict[str, Any]]]:
|
||||||
if to_ts <= from_ts:
|
if to_ts <= from_ts:
|
||||||
raise HTTPException(status_code=422, detail="'to' must be after 'from'")
|
raise HTTPException(status_code=422, detail="'to' must be after 'from'")
|
||||||
@@ -571,6 +589,8 @@ async def get_site_forecast_pv_slots_range_corrected(
|
|||||||
now = datetime.now(tz=timezone.utc)
|
now = datetime.now(tz=timezone.utc)
|
||||||
delta_to = delta_to_ts or now
|
delta_to = delta_to_ts or now
|
||||||
delta_from = delta_from_ts or (delta_to - timedelta(days=60))
|
delta_from = delta_from_ts or (delta_to - timedelta(days=60))
|
||||||
|
ntf = 0.02 if non_top_day_factor is None else float(non_top_day_factor)
|
||||||
|
dg = 1.0 if day_weight_gamma is None else float(day_weight_gamma)
|
||||||
async with db.acquire() as conn:
|
async with db.acquire() as conn:
|
||||||
site_ok = await conn.fetchval(
|
site_ok = await conn.fetchval(
|
||||||
"SELECT EXISTS(SELECT 1 FROM ems.site WHERE id = $1)", site_id
|
"SELECT EXISTS(SELECT 1 FROM ems.site WHERE id = $1)", site_id
|
||||||
@@ -587,7 +607,10 @@ async def get_site_forecast_pv_slots_range_corrected(
|
|||||||
$4::timestamptz,
|
$4::timestamptz,
|
||||||
$5::timestamptz,
|
$5::timestamptz,
|
||||||
$6::numeric,
|
$6::numeric,
|
||||||
$7::int
|
$7::int,
|
||||||
|
$8::int,
|
||||||
|
$9::numeric,
|
||||||
|
$10::numeric
|
||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
site_id,
|
site_id,
|
||||||
@@ -597,6 +620,9 @@ async def get_site_forecast_pv_slots_range_corrected(
|
|||||||
delta_to,
|
delta_to,
|
||||||
half_life_days,
|
half_life_days,
|
||||||
threshold_w,
|
threshold_w,
|
||||||
|
top_n_days,
|
||||||
|
ntf,
|
||||||
|
dg,
|
||||||
)
|
)
|
||||||
slots = raw if isinstance(raw, list) else []
|
slots = raw if isinstance(raw, list) else []
|
||||||
if not isinstance(slots, list):
|
if not isinstance(slots, list):
|
||||||
|
|||||||
@@ -3,12 +3,17 @@
|
|||||||
-- (aditivní korekce: corrected = max(0, forecast - delta[slot]))
|
-- (aditivní korekce: corrected = max(0, forecast - delta[slot]))
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
|
drop function if exists ems.fn_pv_forecast_delta_profile;
|
||||||
|
|
||||||
create or replace function ems.fn_pv_forecast_delta_profile(
|
create or replace function ems.fn_pv_forecast_delta_profile(
|
||||||
p_site_id int,
|
p_site_id int,
|
||||||
p_data_from timestamptz,
|
p_data_from timestamptz,
|
||||||
p_data_to timestamptz default now(),
|
p_data_to timestamptz default now(),
|
||||||
p_half_life_days numeric default 14,
|
p_half_life_days numeric default 14,
|
||||||
p_threshold_w int default 150
|
p_threshold_w int default 150,
|
||||||
|
p_top_n_days int default null,
|
||||||
|
p_non_top_day_factor numeric default 0.02,
|
||||||
|
p_day_weight_gamma numeric default 1.0
|
||||||
)
|
)
|
||||||
returns jsonb
|
returns jsonb
|
||||||
language sql
|
language sql
|
||||||
@@ -143,22 +148,45 @@ as $fn$
|
|||||||
left join day_jump dj on dj.day_local = de.day_local
|
left join day_jump dj on dj.day_local = de.day_local
|
||||||
left join day_med dm on dm.day_local = de.day_local
|
left join day_med dm on dm.day_local = de.day_local
|
||||||
),
|
),
|
||||||
|
-- Volitelně: jen top N kalendářních dní podle (w_energy * w_smooth); zbytek ztlumit (bez hardcodu data).
|
||||||
|
day_rank as (
|
||||||
|
select
|
||||||
|
ds.day_local,
|
||||||
|
row_number() over (
|
||||||
|
order by
|
||||||
|
(coalesce(ds.w_energy, 0.35) * coalesce(ds.w_smooth, 0.35)) desc,
|
||||||
|
ds.day_local desc
|
||||||
|
) as rn
|
||||||
|
from day_stats ds
|
||||||
|
),
|
||||||
filtered as (
|
filtered as (
|
||||||
select
|
select
|
||||||
s.slot_of_day,
|
s.slot_of_day,
|
||||||
(s.forecast_total_w - s.actual_total_w) as error_w,
|
(s.forecast_total_w - s.actual_total_w) as error_w,
|
||||||
exp(-s.age_days / nullif((select half_life_days from bounds), 0))
|
exp(-s.age_days / nullif((select half_life_days from bounds), 0))
|
||||||
|
* (
|
||||||
|
case
|
||||||
|
when p_top_n_days is null then 1::numeric
|
||||||
|
when p_top_n_days < 1 then 1::numeric
|
||||||
|
when dr.rn <= p_top_n_days then 1::numeric
|
||||||
|
else greatest(0::numeric, least(1::numeric, coalesce(p_non_top_day_factor, 0.02)))
|
||||||
|
end
|
||||||
|
)
|
||||||
* (
|
* (
|
||||||
0.05
|
0.05
|
||||||
+ 0.95
|
+ 0.95
|
||||||
* greatest(
|
* power(
|
||||||
|
greatest(
|
||||||
0.0,
|
0.0,
|
||||||
least(1.0, coalesce(ds.w_energy, 0.35) * coalesce(ds.w_smooth, 0.35))
|
least(1.0, coalesce(ds.w_energy, 0.35) * coalesce(ds.w_smooth, 0.35))
|
||||||
|
),
|
||||||
|
greatest(0.25, least(coalesce(p_day_weight_gamma, 1.0), 8.0))
|
||||||
)
|
)
|
||||||
) as w
|
) as w
|
||||||
from slots s
|
from slots s
|
||||||
cross join bounds b
|
cross join bounds b
|
||||||
left join day_stats ds on ds.day_local = s.day_local
|
left join day_stats ds on ds.day_local = s.day_local
|
||||||
|
left join day_rank dr on dr.day_local = s.day_local
|
||||||
where s.slot_of_day between 0 and 95
|
where s.slot_of_day between 0 and 95
|
||||||
and (s.actual_total_w > b.threshold_w or s.forecast_total_w > b.threshold_w)
|
and (s.actual_total_w > b.threshold_w or s.forecast_total_w > b.threshold_w)
|
||||||
),
|
),
|
||||||
@@ -200,5 +228,5 @@ as $fn$
|
|||||||
left join agg a on a.slot_of_day = sp.slot_of_day;
|
left join agg a on a.slot_of_day = sp.slot_of_day;
|
||||||
$fn$;
|
$fn$;
|
||||||
|
|
||||||
comment on function ems.fn_pv_forecast_delta_profile(int, timestamptz, timestamptz, numeric, int) is
|
comment on function ems.fn_pv_forecast_delta_profile is
|
||||||
'Aditivní delta profil chyby PV forecastu po 15min slotu dne (96 slotů). Zdroj: forecast_accuracy, vážení exp(-age/half_life_days) * day_weight (preferuje „clear-ish“ dny: vyšší denní energie vs median v okně + nižší median skoků výkonu mezi 15min v daylight bandu). Vrací JSON {deltas:[{slot_of_day, delta_w, sample_count}], ...}. Interní minimální cutoff dat (2026-04-11 Europe/Prague) brání učení z nekonzistentní historie před kompletním plněním actual.';
|
'Aditivní delta profil chyby PV forecastu po 15min slotu dne (96 slotů). Zdroj: forecast_accuracy, vážení exp(-age/half_life_days) * day_weight (clear-ish dny) * volitelně top_n_days (jen N nejlepších kalendářních dní podle w_energy*w_smooth, ostatní ztlumené) * power(day_weight, day_weight_gamma). Vrací JSON {deltas:[{slot_of_day, delta_w, sample_count}], ...}. Cutoff dat od 2026-04-11 Europe/Prague.';
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
-- corrected = max(0, forecast - delta_profile[slot_of_day])
|
-- corrected = max(0, forecast - delta_profile[slot_of_day])
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
|
drop function if exists ems.fn_forecast_pv_slots_range_corrected;
|
||||||
|
|
||||||
create or replace function ems.fn_forecast_pv_slots_range_corrected(
|
create or replace function ems.fn_forecast_pv_slots_range_corrected(
|
||||||
p_site_id int,
|
p_site_id int,
|
||||||
p_from timestamptz,
|
p_from timestamptz,
|
||||||
@@ -10,7 +12,10 @@ create or replace function ems.fn_forecast_pv_slots_range_corrected(
|
|||||||
p_delta_data_from timestamptz,
|
p_delta_data_from timestamptz,
|
||||||
p_delta_data_to timestamptz default now(),
|
p_delta_data_to timestamptz default now(),
|
||||||
p_half_life_days numeric default 14,
|
p_half_life_days numeric default 14,
|
||||||
p_threshold_w int default 150
|
p_threshold_w int default 150,
|
||||||
|
p_top_n_days int default null,
|
||||||
|
p_non_top_day_factor numeric default 0.02,
|
||||||
|
p_day_weight_gamma numeric default 1.0
|
||||||
)
|
)
|
||||||
returns jsonb
|
returns jsonb
|
||||||
language sql
|
language sql
|
||||||
@@ -67,7 +72,10 @@ as $fn$
|
|||||||
p_delta_data_from,
|
p_delta_data_from,
|
||||||
p_delta_data_to,
|
p_delta_data_to,
|
||||||
p_half_life_days,
|
p_half_life_days,
|
||||||
p_threshold_w
|
p_threshold_w,
|
||||||
|
p_top_n_days,
|
||||||
|
p_non_top_day_factor,
|
||||||
|
p_day_weight_gamma
|
||||||
) as j
|
) as j
|
||||||
),
|
),
|
||||||
deltas as (
|
deltas as (
|
||||||
@@ -119,5 +127,5 @@ as $fn$
|
|||||||
left join fc on fc.interval_start = s.interval_start;
|
left join fc on fc.interval_start = s.interval_start;
|
||||||
$fn$;
|
$fn$;
|
||||||
|
|
||||||
comment on function ems.fn_forecast_pv_slots_range_corrected(int, timestamptz, timestamptz, timestamptz, timestamptz, numeric, int) is
|
comment on function ems.fn_forecast_pv_slots_range_corrected is
|
||||||
'JSON pole {interval_start, pv_forecast_total_w, pv_forecast_corrected_w, slot_of_day} po 15 min pro [p_from, p_to). Korekce je aditivní delta profil z fn_pv_forecast_delta_profile. Horizont je omezený na max. 60 dní.';
|
'JSON pole {interval_start, pv_forecast_total_w, pv_forecast_corrected_w, slot_of_day} po 15 min pro [p_from, p_to). Korekce je aditivní delta profil z fn_pv_forecast_delta_profile (top_n_days / non_top_day_factor / day_weight_gamma). Horizont je omezený na max. 60 dní.';
|
||||||
|
|||||||
@@ -129,6 +129,12 @@ export type ForecastPvSlotsCorrectedParams = {
|
|||||||
delta_to?: string
|
delta_to?: string
|
||||||
half_life_days?: number
|
half_life_days?: number
|
||||||
threshold_w?: number
|
threshold_w?: number
|
||||||
|
/** Jen N nejlepších kalendářních dní pro výpočet delta profilu (backend → fn_pv_forecast_delta_profile). */
|
||||||
|
top_n_days?: number
|
||||||
|
/** Násobek váhy pro dny mimo top N (0–1, default na serveru 0.02). */
|
||||||
|
non_top_day_factor?: number
|
||||||
|
/** Exponent na denní váhu (default 1 = beze změny oproti předchozímu chování bez top_n). */
|
||||||
|
day_weight_gamma?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getForecastPvSlotsRangeCorrected(
|
export async function getForecastPvSlotsRangeCorrected(
|
||||||
|
|||||||
Reference in New Issue
Block a user