Youtube-AI-Recomendations / src /decision_rules.py
jesuszambrano13's picture
Gesti贸n de predicci贸n finales
b865637 verified
Raw
History Blame Contribute Delete
10.5 kB
"""Reglas can贸nicas de decisi贸n para mantener consistencia entre UI, LLM, gr谩ficos y PDF.
Este m贸dulo no reemplaza los modelos. Su funci贸n es integrar sus salidas para
que el dashboard y el reporte ejecutivo no se contradigan.
"""
from __future__ import annotations
from typing import Any, Dict
def _sf(value: Any, default: float = 0.0) -> float:
try:
if value in (None, ""):
return default
return float(value)
except Exception:
return default
def _norm_text(value: Any) -> str:
return str(value or "").strip().lower()
def probability(result: Dict[str, Any] | None = None, final_rec: Dict[str, Any] | None = None, paid_xgb: Dict[str, Any] | None = None) -> float:
result = result or {}
final_rec = final_rec or {}
paid_xgb = paid_xgb or {}
return max(0.0, min(1.0, _sf(
result.get("probabilidad_rendimiento", final_rec.get("probabilidad_rendimiento", paid_xgb.get("logistic_probability", 0.0)))
)))
def visual_score_0_100(visual: Dict[str, Any] | None = None, result: Dict[str, Any] | None = None) -> float | None:
visual = visual or (result or {}).get("analisis_visual", {}) or {}
raw = visual.get("composition_score", visual.get("visual_score", visual.get("score", None)))
if raw in (None, ""):
return None
score = _sf(raw, 0.0)
return score * 100.0 if score <= 1.5 else score
def paid_score_0_100(paid_xgb: Dict[str, Any] | None = None, result: Dict[str, Any] | None = None) -> float | None:
paid_xgb = paid_xgb or (result or {}).get("xgboost_pauta", {}) or ((result or {}).get("metricas", {}) or {}).get("xgboost_pauta", {}) or {}
raw = paid_xgb.get("predicted_paid_performance_score", paid_xgb.get("rules_paid_performance_score", paid_xgb.get("raw_xgboost_paid_performance_score", None)))
if raw in (None, ""):
return None
score = _sf(raw, 0.0)
return score * 100.0 if score <= 1.5 else score
def visual_interpretation(score: float | None) -> Dict[str, str]:
if score is None:
return {
"label": "sin video",
"summary": "No hay score visual suficiente; el an谩lisis se apoya en m茅tricas, texto y pol铆ticas.",
"severity": "neutral",
}
if score <= 59:
return {
"label": "cr铆tico",
"summary": "Score visual por debajo de 60: no se recomienda invertir; requiere atenci贸n inmediata en composici贸n, legibilidad o montaje.",
"severity": "danger",
}
if score <= 70:
return {
"label": "regular",
"summary": "Score visual entre 60 y 70: es viable como prueba controlada, pero requiere ajustes de posproducci贸n y revisi贸n humana.",
"severity": "warning",
}
if score <= 80:
return {
"label": "bueno",
"summary": "Score visual entre 71 y 80: composici贸n buena; puede mejorar retenci贸n y comprensi贸n del mensaje.",
"severity": "success",
}
return {
"label": "muy bien compuesto",
"summary": "Score visual entre 81 y 100: pieza muy bien compuesta y consistente para pauta controlada.",
"severity": "success",
}
def classify_investment_decision(
*,
result: Dict[str, Any] | None = None,
final_rec: Dict[str, Any] | None = None,
paid_xgb: Dict[str, Any] | None = None,
policy: Dict[str, Any] | None = None,
visual: Dict[str, Any] | None = None,
) -> Dict[str, Any]:
"""Devuelve una decisi贸n 煤nica para UI/PDF/LLM.
Estados can贸nicos:
- invest: invertir/pautar
- adjust: ajustar antes de invertir
- no: no invertir
- review: revisi贸n humana inmediata
"""
result = result or {}
final_rec = final_rec or result
paid_xgb = paid_xgb or result.get("xgboost_pauta") or ((result.get("metricas", {}) or {}).get("xgboost_pauta")) or {}
policy = policy or result.get("analisis_politicas") or {}
visual = visual or result.get("analisis_visual") or {}
prob = probability(result, final_rec, paid_xgb)
vscore = visual_score_0_100(visual, result)
pscore = paid_score_0_100(paid_xgb, result)
policy_level = _norm_text(result.get("policy_risk_level") or final_rec.get("policy_risk_level") or policy.get("policy_risk_level") or "bajo")
policy_cap = policy.get("probability_cap")
sensitive = bool(policy.get("policy_forced_probability")) or policy_level in {"alto", "revisi贸n humana", "revision humana", "high"}
gate_passed = bool(paid_xgb.get("gate_passed", paid_xgb.get("eligible_for_paid_xgboost", False)))
eligible = bool(paid_xgb.get("eligible_for_paid_xgboost", False))
cpm = _sf(paid_xgb.get("predicted_cpm") or result.get("cpm_estimado") or 0.0)
# 1) Riesgos sensibles: pol铆ticas duras o probabilidad capada a <=20%.
if policy_level in {"revisi贸n humana", "revision humana"} or (sensitive and prob <= 0.20) or (_sf(policy_cap, 1.0) <= 0.20):
return {
"key": "review",
"label": "REVISI脫N HUMANA INMEDIATA",
"headline": "RECOMENDACI脫N: REVISI脫N HUMANA INMEDIATA",
"ui_label": "馃煟 Revisi贸n humana inmediata: contenido sensible o riesgo de pol铆ticas.",
"pdf_label": "RECOMENDACI脫N: REVISI脫N HUMANA",
"action_final": "REVISI脫N HUMANA",
"color": "violet",
"reason": "El contenido activa reglas sensibles de pol铆ticas o la probabilidad fue limitada a 20% o menos.",
"probability": prob,
"visual_score": vscore,
"paid_score": pscore,
"cpm": cpm,
"gate_passed": gate_passed,
"visual_interpretation": visual_interpretation(vscore),
}
# 2) No invertir por debilidad dura de modelo, visual o score de pauta.
if prob < 0.50 or (vscore is not None and vscore < 50) or (pscore is not None and pscore < 50):
return {
"key": "no",
"label": "NO INVIERTAS",
"headline": "RECOMENDACI脫N: NO INVERTIR",
"ui_label": "馃敶 No inviertas: las se帽ales no justifican pauta.",
"pdf_label": "RECOMENDACI脫N: NO PAUTAR",
"action_final": "NO IMPULSAR",
"color": "red",
"reason": "La probabilidad publicitaria, el score de pauta o el score visual est谩n por debajo del umbral m铆nimo.",
"probability": prob,
"visual_score": vscore,
"paid_score": pscore,
"cpm": cpm,
"gate_passed": gate_passed,
"visual_interpretation": visual_interpretation(vscore),
}
# 3) Ajustar cuando pasa el umbral, pero hay regularidad visual/CPM/score de pauta.
regular_visual = vscore is not None and 60 <= vscore <= 70
regular_paid = pscore is not None and 50 <= pscore < 65
regular_cpm = cpm >= 7.0
if (not eligible) or regular_visual or regular_paid or regular_cpm:
return {
"key": "adjust",
"label": "REALIZA AJUSTES ANTES DE INVERTIR",
"headline": "RECOMENDACI脫N: AJUSTAR ANTES DE INVERTIR",
"ui_label": "馃煛 Realiza ajustes antes de invertir: hay potencial, pero no conviene escalar todav铆a.",
"pdf_label": "RECOMENDACI脫N: AJUSTAR ANTES DE PAUTAR",
"action_final": "AJUSTAR ANTES DE IMPULSAR",
"color": "yellow",
"reason": "El video muestra se帽ales parciales, pero requiere optimizaci贸n creativa, visual o de eficiencia antes de escalar presupuesto.",
"probability": prob,
"visual_score": vscore,
"paid_score": pscore,
"cpm": cpm,
"gate_passed": gate_passed,
"visual_interpretation": visual_interpretation(vscore),
}
# 4) Invertir solo si el gate pasa y no hay bloqueos.
if prob >= 0.51 and eligible and (vscore is None or vscore >= 71) and (pscore is None or pscore >= 65):
return {
"key": "invest",
"label": "INVIERTE",
"headline": "RECOMENDACI脫N: INVERTIR",
"ui_label": "馃煝 Invierte: el video cumple el umbral de aptitud publicitaria.",
"pdf_label": "RECOMENDACI脫N: PAUTAR",
"action_final": "IMPULSAR",
"color": "green",
"reason": "El video supera el umbral de regresi贸n log铆stica y no presenta bloqueos visuales o de pol铆tica.",
"probability": prob,
"visual_score": vscore,
"paid_score": pscore,
"cpm": cpm,
"gate_passed": gate_passed,
"visual_interpretation": visual_interpretation(vscore),
}
return {
"key": "adjust",
"label": "REALIZA AJUSTES ANTES DE INVERTIR",
"headline": "RECOMENDACI脫N: AJUSTAR ANTES DE INVERTIR",
"ui_label": "馃煛 Realiza ajustes antes de invertir: se帽ales mixtas.",
"pdf_label": "RECOMENDACI脫N: AJUSTAR ANTES DE PAUTAR",
"action_final": "AJUSTAR ANTES DE IMPULSAR",
"color": "yellow",
"reason": "Las se帽ales son mixtas; conviene optimizar y volver a evaluar.",
"probability": prob,
"visual_score": vscore,
"paid_score": pscore,
"cpm": cpm,
"gate_passed": gate_passed,
"visual_interpretation": visual_interpretation(vscore),
}
def integrated_markdown(decision: Dict[str, Any]) -> str:
vscore = decision.get("visual_score")
pscore = decision.get("paid_score")
prob = _sf(decision.get("probability"), 0.0) * 100
cpm = _sf(decision.get("cpm"), 0.0)
visual_text = decision.get("visual_interpretation", {}).get("summary", "")
return (
"### Visi贸n integradora de decisi贸n\n\n"
f"**{decision.get('ui_label', decision.get('label'))}**\n\n"
f"- Probabilidad de pauta del modelo principal: **{prob:.1f}%**.\n"
f"- Score de pauta XGBoost/calibrado: **{pscore:.1f}/100**.\n" if pscore is not None else
"### Visi贸n integradora de decisi贸n\n\n"
f"**{decision.get('ui_label', decision.get('label'))}**\n\n"
f"- Probabilidad de pauta del modelo principal: **{prob:.1f}%**.\n"
) + (
f"- Score visual/composici贸n: **{vscore:.0f}/100**. {visual_text}\n" if vscore is not None else
"- Score visual/composici贸n: **sin video o sin frames suficientes**.\n"
) + (
f"- CPM estimado: **${cpm:.2f}**.\n"
f"- Raz贸n integrada: {decision.get('reason', 'Se integraron modelos, pol铆ticas y se帽ales multimodales.')}\n"
)