Forecasting app: hybrid port to HNF stack

Python FastAPI ML backend kept intact; auth replaced with central hnf_session cookie verification. Frontend rebuilt on React 18 + TS + Vite with stack design system, Plotly charts retained. Shared Postgres via DATABASE_URL; schema applied on startup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-04 18:49:34 +00:00
commit 75d2c1fa9d
103 changed files with 70316 additions and 0 deletions

View file

@ -0,0 +1,182 @@
"""
Accuracy calculation job
Compares forecasts to actuals once dates have passed
"""
import logging
from datetime import date, timedelta
from sqlalchemy import text
from database import SyncSessionLocal
logger = logging.getLogger(__name__)
async def run_accuracy_calculation():
"""
Calculate forecast accuracy for dates that have passed.
Updates actual_vs_forecast table with error metrics.
"""
logger.info("Starting accuracy calculation")
db = next(iter([SyncSessionLocal()]))
try:
# Process yesterday's actuals
calc_date = date.today() - timedelta(days=1)
# Get all metrics
metrics_result = db.execute(
text("""
SELECT metric_code FROM forecast_metrics WHERE is_active = TRUE
""")
)
metrics = [row.metric_code for row in metrics_result.fetchall()]
for metric_code in metrics:
# Get actual value from daily_metrics
actual_result = db.execute(
text("""
SELECT actual_value FROM daily_metrics
WHERE date = :calc_date AND metric_code = :metric_code
"""),
{"calc_date": calc_date, "metric_code": metric_code}
)
actual_row = actual_result.fetchone()
actual_value = actual_row.actual_value if actual_row else None
if actual_value is None:
continue # Skip if no actual available
# Get forecasts for this date
forecast_result = db.execute(
text("""
SELECT model_type, predicted_value, lower_bound, upper_bound
FROM forecasts
WHERE forecast_date = :calc_date AND forecast_type = :metric_code
"""),
{"calc_date": calc_date, "metric_code": metric_code}
)
forecasts = {row.model_type: row for row in forecast_result.fetchall()}
prophet_forecast = forecasts.get('prophet')
xgboost_forecast = forecasts.get('xgboost')
pickup_forecast = forecasts.get('pickup')
catboost_forecast = forecasts.get('catboost')
# Calculate errors
def calc_error(forecast_val):
if forecast_val is None:
return None, None
error = actual_value - forecast_val
pct_error = (error / actual_value * 100) if actual_value != 0 else None
return error, pct_error
prophet_error, prophet_pct = calc_error(
prophet_forecast.predicted_value if prophet_forecast else None
)
xgboost_error, xgboost_pct = calc_error(
xgboost_forecast.predicted_value if xgboost_forecast else None
)
pickup_error, pickup_pct = calc_error(
pickup_forecast.predicted_value if pickup_forecast else None
)
catboost_error, catboost_pct = calc_error(
catboost_forecast.predicted_value if catboost_forecast else None
)
# Determine best model
errors = []
if prophet_error is not None:
errors.append(('prophet', abs(prophet_error)))
if xgboost_error is not None:
errors.append(('xgboost', abs(xgboost_error)))
if pickup_error is not None:
errors.append(('pickup', abs(pickup_error)))
if catboost_error is not None:
errors.append(('catboost', abs(catboost_error)))
best_model = min(errors, key=lambda x: x[1])[0] if errors else None
# Get budget value
budget_result = db.execute(
text("""
SELECT budget_value FROM daily_budgets
WHERE date = :calc_date AND budget_type = :metric_code
"""),
{"calc_date": calc_date, "metric_code": metric_code}
)
budget_row = budget_result.fetchone()
budget_value = budget_row.budget_value if budget_row else None
# Upsert accuracy record
db.execute(
text("""
INSERT INTO actual_vs_forecast (
date, metric_type, actual_value,
prophet_forecast, prophet_lower, prophet_upper,
xgboost_forecast, pickup_forecast,
catboost_forecast,
budget_value,
prophet_error, prophet_pct_error,
xgboost_error, xgboost_pct_error,
pickup_error, pickup_pct_error,
catboost_error, catboost_pct_error,
best_model, calculated_at
) VALUES (
:date, :metric_type, :actual,
:prophet_val, :prophet_lower, :prophet_upper,
:xgboost_val, :pickup_val,
:catboost_val,
:budget,
:prophet_error, :prophet_pct,
:xgboost_error, :xgboost_pct,
:pickup_error, :pickup_pct,
:catboost_error, :catboost_pct,
:best_model, NOW()
)
ON CONFLICT (date, metric_type) DO UPDATE SET
actual_value = :actual,
prophet_error = :prophet_error,
prophet_pct_error = :prophet_pct,
xgboost_error = :xgboost_error,
xgboost_pct_error = :xgboost_pct,
pickup_error = :pickup_error,
pickup_pct_error = :pickup_pct,
catboost_forecast = :catboost_val,
catboost_error = :catboost_error,
catboost_pct_error = :catboost_pct,
best_model = :best_model,
calculated_at = NOW()
"""),
{
"date": calc_date,
"metric_type": metric_code,
"actual": actual_value,
"prophet_val": prophet_forecast.predicted_value if prophet_forecast else None,
"prophet_lower": prophet_forecast.lower_bound if prophet_forecast else None,
"prophet_upper": prophet_forecast.upper_bound if prophet_forecast else None,
"xgboost_val": xgboost_forecast.predicted_value if xgboost_forecast else None,
"pickup_val": pickup_forecast.predicted_value if pickup_forecast else None,
"catboost_val": catboost_forecast.predicted_value if catboost_forecast else None,
"budget": budget_value,
"prophet_error": prophet_error,
"prophet_pct": prophet_pct,
"xgboost_error": xgboost_error,
"xgboost_pct": xgboost_pct,
"pickup_error": pickup_error,
"pickup_pct": pickup_pct,
"catboost_error": catboost_error,
"catboost_pct": catboost_pct,
"best_model": best_model
}
)
db.commit()
logger.info(f"Accuracy calculation completed for {calc_date}")
except Exception as e:
logger.error(f"Accuracy calculation failed: {e}")
db.rollback()
raise
finally:
db.close()