File size: 3,371 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
"""
Modelo de dominio para anomalías detectadas.

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

from dataclasses import dataclass
from typing import Optional


@dataclass
class AnomalyPoint:
    """
    Representa un punto con posible anomalía detectada.
    
    Attributes:
        index: Índice del punto en la serie
        value: Valor observado
        expected: Valor esperado (mediana del pronóstico)
        lower_bound: Límite inferior del intervalo de confianza
        upper_bound: Límite superior del intervalo de confianza
        is_anomaly: Indica si el punto es una anomalía
        z_score: Puntuación Z del punto (opcional)
        severity: Severidad de la anomalía (low, medium, high)
    
    Example:
        >>> point = AnomalyPoint(
        ...     index=5,
        ...     value=200.0,
        ...     expected=120.0,
        ...     lower_bound=115.0,
        ...     upper_bound=125.0,
        ...     is_anomaly=True,
        ...     z_score=4.5
        ... )
        >>> point.deviation
        80.0
        >>> point.severity
        'high'
    """
    
    index: int
    value: float
    expected: float
    lower_bound: float
    upper_bound: float
    is_anomaly: bool
    z_score: float = 0.0
    severity: Optional[str] = None
    
    def __post_init__(self):
        """Cálculo automático de severidad"""
        if self.severity is None and self.is_anomaly:
            self.severity = self._calculate_severity()
    
    @property
    def deviation(self) -> float:
        """
        Calcula la desviación del valor respecto al esperado.
        
        Returns:
            float: Diferencia absoluta entre valor y esperado
        """
        return abs(self.value - self.expected)
    
    @property
    def deviation_percentage(self) -> float:
        """
        Calcula el porcentaje de desviación.
        
        Returns:
            float: Desviación como porcentaje del valor esperado
        """
        if self.expected == 0:
            return float('inf') if self.value != 0 else 0.0
        return (self.deviation / abs(self.expected)) * 100
    
    def _calculate_severity(self) -> str:
        """
        Calcula la severidad de la anomalía basada en z_score.
        
        Returns:
            str: "low", "medium" o "high"
        """
        abs_z = abs(self.z_score)
        
        if abs_z >= 4.0:
            return "high"
        elif abs_z >= 3.0:
            return "medium"
        else:
            return "low"
    
    def is_above_expected(self) -> bool:
        """Retorna True si el valor está por encima del esperado"""
        return self.value > self.expected
    
    def is_below_expected(self) -> bool:
        """Retorna True si el valor está por debajo del esperado"""
        return self.value < self.expected
    
    def to_dict(self) -> dict:
        """Serializa el punto a diccionario"""
        return {
            "index": self.index,
            "value": self.value,
            "expected": self.expected,
            "lower_bound": self.lower_bound,
            "upper_bound": self.upper_bound,
            "is_anomaly": self.is_anomaly,
            "z_score": self.z_score,
            "severity": self.severity,
            "deviation": self.deviation,
            "deviation_percentage": self.deviation_percentage
        }