-- Diagnostika: z kterých kalendářních dní (Europe/Prague) se skládá váha pro delta profil -- (stejná logika jako ems.fn_pv_forecast_delta_profile: best → slots → day_stats → day_rank → váhy w). -- -- 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 ), cutoff AS ( SELECT timestamptz '2026-04-11T22:00:00Z' AS min_ts ), bounds AS ( SELECT greatest(p.p_data_from, p.p_data_to - interval '120 days', (SELECT min_ts FROM cutoff)) AS ts_from, p.p_data_to AS ts_to, greatest(p.half_life_days, 1) AS half_life_days, greatest(p.threshold_w, 0) AS threshold_w FROM params p ), 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 ), 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, (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 GROUP BY b.interval_start, slot_of_day, day_local, tz.tz_name ), 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 p_top_n_days FROM params) IS NULL THEN 1::numeric WHEN (SELECT p_top_n_days FROM params) < 1 THEN 1::numeric WHEN dr.rn <= (SELECT p_top_n_days FROM params) THEN 1::numeric ELSE greatest( 0::numeric, least(1::numeric, coalesce((SELECT p_non_top_day_factor FROM params), 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 p_day_weight_gamma FROM params), 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;