Ciroc0 commited on
Commit
863d586
·
1 Parent(s): 0a1149f

Change for frontend

Browse files
Files changed (1) hide show
  1. app.py +308 -44
app.py CHANGED
@@ -904,6 +904,7 @@ def calculate_verification_metrics(predictions_df=None, backtest_df=None):
904
  period_label = "Seneste 7 dages backtest"
905
 
906
  result = {
 
907
  "periodLabel": period_label,
908
  "rmseDmi": None,
909
  "rmseMl": None,
@@ -964,6 +965,280 @@ def build_lead_bucket_rows(registry):
964
  return rows
965
 
966
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
  def build_alert_rows(forecast_rows):
968
  """Derive lightweight alert messages from the live forecast."""
969
  alerts = []
@@ -977,10 +1252,28 @@ def build_alert_rows(forecast_rows):
977
  }
978
  ]
979
 
980
- max_wind_speed = max((row.get("mlWindSpeed") or 0.0) for row in forecast_rows)
981
- max_wind_gust = max((row.get("mlWindGust") or 0.0) for row in forecast_rows)
982
- max_rain_prob = max((row.get("mlRainProb") or 0.0) for row in forecast_rows)
983
- max_rain_amount = max((row.get("mlRainAmount") or 0.0) for row in forecast_rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
 
985
  if max_wind_speed >= 15 or max_wind_gust >= 20:
986
  alerts.append(
@@ -988,7 +1281,7 @@ def build_alert_rows(forecast_rows):
988
  "type": "wind",
989
  "severity": "warning",
990
  "title": "Kraftig vind",
991
- "message": f"ML forventer op til {max_wind_speed:.1f} m/s og vindstød op til {max_wind_gust:.1f} m/s i forecast-vinduet.",
992
  }
993
  )
994
 
@@ -997,8 +1290,8 @@ def build_alert_rows(forecast_rows):
997
  {
998
  "type": "rain",
999
  "severity": "warning",
1000
- "title": "Våd periode",
1001
- "message": f"ML forventer op til {max_rain_prob:.0f}% regnrisiko og {max_rain_amount:.1f} mm/time i forecast-vinduet.",
1002
  }
1003
  )
1004
 
@@ -1021,9 +1314,11 @@ def build_frontend_snapshot():
1021
  training_df, _ = load_existing_training_matrix()
1022
  registry = load_json_from_dataset("model_registry.json", DATASET_NAME) or {}
1023
  model_meta = load_json_from_dataset("model_meta.json", DATASET_NAME) or {}
 
1024
 
1025
  backtest_df = build_recent_backtest(training_df)
1026
  verification = calculate_verification_metrics(predictions_df=predictions_df, backtest_df=backtest_df)
 
1027
 
1028
  future_df = None
1029
  if predictions_df is not None and len(predictions_df) > 0:
@@ -1033,45 +1328,10 @@ def build_frontend_snapshot():
1033
  forecast_rows = []
1034
  if future_df is not None and len(future_df) > 0:
1035
  for _, row in future_df.iterrows():
1036
- forecast_rows.append(
1037
- {
1038
- "timestamp": to_iso(row.get("target_timestamp")),
1039
- "hour": int(row["target_timestamp"].hour),
1040
- "leadTimeHours": int(row.get("lead_time_hours") or 0),
1041
- "dmiTemp": safe_number(row.get("dmi_temperature_2m_pred")),
1042
- "mlTemp": safe_number(row.get("ml_temp")),
1043
- "apparentTemp": safe_number(row.get("dmi_apparent_temperature_pred")),
1044
- "dmiWindSpeed": safe_number(row.get("dmi_windspeed_10m_pred")),
1045
- "mlWindSpeed": safe_number(row.get("ml_wind_speed")),
1046
- "dmiWindGust": safe_number(row.get("dmi_windgusts_10m_pred")),
1047
- "mlWindGust": safe_number(row.get("ml_wind_gust")),
1048
- "windDirection": safe_number(row.get("dmi_winddirection_10m_pred")),
1049
- "dmiRainProb": safe_number(row.get("dmi_precipitation_probability_pred"), digits=2, default=0.0) or 0.0,
1050
- "mlRainProb": round((safe_number(row.get("ml_rain_prob"), digits=4, default=0.0) or 0.0) * 100, 2),
1051
- "dmiRainAmount": safe_number(row.get("dmi_precipitation_pred"), digits=3, default=0.0) or 0.0,
1052
- "mlRainAmount": safe_number(row.get("ml_rain_amount"), digits=3, default=0.0) or 0.0,
1053
- "weatherCode": int(row.get("dmi_weather_code_pred")) if safe_number(row.get("dmi_weather_code_pred"), digits=0) is not None else None,
1054
- "cloudCover": safe_number(row.get("dmi_cloud_cover_pred")),
1055
- "humidity": safe_number(row.get("dmi_relative_humidity_2m_pred")),
1056
- "pressure": safe_number(row.get("dmi_pressure_msl_pred")),
1057
- }
1058
- )
1059
 
1060
  current_row = forecast_rows[0] if forecast_rows else None
1061
- current = {
1062
- "timestamp": current_row["timestamp"] if current_row else to_iso(now_cph()),
1063
- "temp": current_row["mlTemp"] if current_row else None,
1064
- "apparentTemp": current_row["apparentTemp"] if current_row else None,
1065
- "windSpeed": current_row["mlWindSpeed"] if current_row else None,
1066
- "windGust": current_row["mlWindGust"] if current_row else None,
1067
- "windDirection": current_row["windDirection"] if current_row else None,
1068
- "rainProb": current_row["mlRainProb"] if current_row else 0.0,
1069
- "rainAmount": current_row["mlRainAmount"] if current_row else 0.0,
1070
- "humidity": current_row["humidity"] if current_row else None,
1071
- "pressure": current_row["pressure"] if current_row else None,
1072
- "cloudCover": current_row["cloudCover"] if current_row else None,
1073
- "weatherCode": current_row["weatherCode"] if current_row else None,
1074
- }
1075
 
1076
  feature_importance = []
1077
  registry_revision = registry.get("generated_at")
@@ -1092,8 +1352,12 @@ def build_frontend_snapshot():
1092
  "timezone": "Europe/Copenhagen",
1093
  },
1094
  "generatedAt": to_iso(now_cph()),
 
 
 
1095
  "current": current,
1096
  "forecast": forecast_rows,
 
1097
  "verification": verification,
1098
  "leadBuckets": build_lead_bucket_rows(registry),
1099
  "featureImportance": feature_importance[:24],
 
904
  period_label = "Seneste 7 dages backtest"
905
 
906
  result = {
907
+ "target": "temperature",
908
  "periodLabel": period_label,
909
  "rmseDmi": None,
910
  "rmseMl": None,
 
965
  return rows
966
 
967
 
968
+ TARGET_LABELS = {
969
+ "temperature": "Temperatur",
970
+ "wind_speed": "Vindhastighed",
971
+ "wind_gust": "Vindstoed",
972
+ "rain_event": "Regnrisiko",
973
+ "rain_amount": "Regnmaengde",
974
+ }
975
+
976
+ LEAD_BUCKET_ORDER = ["1-6", "7-12", "13-24", "25-48"]
977
+
978
+
979
+ def build_target_labels():
980
+ return dict(TARGET_LABELS)
981
+
982
+
983
+ def build_explanations():
984
+ return {
985
+ "forecast": "Du ser DMI's prognose side om side med vores ML-justering, naar der er en aktiv model.",
986
+ "performance": "Her kan du sammenligne, hvad DMI sagde, hvad ML sagde, og hvad vejret faktisk endte med at blive.",
987
+ "sources": "DMI er grundprognosen. ML er vores lokale justering. Hvis en ML-model ikke er aktiv, viser vi DMI direkte.",
988
+ }
989
+
990
+
991
+ def build_target_status(registry):
992
+ registry_targets = registry.get("targets", {}) if registry else {}
993
+ target_status = {}
994
+ for target_name in MODEL_FILES:
995
+ active_buckets = registry_targets.get(target_name, {}).get("active_buckets") or []
996
+ ordered_buckets = [bucket for bucket in LEAD_BUCKET_ORDER if bucket in active_buckets]
997
+ has_active_model = len(ordered_buckets) > 0
998
+ target_label = TARGET_LABELS.get(target_name, target_name)
999
+
1000
+ if has_active_model:
1001
+ status_label = "ML aktiv"
1002
+ status_description = (
1003
+ f"Vi viser baade DMI og ML for {target_label.lower()}, og ML bruges som den aktive prognose, naar den findes."
1004
+ )
1005
+ else:
1006
+ status_label = "Vises som DMI-prognose"
1007
+ status_description = (
1008
+ f"Vi viser DMI direkte for {target_label.lower()}, fordi der ikke er en aktiv ML-model for dette signal endnu."
1009
+ )
1010
+
1011
+ target_status[target_name] = {
1012
+ "hasActiveModel": has_active_model,
1013
+ "activeBuckets": ordered_buckets,
1014
+ "statusLabel": status_label,
1015
+ "statusDescription": status_description,
1016
+ }
1017
+
1018
+ return target_status
1019
+
1020
+
1021
+ def choose_effective_value(ml_value, dmi_value, has_active_model):
1022
+ if has_active_model and ml_value is not None:
1023
+ return ml_value, "ml"
1024
+ if dmi_value is not None:
1025
+ return dmi_value, "dmi"
1026
+ if ml_value is not None:
1027
+ return ml_value, "ml"
1028
+ return None, "dmi"
1029
+
1030
+
1031
+ def build_history_payload(predictions_df=None, backtest_df=None):
1032
+ source_df = None
1033
+
1034
+ if predictions_df is not None and len(predictions_df) > 0:
1035
+ verified = predictions_df[predictions_df["verified"].fillna(False).astype(bool)].copy()
1036
+ if len(verified) > 0:
1037
+ source_df = verified.sort_values("target_timestamp").tail(72).reset_index(drop=True)
1038
+
1039
+ if source_df is None and backtest_df is not None and len(backtest_df) > 0:
1040
+ source_df = backtest_df.sort_values("target_timestamp").tail(72).reset_index(drop=True)
1041
+
1042
+ history = {
1043
+ "temperature": [],
1044
+ "wind": [],
1045
+ "rain": [],
1046
+ }
1047
+
1048
+ if source_df is None or len(source_df) == 0:
1049
+ return history
1050
+
1051
+ for _, row in source_df.iterrows():
1052
+ history["temperature"].append(
1053
+ {
1054
+ "timestamp": to_iso(row.get("target_timestamp")),
1055
+ "dmiTemp": safe_number(row.get("dmi_temperature_2m_pred")),
1056
+ "mlTemp": safe_number(row.get("ml_temp")),
1057
+ "actualTemp": safe_number(row.get("actual_temp")),
1058
+ "verified": True,
1059
+ }
1060
+ )
1061
+ history["wind"].append(
1062
+ {
1063
+ "timestamp": to_iso(row.get("target_timestamp")),
1064
+ "dmiWindSpeed": safe_number(row.get("dmi_windspeed_10m_pred")),
1065
+ "mlWindSpeed": safe_number(row.get("ml_wind_speed")),
1066
+ "actualWindSpeed": safe_number(row.get("actual_wind_speed")),
1067
+ "dmiWindGust": safe_number(row.get("dmi_windgusts_10m_pred")),
1068
+ "mlWindGust": safe_number(row.get("ml_wind_gust")),
1069
+ "actualWindGust": safe_number(row.get("actual_wind_gust")),
1070
+ "verified": True,
1071
+ }
1072
+ )
1073
+
1074
+ dmi_rain_prob = safe_number(row.get("dmi_precipitation_probability_pred"), digits=2, default=0.0) or 0.0
1075
+ ml_rain_prob = round((safe_number(row.get("ml_rain_prob"), digits=4, default=0.0) or 0.0) * 100, 2)
1076
+ actual_rain_amount = safe_number(
1077
+ row.get("actual_rain_amount", row.get("actual_precipitation")),
1078
+ digits=3,
1079
+ default=None,
1080
+ )
1081
+ if actual_rain_amount is None:
1082
+ actual_rain_amount = safe_number(row.get("actual_precipitation"), digits=3, default=None)
1083
+ actual_rain_event = row.get("actual_rain_event")
1084
+ if actual_rain_event is None or pd.isna(actual_rain_event):
1085
+ if actual_rain_amount is None:
1086
+ actual_rain_event = None
1087
+ else:
1088
+ actual_rain_event = 1 if actual_rain_amount > 0.1 else 0
1089
+ else:
1090
+ actual_rain_event = int(actual_rain_event)
1091
+
1092
+ history["rain"].append(
1093
+ {
1094
+ "timestamp": to_iso(row.get("target_timestamp")),
1095
+ "dmiRainProb": dmi_rain_prob,
1096
+ "mlRainProb": ml_rain_prob,
1097
+ "actualRainEvent": actual_rain_event,
1098
+ "dmiRainAmount": safe_number(row.get("dmi_precipitation_pred"), digits=3, default=0.0) or 0.0,
1099
+ "mlRainAmount": safe_number(row.get("ml_rain_amount"), digits=3, default=0.0) or 0.0,
1100
+ "actualRainAmount": actual_rain_amount,
1101
+ "verified": True,
1102
+ }
1103
+ )
1104
+
1105
+ return history
1106
+
1107
+
1108
+ def build_forecast_row(row, target_status):
1109
+ dmi_temp = safe_number(row.get("dmi_temperature_2m_pred"))
1110
+ ml_temp = safe_number(row.get("ml_temp"))
1111
+ dmi_wind_speed = safe_number(row.get("dmi_windspeed_10m_pred"))
1112
+ ml_wind_speed = safe_number(row.get("ml_wind_speed"))
1113
+ dmi_wind_gust = safe_number(row.get("dmi_windgusts_10m_pred"))
1114
+ ml_wind_gust = safe_number(row.get("ml_wind_gust"))
1115
+ dmi_rain_prob = safe_number(row.get("dmi_precipitation_probability_pred"), digits=2, default=0.0) or 0.0
1116
+ ml_rain_prob = round((safe_number(row.get("ml_rain_prob"), digits=4, default=0.0) or 0.0) * 100, 2)
1117
+ dmi_rain_amount = safe_number(row.get("dmi_precipitation_pred"), digits=3, default=0.0) or 0.0
1118
+ ml_rain_amount = safe_number(row.get("ml_rain_amount"), digits=3, default=0.0) or 0.0
1119
+
1120
+ effective_temp, effective_temp_source = choose_effective_value(
1121
+ ml_temp,
1122
+ dmi_temp,
1123
+ target_status["temperature"]["hasActiveModel"],
1124
+ )
1125
+ effective_wind_speed, effective_wind_speed_source = choose_effective_value(
1126
+ ml_wind_speed,
1127
+ dmi_wind_speed,
1128
+ target_status["wind_speed"]["hasActiveModel"],
1129
+ )
1130
+ effective_wind_gust, effective_wind_gust_source = choose_effective_value(
1131
+ ml_wind_gust,
1132
+ dmi_wind_gust,
1133
+ target_status["wind_gust"]["hasActiveModel"],
1134
+ )
1135
+ effective_rain_prob, effective_rain_prob_source = choose_effective_value(
1136
+ ml_rain_prob,
1137
+ dmi_rain_prob,
1138
+ target_status["rain_event"]["hasActiveModel"],
1139
+ )
1140
+ effective_rain_amount, effective_rain_amount_source = choose_effective_value(
1141
+ ml_rain_amount,
1142
+ dmi_rain_amount,
1143
+ target_status["rain_amount"]["hasActiveModel"],
1144
+ )
1145
+
1146
+ return {
1147
+ "timestamp": to_iso(row.get("target_timestamp")),
1148
+ "hour": int(row["target_timestamp"].hour),
1149
+ "leadTimeHours": int(row.get("lead_time_hours") or 0),
1150
+ "dmiTemp": dmi_temp,
1151
+ "mlTemp": ml_temp,
1152
+ "effectiveTemp": effective_temp,
1153
+ "effectiveTempSource": effective_temp_source,
1154
+ "apparentTemp": safe_number(row.get("dmi_apparent_temperature_pred")),
1155
+ "dmiWindSpeed": dmi_wind_speed,
1156
+ "mlWindSpeed": ml_wind_speed,
1157
+ "effectiveWindSpeed": effective_wind_speed,
1158
+ "effectiveWindSpeedSource": effective_wind_speed_source,
1159
+ "dmiWindGust": dmi_wind_gust,
1160
+ "mlWindGust": ml_wind_gust,
1161
+ "effectiveWindGust": effective_wind_gust,
1162
+ "effectiveWindGustSource": effective_wind_gust_source,
1163
+ "windDirection": safe_number(row.get("dmi_winddirection_10m_pred")),
1164
+ "dmiRainProb": dmi_rain_prob,
1165
+ "mlRainProb": ml_rain_prob,
1166
+ "effectiveRainProb": effective_rain_prob or 0.0,
1167
+ "effectiveRainProbSource": effective_rain_prob_source,
1168
+ "dmiRainAmount": dmi_rain_amount,
1169
+ "mlRainAmount": ml_rain_amount,
1170
+ "effectiveRainAmount": effective_rain_amount or 0.0,
1171
+ "effectiveRainAmountSource": effective_rain_amount_source,
1172
+ "weatherCode": int(row.get("dmi_weather_code_pred")) if safe_number(row.get("dmi_weather_code_pred"), digits=0) is not None else None,
1173
+ "cloudCover": safe_number(row.get("dmi_cloud_cover_pred")),
1174
+ "humidity": safe_number(row.get("dmi_relative_humidity_2m_pred")),
1175
+ "pressure": safe_number(row.get("dmi_pressure_msl_pred")),
1176
+ }
1177
+
1178
+
1179
+ def build_current_payload(current_row):
1180
+ if not current_row:
1181
+ return {
1182
+ "timestamp": to_iso(now_cph()),
1183
+ "temp": None,
1184
+ "dmiTemp": None,
1185
+ "mlTemp": None,
1186
+ "tempSource": "dmi",
1187
+ "apparentTemp": None,
1188
+ "windSpeed": None,
1189
+ "dmiWindSpeed": None,
1190
+ "mlWindSpeed": None,
1191
+ "windSpeedSource": "dmi",
1192
+ "windGust": None,
1193
+ "dmiWindGust": None,
1194
+ "mlWindGust": None,
1195
+ "windGustSource": "dmi",
1196
+ "windDirection": None,
1197
+ "rainProb": 0.0,
1198
+ "dmiRainProb": 0.0,
1199
+ "mlRainProb": 0.0,
1200
+ "rainProbSource": "dmi",
1201
+ "rainAmount": 0.0,
1202
+ "dmiRainAmount": 0.0,
1203
+ "mlRainAmount": 0.0,
1204
+ "rainAmountSource": "dmi",
1205
+ "humidity": None,
1206
+ "pressure": None,
1207
+ "cloudCover": None,
1208
+ "weatherCode": None,
1209
+ }
1210
+
1211
+ return {
1212
+ "timestamp": current_row["timestamp"],
1213
+ "temp": current_row["effectiveTemp"],
1214
+ "dmiTemp": current_row["dmiTemp"],
1215
+ "mlTemp": current_row["mlTemp"],
1216
+ "tempSource": current_row["effectiveTempSource"],
1217
+ "apparentTemp": current_row["apparentTemp"],
1218
+ "windSpeed": current_row["effectiveWindSpeed"],
1219
+ "dmiWindSpeed": current_row["dmiWindSpeed"],
1220
+ "mlWindSpeed": current_row["mlWindSpeed"],
1221
+ "windSpeedSource": current_row["effectiveWindSpeedSource"],
1222
+ "windGust": current_row["effectiveWindGust"],
1223
+ "dmiWindGust": current_row["dmiWindGust"],
1224
+ "mlWindGust": current_row["mlWindGust"],
1225
+ "windGustSource": current_row["effectiveWindGustSource"],
1226
+ "windDirection": current_row["windDirection"],
1227
+ "rainProb": current_row["effectiveRainProb"],
1228
+ "dmiRainProb": current_row["dmiRainProb"],
1229
+ "mlRainProb": current_row["mlRainProb"],
1230
+ "rainProbSource": current_row["effectiveRainProbSource"],
1231
+ "rainAmount": current_row["effectiveRainAmount"],
1232
+ "dmiRainAmount": current_row["dmiRainAmount"],
1233
+ "mlRainAmount": current_row["mlRainAmount"],
1234
+ "rainAmountSource": current_row["effectiveRainAmountSource"],
1235
+ "humidity": current_row["humidity"],
1236
+ "pressure": current_row["pressure"],
1237
+ "cloudCover": current_row["cloudCover"],
1238
+ "weatherCode": current_row["weatherCode"],
1239
+ }
1240
+
1241
+
1242
  def build_alert_rows(forecast_rows):
1243
  """Derive lightweight alert messages from the live forecast."""
1244
  alerts = []
 
1252
  }
1253
  ]
1254
 
1255
+ max_wind_row = max(
1256
+ forecast_rows,
1257
+ key=lambda row: max(row.get("effectiveWindSpeed") or 0.0, row.get("effectiveWindGust") or 0.0),
1258
+ )
1259
+ max_rain_row = max(
1260
+ forecast_rows,
1261
+ key=lambda row: max(row.get("effectiveRainProb") or 0.0, row.get("effectiveRainAmount") or 0.0),
1262
+ )
1263
+ max_wind_speed = max_wind_row.get("effectiveWindSpeed") or 0.0
1264
+ max_wind_gust = max_wind_row.get("effectiveWindGust") or 0.0
1265
+ max_rain_prob = max_rain_row.get("effectiveRainProb") or 0.0
1266
+ max_rain_amount = max_rain_row.get("effectiveRainAmount") or 0.0
1267
+ wind_source = (
1268
+ "ML"
1269
+ if max_wind_row.get("effectiveWindSpeedSource") == "ml" or max_wind_row.get("effectiveWindGustSource") == "ml"
1270
+ else "DMI"
1271
+ )
1272
+ rain_source = (
1273
+ "ML"
1274
+ if max_rain_row.get("effectiveRainProbSource") == "ml" or max_rain_row.get("effectiveRainAmountSource") == "ml"
1275
+ else "DMI"
1276
+ )
1277
 
1278
  if max_wind_speed >= 15 or max_wind_gust >= 20:
1279
  alerts.append(
 
1281
  "type": "wind",
1282
  "severity": "warning",
1283
  "title": "Kraftig vind",
1284
+ "message": f"{wind_source} forventer op til {max_wind_speed:.1f} m/s og vindstoed op til {max_wind_gust:.1f} m/s i forecast-vinduet.",
1285
  }
1286
  )
1287
 
 
1290
  {
1291
  "type": "rain",
1292
  "severity": "warning",
1293
+ "title": "Vaad periode",
1294
+ "message": f"{rain_source} forventer op til {max_rain_prob:.0f}% regnrisiko og {max_rain_amount:.1f} mm/time i forecast-vinduet.",
1295
  }
1296
  )
1297
 
 
1314
  training_df, _ = load_existing_training_matrix()
1315
  registry = load_json_from_dataset("model_registry.json", DATASET_NAME) or {}
1316
  model_meta = load_json_from_dataset("model_meta.json", DATASET_NAME) or {}
1317
+ target_status = build_target_status(registry)
1318
 
1319
  backtest_df = build_recent_backtest(training_df)
1320
  verification = calculate_verification_metrics(predictions_df=predictions_df, backtest_df=backtest_df)
1321
+ history = build_history_payload(predictions_df=predictions_df, backtest_df=backtest_df)
1322
 
1323
  future_df = None
1324
  if predictions_df is not None and len(predictions_df) > 0:
 
1328
  forecast_rows = []
1329
  if future_df is not None and len(future_df) > 0:
1330
  for _, row in future_df.iterrows():
1331
+ forecast_rows.append(build_forecast_row(row, target_status))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1332
 
1333
  current_row = forecast_rows[0] if forecast_rows else None
1334
+ current = build_current_payload(current_row)
 
 
 
 
 
 
 
 
 
 
 
 
 
1335
 
1336
  feature_importance = []
1337
  registry_revision = registry.get("generated_at")
 
1352
  "timezone": "Europe/Copenhagen",
1353
  },
1354
  "generatedAt": to_iso(now_cph()),
1355
+ "targetLabels": build_target_labels(),
1356
+ "explanations": build_explanations(),
1357
+ "targetStatus": target_status,
1358
  "current": current,
1359
  "forecast": forecast_rows,
1360
+ "history": history,
1361
  "verification": verification,
1362
  "leadBuckets": build_lead_bucket_rows(registry),
1363
  "featureImportance": feature_importance[:24],