Spaces:
Running
Running
Change for frontend
Browse files
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 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 992 |
}
|
| 993 |
)
|
| 994 |
|
|
@@ -997,8 +1290,8 @@ def build_alert_rows(forecast_rows):
|
|
| 997 |
{
|
| 998 |
"type": "rain",
|
| 999 |
"severity": "warning",
|
| 1000 |
-
"title": "
|
| 1001 |
-
"message": f"
|
| 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],
|