"""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": }``. """ 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, }