wxindex_trend.py 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. """微信指数趋势计算工具。"""
  2. from __future__ import annotations
  3. import math
  4. from typing import Any
  5. MIN_TREND_POINTS = 4
  6. MAX_TREND_POINTS = 7
  7. UP_FIT_CHANGE_RATE = 0.04
  8. UP_WINDOW_CHANGE_RATE = 0.02
  9. DOWN_FIT_CHANGE_RATE = -0.04
  10. DOWN_WINDOW_CHANGE_RATE = -0.02
  11. def _median(values: list[float]) -> float:
  12. if not values:
  13. return 0.0
  14. ordered = sorted(values)
  15. mid = len(ordered) // 2
  16. if len(ordered) % 2:
  17. return ordered[mid]
  18. return (ordered[mid - 1] + ordered[mid]) / 2
  19. def _extract_recent_scores(series: list[dict[str, Any]]) -> list[float]:
  20. scored_rows: list[tuple[str, int, float]] = []
  21. for index, row in enumerate(series):
  22. if not isinstance(row, dict):
  23. continue
  24. try:
  25. score = float(row.get("total_score"))
  26. except (TypeError, ValueError):
  27. continue
  28. if math.isnan(score) or score < 0:
  29. continue
  30. ymd = str(row.get("ymd") or "").strip()
  31. scored_rows.append((ymd, index, score))
  32. scored_rows.sort(key=lambda item: (item[0], item[1]))
  33. return [score for _, _, score in scored_rows[-MAX_TREND_POINTS:]]
  34. def _theil_sen_slope(values: list[float]) -> float:
  35. slopes: list[float] = []
  36. for start_index, start_value in enumerate(values):
  37. for end_index in range(start_index + 1, len(values)):
  38. slopes.append((values[end_index] - start_value) / (end_index - start_index))
  39. return _median(slopes)
  40. def calc_wxindex_trend(series: list[dict[str, Any]]) -> str:
  41. """按最近 7 天整体走势计算趋势,避免被最后一天波动误导。"""
  42. scores = _extract_recent_scores(series)
  43. if len(scores) < MIN_TREND_POINTS:
  44. return "未知"
  45. log_scores = [math.log1p(score) for score in scores]
  46. slope = _theil_sen_slope(log_scores)
  47. fit_change_rate = math.expm1(slope * (len(log_scores) - 1))
  48. early_avg = sum(scores[:3]) / 3
  49. late_avg = sum(scores[-3:]) / 3
  50. window_change_rate = (late_avg - early_avg) / max(early_avg, 1.0)
  51. if fit_change_rate >= UP_FIT_CHANGE_RATE and window_change_rate >= UP_WINDOW_CHANGE_RATE:
  52. return "上升"
  53. if (
  54. fit_change_rate <= DOWN_FIT_CHANGE_RATE
  55. and window_change_rate <= DOWN_WINDOW_CHANGE_RATE
  56. ):
  57. return "下降"
  58. return "平稳"