korkece fve predikce, grafy predikci
This commit is contained in:
121
db/routines/R__078_fn_pv_forecast_delta_profile.sql
Normal file
121
db/routines/R__078_fn_pv_forecast_delta_profile.sql
Normal file
@@ -0,0 +1,121 @@
|
||||
-- ============================================================
|
||||
-- Profil systematické chyby PV forecastu po 15min slotu dne
|
||||
-- (aditivní korekce: corrected = max(0, forecast - delta[slot]))
|
||||
-- ============================================================
|
||||
|
||||
create or replace function ems.fn_pv_forecast_delta_profile(
|
||||
p_site_id int,
|
||||
p_data_from timestamptz,
|
||||
p_data_to timestamptz default now(),
|
||||
p_half_life_days numeric default 14,
|
||||
p_threshold_w int default 150
|
||||
)
|
||||
returns jsonb
|
||||
language sql
|
||||
stable
|
||||
as $fn$
|
||||
with tz as (
|
||||
select coalesce(nullif(trim(s.timezone), ''), 'Europe/Prague') as tz_name
|
||||
from ems.site s
|
||||
where s.id = p_site_id
|
||||
),
|
||||
-- Cutoff z analýzy DB (EMS Postgres): u site_id=2 (`home-01`) začíná být
|
||||
-- `forecast_accuracy.actual_power_w` spolehlivě vyplněné pro celé kalendářní dny
|
||||
-- od 2026-04-06 (Europe/Prague). Dřívší dny mají výrazně nižší podíl slotů s actual
|
||||
-- (částečný backfill / výpadky) a zkreslují delta profil.
|
||||
cutoff as (
|
||||
select timestamptz '2026-04-05T22:00:00Z' as min_ts
|
||||
),
|
||||
bounds as (
|
||||
select
|
||||
greatest(p_data_from, p_data_to - interval '120 days', (select min_ts from cutoff)) as ts_from,
|
||||
p_data_to as ts_to,
|
||||
greatest(p_half_life_days, 1) as half_life_days,
|
||||
greatest(p_threshold_w, 0) as threshold_w
|
||||
),
|
||||
-- vezmeme jeden „reprezentativní“ forecast z historie: pro každý interval_start a pv_array_id
|
||||
-- vybereme nejnovější forecast (forecast_created_at) který je <= interval_start (lead_time >= 0)
|
||||
best as (
|
||||
select
|
||||
fa.interval_start,
|
||||
fa.pv_array_id,
|
||||
fa.forecast_power_w,
|
||||
fa.actual_power_w,
|
||||
fa.forecast_created_at,
|
||||
row_number() over (
|
||||
partition by fa.interval_start, fa.pv_array_id
|
||||
order by fa.forecast_created_at desc
|
||||
) as rn
|
||||
from ems.forecast_accuracy fa
|
||||
cross join bounds b
|
||||
where fa.site_id = p_site_id
|
||||
and fa.interval_start >= b.ts_from
|
||||
and fa.interval_start < b.ts_to
|
||||
and fa.actual_power_w is not null
|
||||
and fa.forecast_created_at <= fa.interval_start
|
||||
),
|
||||
slots as (
|
||||
select
|
||||
b.interval_start,
|
||||
sum(b.forecast_power_w)::numeric as forecast_total_w,
|
||||
sum(b.actual_power_w)::numeric as actual_total_w,
|
||||
(
|
||||
(extract(hour from (b.interval_start at time zone tz.tz_name))::int * 60)
|
||||
+ extract(minute from (b.interval_start at time zone tz.tz_name))::int
|
||||
) / 15 as slot_of_day,
|
||||
extract(epoch from (now() - b.interval_start)) / 86400.0 as age_days
|
||||
from best b
|
||||
cross join tz
|
||||
where b.rn = 1
|
||||
group by b.interval_start, slot_of_day, tz.tz_name
|
||||
),
|
||||
filtered as (
|
||||
select
|
||||
s.slot_of_day,
|
||||
(s.forecast_total_w - s.actual_total_w) as error_w,
|
||||
exp(-s.age_days / nullif((select half_life_days from bounds), 0)) as w
|
||||
from slots s
|
||||
cross join bounds b
|
||||
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)
|
||||
),
|
||||
agg as (
|
||||
select
|
||||
slot_of_day,
|
||||
count(*) as sample_count,
|
||||
sum(w) as w_sum,
|
||||
case
|
||||
when sum(w) > 0 then sum(error_w * w) / sum(w)
|
||||
else null
|
||||
end as delta_w
|
||||
from filtered
|
||||
group by slot_of_day
|
||||
),
|
||||
spine as (
|
||||
select generate_series(0, 95) as slot_of_day
|
||||
)
|
||||
select jsonb_build_object(
|
||||
'site_id', p_site_id,
|
||||
'data_from', (select ts_from from bounds),
|
||||
'data_to', (select ts_to from bounds),
|
||||
'half_life_days', (select half_life_days from bounds),
|
||||
'threshold_w', (select threshold_w from bounds),
|
||||
'deltas',
|
||||
coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'slot_of_day', sp.slot_of_day,
|
||||
'delta_w', coalesce(round(a.delta_w)::int, 0),
|
||||
'sample_count', coalesce(a.sample_count, 0)
|
||||
)
|
||||
order by sp.slot_of_day
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
)
|
||||
from spine sp
|
||||
left join agg a on a.slot_of_day = sp.slot_of_day;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_pv_forecast_delta_profile(int, timestamptz, timestamptz, numeric, int) is
|
||||
'Aditivní delta profil chyby PV forecastu po 15min slotu dne (96 slotů). Zdroj: forecast_accuracy, vážení exp(-age/half_life_days). Vrací JSON {deltas:[{slot_of_day, delta_w, sample_count}], ...}. Interní minimální cutoff dat (2026-04-06 Europe/Prague) brání učení z nekonzistentní historie před kompletním plněním actual.';
|
||||
123
db/routines/R__079_fn_forecast_pv_slots_range_corrected.sql
Normal file
123
db/routines/R__079_fn_forecast_pv_slots_range_corrected.sql
Normal file
@@ -0,0 +1,123 @@
|
||||
-- ============================================================
|
||||
-- PV forecast sloty (15min) + aditivně korigovaný forecast
|
||||
-- corrected = max(0, forecast - delta_profile[slot_of_day])
|
||||
-- ============================================================
|
||||
|
||||
create or replace function ems.fn_forecast_pv_slots_range_corrected(
|
||||
p_site_id int,
|
||||
p_from timestamptz,
|
||||
p_to timestamptz,
|
||||
p_delta_data_from timestamptz,
|
||||
p_delta_data_to timestamptz default now(),
|
||||
p_half_life_days numeric default 14,
|
||||
p_threshold_w int default 150
|
||||
)
|
||||
returns jsonb
|
||||
language sql
|
||||
stable
|
||||
as $fn$
|
||||
with tz as (
|
||||
select coalesce(nullif(trim(s.timezone), ''), 'Europe/Prague') as tz_name
|
||||
from ems.site s
|
||||
where s.id = p_site_id
|
||||
),
|
||||
bounds as (
|
||||
select
|
||||
p_from as ts_from,
|
||||
case
|
||||
when p_to <= p_from then p_from + interval '15 minutes'
|
||||
when p_to > p_from + interval '120 hours' then p_from + interval '120 hours'
|
||||
else p_to
|
||||
end as ts_to
|
||||
),
|
||||
slot_spine as (
|
||||
select gs as interval_start
|
||||
from bounds b,
|
||||
generate_series(
|
||||
b.ts_from,
|
||||
(b.ts_to - interval '15 minutes')::timestamptz,
|
||||
interval '15 minutes'
|
||||
) as gs
|
||||
),
|
||||
fc as (
|
||||
select
|
||||
u.interval_start,
|
||||
coalesce(sum(u.power_w), 0)::bigint as pv_forecast_total_w
|
||||
from (
|
||||
select distinct on (fpi.interval_start, fpr.pv_array_id)
|
||||
fpi.interval_start,
|
||||
fpi.power_w
|
||||
from ems.forecast_pv_interval fpi
|
||||
join ems.forecast_pv_run fpr on fpr.id = fpi.run_id
|
||||
join ems.asset_pv_array apa
|
||||
on apa.id = fpr.pv_array_id
|
||||
and apa.site_id = fpr.site_id
|
||||
cross join bounds b
|
||||
where fpr.site_id = p_site_id
|
||||
and fpr.status = 'ok'
|
||||
and fpi.interval_start >= b.ts_from
|
||||
and fpi.interval_start < b.ts_to
|
||||
order by fpi.interval_start, fpr.pv_array_id, fpr.created_at desc
|
||||
) u
|
||||
group by u.interval_start
|
||||
),
|
||||
profile as (
|
||||
select ems.fn_pv_forecast_delta_profile(
|
||||
p_site_id,
|
||||
p_delta_data_from,
|
||||
p_delta_data_to,
|
||||
p_half_life_days,
|
||||
p_threshold_w
|
||||
) as j
|
||||
),
|
||||
deltas as (
|
||||
select
|
||||
(x->>'slot_of_day')::int as slot_of_day,
|
||||
(x->>'delta_w')::int as delta_w,
|
||||
(x->>'sample_count')::int as sample_count
|
||||
from profile p
|
||||
cross join lateral jsonb_array_elements(p.j->'deltas') as x
|
||||
)
|
||||
select coalesce(
|
||||
jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'interval_start', s.interval_start,
|
||||
'pv_forecast_total_w', coalesce(fc.pv_forecast_total_w, 0),
|
||||
'pv_forecast_corrected_w',
|
||||
greatest(
|
||||
0,
|
||||
coalesce(fc.pv_forecast_total_w, 0)::int
|
||||
- coalesce(
|
||||
(
|
||||
select d.delta_w
|
||||
from deltas d
|
||||
cross join tz
|
||||
where d.slot_of_day = (
|
||||
(
|
||||
(extract(hour from (s.interval_start at time zone tz.tz_name))::int * 60)
|
||||
+ extract(minute from (s.interval_start at time zone tz.tz_name))::int
|
||||
) / 15
|
||||
)
|
||||
),
|
||||
0
|
||||
)
|
||||
),
|
||||
'slot_of_day',
|
||||
(
|
||||
(
|
||||
(extract(hour from (s.interval_start at time zone tz.tz_name))::int * 60)
|
||||
+ extract(minute from (s.interval_start at time zone tz.tz_name))::int
|
||||
) / 15
|
||||
)
|
||||
)
|
||||
order by s.interval_start
|
||||
),
|
||||
'[]'::jsonb
|
||||
)
|
||||
from slot_spine s
|
||||
cross join tz
|
||||
left join fc on fc.interval_start = s.interval_start;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_forecast_pv_slots_range_corrected(int, timestamptz, timestamptz, timestamptz, timestamptz, numeric, int) 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.';
|
||||
Reference in New Issue
Block a user