111 lines
4.8 KiB
Python
111 lines
4.8 KiB
Python
|
|
# ================================================================
|
||
|
|
# services/forecasting/app/models/validation_run.py
|
||
|
|
# ================================================================
|
||
|
|
"""
|
||
|
|
Validation run models for tracking forecast validation executions
|
||
|
|
"""
|
||
|
|
|
||
|
|
from sqlalchemy import Column, String, Integer, Float, DateTime, Text, JSON, Index
|
||
|
|
from sqlalchemy.dialects.postgresql import UUID
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
from shared.database.base import Base
|
||
|
|
|
||
|
|
|
||
|
|
class ValidationRun(Base):
|
||
|
|
"""Track forecast validation execution runs"""
|
||
|
|
__tablename__ = "validation_runs"
|
||
|
|
|
||
|
|
__table_args__ = (
|
||
|
|
Index('ix_validation_runs_tenant_created', 'tenant_id', 'started_at'),
|
||
|
|
Index('ix_validation_runs_status', 'status', 'started_at'),
|
||
|
|
Index('ix_validation_runs_orchestration', 'orchestration_run_id'),
|
||
|
|
)
|
||
|
|
|
||
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||
|
|
tenant_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||
|
|
|
||
|
|
# Link to orchestration run (if triggered by orchestrator)
|
||
|
|
orchestration_run_id = Column(UUID(as_uuid=True), nullable=True)
|
||
|
|
|
||
|
|
# Validation period
|
||
|
|
validation_start_date = Column(DateTime(timezone=True), nullable=False)
|
||
|
|
validation_end_date = Column(DateTime(timezone=True), nullable=False)
|
||
|
|
|
||
|
|
# Execution metadata
|
||
|
|
started_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||
|
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||
|
|
duration_seconds = Column(Float, nullable=True)
|
||
|
|
|
||
|
|
# Status and results
|
||
|
|
status = Column(String(50), default="pending") # pending, running, completed, failed
|
||
|
|
|
||
|
|
# Validation statistics
|
||
|
|
total_forecasts_evaluated = Column(Integer, default=0)
|
||
|
|
forecasts_with_actuals = Column(Integer, default=0)
|
||
|
|
forecasts_without_actuals = Column(Integer, default=0)
|
||
|
|
|
||
|
|
# Accuracy metrics summary (across all validated forecasts)
|
||
|
|
overall_mae = Column(Float, nullable=True)
|
||
|
|
overall_mape = Column(Float, nullable=True)
|
||
|
|
overall_rmse = Column(Float, nullable=True)
|
||
|
|
overall_r2_score = Column(Float, nullable=True)
|
||
|
|
overall_accuracy_percentage = Column(Float, nullable=True)
|
||
|
|
|
||
|
|
# Additional statistics
|
||
|
|
total_predicted_demand = Column(Float, default=0.0)
|
||
|
|
total_actual_demand = Column(Float, default=0.0)
|
||
|
|
|
||
|
|
# Breakdown by product/location (JSON)
|
||
|
|
metrics_by_product = Column(JSON, nullable=True) # {product_id: {mae, mape, ...}}
|
||
|
|
metrics_by_location = Column(JSON, nullable=True) # {location: {mae, mape, ...}}
|
||
|
|
|
||
|
|
# Performance metrics created count
|
||
|
|
metrics_records_created = Column(Integer, default=0)
|
||
|
|
|
||
|
|
# Error tracking
|
||
|
|
error_message = Column(Text, nullable=True)
|
||
|
|
error_details = Column(JSON, nullable=True)
|
||
|
|
|
||
|
|
# Execution context
|
||
|
|
triggered_by = Column(String(100), default="manual") # manual, orchestrator, scheduled
|
||
|
|
execution_mode = Column(String(50), default="batch") # batch, single_day, real_time
|
||
|
|
|
||
|
|
def __repr__(self):
|
||
|
|
return (
|
||
|
|
f"<ValidationRun(id={self.id}, tenant_id={self.tenant_id}, "
|
||
|
|
f"status={self.status}, forecasts_evaluated={self.total_forecasts_evaluated})>"
|
||
|
|
)
|
||
|
|
|
||
|
|
def to_dict(self):
|
||
|
|
"""Convert to dictionary for API responses"""
|
||
|
|
return {
|
||
|
|
'id': str(self.id),
|
||
|
|
'tenant_id': str(self.tenant_id),
|
||
|
|
'orchestration_run_id': str(self.orchestration_run_id) if self.orchestration_run_id else None,
|
||
|
|
'validation_start_date': self.validation_start_date.isoformat() if self.validation_start_date else None,
|
||
|
|
'validation_end_date': self.validation_end_date.isoformat() if self.validation_end_date else None,
|
||
|
|
'started_at': self.started_at.isoformat() if self.started_at else None,
|
||
|
|
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
|
||
|
|
'duration_seconds': self.duration_seconds,
|
||
|
|
'status': self.status,
|
||
|
|
'total_forecasts_evaluated': self.total_forecasts_evaluated,
|
||
|
|
'forecasts_with_actuals': self.forecasts_with_actuals,
|
||
|
|
'forecasts_without_actuals': self.forecasts_without_actuals,
|
||
|
|
'overall_mae': self.overall_mae,
|
||
|
|
'overall_mape': self.overall_mape,
|
||
|
|
'overall_rmse': self.overall_rmse,
|
||
|
|
'overall_r2_score': self.overall_r2_score,
|
||
|
|
'overall_accuracy_percentage': self.overall_accuracy_percentage,
|
||
|
|
'total_predicted_demand': self.total_predicted_demand,
|
||
|
|
'total_actual_demand': self.total_actual_demand,
|
||
|
|
'metrics_by_product': self.metrics_by_product,
|
||
|
|
'metrics_by_location': self.metrics_by_location,
|
||
|
|
'metrics_records_created': self.metrics_records_created,
|
||
|
|
'error_message': self.error_message,
|
||
|
|
'error_details': self.error_details,
|
||
|
|
'triggered_by': self.triggered_by,
|
||
|
|
'execution_mode': self.execution_mode,
|
||
|
|
}
|