File size: 4,542 Bytes
c40c447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
"""
Modelo de dominio para resultados de forecasting.

Este módulo define la entidad ForecastResult, cumpliendo con SRP.
"""

from dataclasses import dataclass
from typing import List, Dict, Any


@dataclass
class ForecastResult:
    """
    Resultado de una operación de forecasting.
    
    Encapsula los pronósticos generados, incluyendo timestamps,
    valores medianos y cuantiles.
    
    Attributes:
        timestamps: Lista de timestamps pronosticados
        median: Lista de valores medianos (cuantil 0.5)
        quantiles: Dict de cuantil -> valores (ej: {"0.1": [...], "0.9": [...]})
        series_id: Identificador de la serie
        metadata: Información adicional del forecast
    
    Example:
        >>> result = ForecastResult(
        ...     timestamps=["2025-11-10", "2025-11-11"],
        ...     median=[120.5, 122.3],
        ...     quantiles={"0.1": [115.2, 116.8], "0.9": [125.8, 127.8]},
        ...     series_id="sales_A"
        ... )
        >>> result.length
        2
    """
    
    timestamps: List[str]
    median: List[float]
    quantiles: Dict[str, List[float]]
    series_id: str = "series_0"
    metadata: Dict[str, Any] = None
    
    def __post_init__(self):
        """Validación automática al crear la instancia"""
        if self.metadata is None:
            self.metadata = {}
        self.validate()
    
    @property
    def length(self) -> int:
        """Retorna el número de períodos pronosticados"""
        return len(self.timestamps)
    
    def validate(self) -> bool:
        """
        Valida la consistencia del resultado.
        
        Returns:
            bool: True si es válido
        
        Raises:
            ValueError: Si el resultado es inválido
        """
        n = len(self.timestamps)
        
        # Validar que no esté vacío
        if n == 0:
            raise ValueError("El resultado no puede estar vacío")
        
        # Validar longitud de median
        if len(self.median) != n:
            raise ValueError(
                f"Median ({len(self.median)}) debe tener la misma longitud "
                f"que timestamps ({n})"
            )
        
        # Validar longitud de cada cuantil
        for q, values in self.quantiles.items():
            if len(values) != n:
                raise ValueError(
                    f"Cuantil {q} ({len(values)}) debe tener la misma longitud "
                    f"que timestamps ({n})"
                )
        
        # Validar que todos los valores sean numéricos
        if not all(isinstance(v, (int, float)) for v in self.median):
            raise ValueError("Median debe contener solo valores numéricos")
        
        for q, values in self.quantiles.items():
            if not all(isinstance(v, (int, float)) for v in values):
                raise ValueError(f"Cuantil {q} debe contener solo valores numéricos")
        
        return True
    
    def get_quantile(self, level: float) -> List[float]:
        """
        Obtiene los valores de un cuantil específico.
        
        Args:
            level: Nivel del cuantil (ej: 0.1, 0.5, 0.9)
        
        Returns:
            List[float]: Valores del cuantil
        
        Raises:
            KeyError: Si el cuantil no existe
        """
        key = f"{level:.3g}"
        if key not in self.quantiles:
            available = list(self.quantiles.keys())
            raise KeyError(
                f"Cuantil {level} no encontrado. Disponibles: {available}"
            )
        return self.quantiles[key]
    
    def get_interval(self, lower: float = 0.1, upper: float = 0.9) -> Dict[str, List[float]]:
        """
        Obtiene un intervalo de predicción.
        
        Args:
            lower: Cuantil inferior (default: 0.1)
            upper: Cuantil superior (default: 0.9)
        
        Returns:
            Dict con "lower", "median", "upper"
        """
        return {
            "lower": self.get_quantile(lower),
            "median": self.median,
            "upper": self.get_quantile(upper)
        }
    
    def to_dict(self) -> Dict[str, Any]:
        """
        Serializa el resultado a diccionario.
        
        Returns:
            Dict con la representación del resultado
        """
        return {
            "timestamps": self.timestamps,
            "median": self.median,
            "quantiles": self.quantiles,
            "series_id": self.series_id,
            "length": self.length,
            "metadata": self.metadata
        }