fifty_plus.py 2.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. """M9A:抖音 50+ 受众子分(作者粉丝画像 → 0-100)。
  2. 口径(已拍板):band=50+(仅 50-=50 岁及以上,不含 41-50 中年);
  3. TGI_score=clamp(bandTGI/2,0,100)(TGI 200 满分);percent_score=clamp(band占比,0,100);
  4. 50+子分=0.6·TGI_score+0.4·percent_score。TGI=画像 preference,占比=画像 percentage,同一接口。
  5. """
  6. from __future__ import annotations
  7. from typing import Any
  8. SCHEMA_VERSION = "v4_fifty_plus.v1"
  9. BAND_KEYS = ("50-",) # 50- = 50 岁及以上(纯 50+,已弃用 41-50)
  10. def fifty_plus_score(fans_age_data: dict[str, Any] | None) -> dict[str, Any]:
  11. """输入 = 画像 fans.age.data(逐桶 {percentage, preference});输出 50+ 子分 dict。
  12. 取不到任一有效 band 桶(缺桶 / 缺 percentage / 缺 preference)→ status="incomplete"。
  13. """
  14. buckets = fans_age_data if isinstance(fans_age_data, dict) else {}
  15. pairs: list[tuple[float, float]] = []
  16. for key in BAND_KEYS:
  17. bucket = buckets.get(key)
  18. if not isinstance(bucket, dict):
  19. continue
  20. pct = _to_float(bucket.get("percentage"))
  21. tgi = _to_float(bucket.get("preference"))
  22. if pct is None or tgi is None:
  23. continue
  24. pairs.append((pct, tgi))
  25. if not pairs:
  26. return {"schema_version": SCHEMA_VERSION, "status": "incomplete", "band": "50+"}
  27. band_percent = sum(pct for pct, _ in pairs)
  28. weight = band_percent if band_percent > 0 else float(len(pairs))
  29. band_tgi = sum(pct * tgi for pct, tgi in pairs) / weight
  30. tgi_score = _clamp(band_tgi / 2)
  31. percent_score = _clamp(band_percent)
  32. score = round(0.6 * tgi_score + 0.4 * percent_score, 2)
  33. return {
  34. "schema_version": SCHEMA_VERSION,
  35. "status": "ok",
  36. "score": score,
  37. "band": "50+",
  38. "band_tgi": round(band_tgi, 2),
  39. "band_percent": round(band_percent, 2),
  40. "tgi_score": round(tgi_score, 2),
  41. "percent_score": round(percent_score, 2),
  42. }
  43. def _to_float(value: Any) -> float | None:
  44. if isinstance(value, (int, float)):
  45. return float(value)
  46. if isinstance(value, str):
  47. text = value.strip().rstrip("%").strip()
  48. try:
  49. return float(text)
  50. except ValueError:
  51. return None
  52. return None
  53. def _clamp(value: float, low: float = 0.0, high: float = 100.0) -> float:
  54. return max(low, min(high, value))