763e06c127
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
"""Deteccion de tendencia en una serie via regresion lineal simple (grupo eda)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from scipy.stats import linregress
|
|
|
|
|
|
def trend_slope(values: list, x: list = None) -> dict:
|
|
"""Detecta la tendencia (sube/baja/plana) de una serie y su significancia.
|
|
|
|
Ajusta una regresion lineal simple (minimos cuadrados) de ``values`` sobre
|
|
``x`` y resume el resultado en una direccion legible mas estadisticos. Si
|
|
``x`` es ``None`` se usa el indice posicional ``0..n-1``. Los pares cuyo
|
|
valor (en ``values`` o ``x``) sea ``None`` o ``NaN`` se descartan antes del
|
|
ajuste, de modo que series con huecos se manejan sin fallar.
|
|
|
|
Funcion pura y determinista: no hace I/O, no muta los inputs.
|
|
|
|
Args:
|
|
values: serie de valores numericos (la variable dependiente, eje Y).
|
|
x: posiciones de cada valor (la variable independiente, eje X). Si es
|
|
``None`` se usa ``range(len(values))``. Debe tener la misma longitud
|
|
que ``values`` cuando se proporciona.
|
|
|
|
Returns:
|
|
dict con la pendiente y el resumen de la tendencia:
|
|
|
|
- ``slope``: pendiente de la recta ajustada (float) o ``None`` si no
|
|
hay suficientes pares validos.
|
|
- ``intercept``: ordenada en el origen (float).
|
|
- ``r``: coeficiente de correlacion de Pearson (float).
|
|
- ``r_squared``: ``r**2``, fraccion de varianza explicada (float).
|
|
- ``p_value``: p-valor del test de pendiente nula (float).
|
|
- ``std_err``: error estandar de la pendiente (float).
|
|
- ``direction``: ``"up"`` (slope > 0 y significativa), ``"down"``
|
|
(slope < 0 y significativa), ``"flat"`` (no significativa) o
|
|
``"unknown"`` (menos de 3 pares validos).
|
|
- ``significant``: ``True`` si ``p_value < 0.05``.
|
|
- ``n``: numero de pares validos usados en el ajuste.
|
|
|
|
Con menos de 3 pares validos devuelve
|
|
``{"slope": None, "direction": "unknown", "significant": False,
|
|
"n": <n>}``.
|
|
"""
|
|
xs_raw = list(range(len(values))) if x is None else list(x)
|
|
ys_raw = list(values)
|
|
|
|
xs: list[float] = []
|
|
ys: list[float] = []
|
|
for xi, yi in zip(xs_raw, ys_raw):
|
|
if xi is None or yi is None:
|
|
continue
|
|
if isinstance(xi, float) and math.isnan(xi):
|
|
continue
|
|
if isinstance(yi, float) and math.isnan(yi):
|
|
continue
|
|
xs.append(float(xi))
|
|
ys.append(float(yi))
|
|
|
|
n = len(xs)
|
|
if n < 3:
|
|
return {"slope": None, "direction": "unknown", "significant": False, "n": n}
|
|
|
|
result = linregress(xs, ys)
|
|
slope = float(result.slope)
|
|
p_value = float(result.pvalue)
|
|
r = float(result.rvalue)
|
|
|
|
significant = p_value < 0.05
|
|
if not significant:
|
|
direction = "flat"
|
|
elif slope > 0:
|
|
direction = "up"
|
|
elif slope < 0:
|
|
direction = "down"
|
|
else:
|
|
direction = "flat"
|
|
|
|
return {
|
|
"slope": slope,
|
|
"intercept": float(result.intercept),
|
|
"r": r,
|
|
"r_squared": r * r,
|
|
"p_value": p_value,
|
|
"std_err": float(result.stderr),
|
|
"direction": direction,
|
|
"significant": significant,
|
|
"n": n,
|
|
}
|