246 lines
7.7 KiB
SQL
246 lines
7.7 KiB
SQL
-- Diagnostika: z kterých kalendářních dní (Europe/Prague) se skládá váha pro delta profil
|
|
-- (zarovnáno s ems.fn_pv_forecast_delta_profile: eff z site_pv_forecast_calibration, best s learning_eligible,
|
|
-- agregace slotů na úroveň site pro day_rank / váhy w — stejné jako slot_totals v R__078).
|
|
--
|
|
-- Uprav params (site_id, okno, half_life, threshold, top_n_days / non_top / gamma) a spusť v psql.
|
|
-- Jedna řádka = jeden kalendářní den v okně; p_top_n_days mění tier u vah (ne počet řádků).
|
|
|
|
WITH params AS (
|
|
SELECT
|
|
2::int AS site_id,
|
|
(now() - interval '60 days')::timestamptz AS p_data_from,
|
|
now()::timestamptz AS p_data_to,
|
|
14::numeric AS half_life_days,
|
|
150::int AS threshold_w,
|
|
NULL::int AS p_top_n_days,
|
|
0.02::numeric AS p_non_top_day_factor,
|
|
1.0::numeric AS p_day_weight_gamma
|
|
),
|
|
tz AS (
|
|
SELECT coalesce(nullif(trim(s.timezone), ''), 'Europe/Prague') AS tz_name
|
|
FROM ems.site s
|
|
JOIN params p ON s.id = p.site_id
|
|
),
|
|
eff AS (
|
|
SELECT
|
|
coalesce(cal.delta_learn_min_ts, timestamptz '2026-04-11T22:00:00Z') AS delta_learn_min_ts,
|
|
coalesce(cal.half_life_days, p.half_life_days) AS half_life_days,
|
|
coalesce(cal.threshold_w, p.threshold_w) AS threshold_w,
|
|
coalesce(cal.top_n_days, p.p_top_n_days) AS top_n_days,
|
|
coalesce(cal.non_top_day_factor, p.p_non_top_day_factor) AS non_top_day_factor,
|
|
coalesce(cal.day_weight_gamma, p.p_day_weight_gamma) AS day_weight_gamma
|
|
FROM params p
|
|
JOIN ems.site s ON s.id = p.site_id
|
|
LEFT JOIN ems.site_pv_forecast_calibration cal ON cal.site_id = s.id
|
|
),
|
|
bounds AS (
|
|
SELECT
|
|
greatest(p.p_data_from, p.p_data_to - interval '120 days', e.delta_learn_min_ts) AS ts_from,
|
|
p.p_data_to AS ts_to,
|
|
greatest(e.half_life_days, 1::numeric) AS half_life_days,
|
|
greatest(e.threshold_w, 0::numeric) AS threshold_w
|
|
FROM params p
|
|
CROSS JOIN eff e
|
|
),
|
|
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
|
|
JOIN params p ON fa.site_id = p.site_id
|
|
WHERE 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
|
|
AND coalesce(fa.learning_eligible, true) IS TRUE
|
|
),
|
|
slots_array AS (
|
|
SELECT
|
|
b.interval_start,
|
|
b.pv_array_id,
|
|
b.forecast_power_w::numeric AS forecast_w,
|
|
b.actual_power_w::numeric AS actual_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,
|
|
(b.interval_start AT TIME ZONE tz.tz_name)::date AS day_local,
|
|
extract(epoch FROM (now() - b.interval_start)) / 86400.0 AS age_days
|
|
FROM best b
|
|
CROSS JOIN tz
|
|
WHERE b.rn = 1
|
|
),
|
|
slots AS (
|
|
SELECT
|
|
sa.interval_start,
|
|
sum(sa.forecast_w)::numeric AS forecast_total_w,
|
|
sum(sa.actual_w)::numeric AS actual_total_w,
|
|
sa.slot_of_day,
|
|
sa.day_local,
|
|
max(sa.age_days) AS age_days
|
|
FROM slots_array sa
|
|
GROUP BY sa.interval_start, sa.slot_of_day, sa.day_local
|
|
),
|
|
day_energy AS (
|
|
SELECT s.day_local, sum(s.actual_total_w)::numeric / 4000.0 AS energy_kwh
|
|
FROM slots s
|
|
GROUP BY s.day_local
|
|
),
|
|
ref AS (
|
|
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY de.energy_kwh) AS med_kwh
|
|
FROM day_energy de
|
|
),
|
|
slot_steps AS (
|
|
SELECT
|
|
s.*,
|
|
lag(s.actual_total_w) OVER (PARTITION BY s.day_local ORDER BY s.interval_start) AS prev_actual_w
|
|
FROM slots s
|
|
WHERE s.slot_of_day BETWEEN 20 AND 80
|
|
AND s.actual_total_w > (SELECT threshold_w FROM bounds)
|
|
),
|
|
day_jump AS (
|
|
SELECT
|
|
ss.day_local,
|
|
percentile_cont(0.5) WITHIN GROUP (ORDER BY abs(ss.actual_total_w - ss.prev_actual_w)) AS med_jump_w
|
|
FROM slot_steps ss
|
|
WHERE ss.prev_actual_w IS NOT NULL
|
|
GROUP BY ss.day_local
|
|
),
|
|
day_med AS (
|
|
SELECT
|
|
s.day_local,
|
|
percentile_cont(0.5) WITHIN GROUP (ORDER BY s.actual_total_w) AS p50_actual_w
|
|
FROM slots s
|
|
WHERE s.actual_total_w > (SELECT threshold_w FROM bounds)
|
|
GROUP BY s.day_local
|
|
),
|
|
day_stats AS (
|
|
SELECT
|
|
de.day_local,
|
|
de.energy_kwh,
|
|
dj.med_jump_w,
|
|
dm.p50_actual_w,
|
|
CASE
|
|
WHEN (SELECT med_kwh FROM ref) IS NULL OR (SELECT med_kwh FROM ref) <= 0 THEN 0.5
|
|
ELSE greatest(
|
|
0.0,
|
|
least(
|
|
1.0,
|
|
(de.energy_kwh - (SELECT med_kwh FROM ref) * 0.55)
|
|
/ nullif((SELECT med_kwh FROM ref) * 0.35, 0)
|
|
)
|
|
)
|
|
END AS w_energy,
|
|
CASE
|
|
WHEN dj.med_jump_w IS NULL OR dm.p50_actual_w IS NULL THEN 0.35
|
|
ELSE greatest(
|
|
0.0,
|
|
least(
|
|
1.0,
|
|
1.0
|
|
- (
|
|
dj.med_jump_w
|
|
/ nullif(greatest(300.0, dm.p50_actual_w * 0.25), 0)
|
|
)
|
|
)
|
|
)
|
|
END AS w_smooth
|
|
FROM day_energy de
|
|
LEFT JOIN day_jump dj ON dj.day_local = de.day_local
|
|
LEFT JOIN day_med dm ON dm.day_local = de.day_local
|
|
),
|
|
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,
|
|
(coalesce(ds.w_energy, 0.35) * coalesce(ds.w_smooth, 0.35)) AS day_score
|
|
FROM day_stats ds
|
|
),
|
|
filtered AS (
|
|
SELECT
|
|
s.day_local,
|
|
s.slot_of_day,
|
|
exp(-s.age_days / nullif((SELECT half_life_days FROM bounds), 0))
|
|
* (
|
|
CASE
|
|
WHEN (SELECT top_n_days FROM eff) IS NULL THEN 1::numeric
|
|
WHEN (SELECT top_n_days FROM eff) < 1 THEN 1::numeric
|
|
WHEN dr.rn <= (SELECT top_n_days FROM eff) THEN 1::numeric
|
|
ELSE greatest(
|
|
0::numeric,
|
|
least(1::numeric, coalesce((SELECT non_top_day_factor FROM eff), 0.02))
|
|
)
|
|
END
|
|
)
|
|
* (
|
|
0.05
|
|
+ 0.95
|
|
* power(
|
|
greatest(
|
|
0.0,
|
|
least(1.0, coalesce(ds.w_energy, 0.35) * coalesce(ds.w_smooth, 0.35))
|
|
),
|
|
greatest(
|
|
0.25,
|
|
least(coalesce((SELECT day_weight_gamma FROM eff), 1.0), 8.0)
|
|
)
|
|
)
|
|
) AS w
|
|
FROM slots s
|
|
CROSS JOIN bounds b
|
|
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
|
|
AND (s.actual_total_w > b.threshold_w OR s.forecast_total_w > b.threshold_w)
|
|
),
|
|
by_day AS (
|
|
SELECT day_local, sum(w) AS w_sum, count(*)::bigint AS slot_rows
|
|
FROM filtered
|
|
GROUP BY day_local
|
|
),
|
|
tot AS (
|
|
SELECT sum(w_sum) AS w_tot FROM by_day
|
|
)
|
|
SELECT
|
|
dr.rn AS day_rank,
|
|
ds.day_local,
|
|
round(ds.energy_kwh::numeric, 2) AS energy_kwh,
|
|
round(ds.w_energy::numeric, 3) AS w_energy,
|
|
round(ds.w_smooth::numeric, 3) AS w_smooth,
|
|
round(dr.day_score::numeric, 4) AS day_score,
|
|
round(coalesce(bd.w_sum, 0)::numeric, 2) AS sum_w_in_filtered,
|
|
coalesce(bd.slot_rows, 0::bigint) AS slot_rows,
|
|
round(
|
|
(100.0 * coalesce(bd.w_sum, 0) / nullif((SELECT w_tot FROM tot), 0))::numeric,
|
|
2
|
|
) AS pct_of_total_weight
|
|
FROM day_stats ds
|
|
JOIN day_rank dr ON dr.day_local = ds.day_local
|
|
LEFT JOIN by_day bd ON bd.day_local = ds.day_local
|
|
ORDER BY dr.rn;
|
|
|
|
-- Shrnutí okna + výstup funkce (stejné parametry jako v params výše):
|
|
-- SELECT (ems.fn_pv_forecast_delta_profile(
|
|
-- (SELECT site_id FROM params),
|
|
-- (SELECT p_data_from FROM params),
|
|
-- (SELECT p_data_to FROM params),
|
|
-- (SELECT half_life_days FROM params),
|
|
-- (SELECT threshold_w FROM params),
|
|
-- (SELECT p_top_n_days FROM params),
|
|
-- (SELECT p_non_top_day_factor FROM params),
|
|
-- (SELECT p_day_weight_gamma FROM params)
|
|
-- ))->'data_from' AS data_from,
|
|
-- (ems.fn_pv_forecast_delta_profile(...))->'data_to' AS data_to;
|