2026-11-06 11:55:25 +05:30
|
|
|
import math
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from typing import Any, Dict, Optional, Tuple, Union
|
2026-10-22 13:06:10 +05:30
|
|
|
import numpy as np
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
|
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
class HIFFIRankingEngine:
|
2026-11-06 11:55:25 +05:30
|
|
|
"""
|
|
|
|
|
HIFFI-XP Core Ranking Engine & Governance Suite.
|
|
|
|
|
- 45/35/20 Bounded log-scaled scoring with dynamic weight redistribution.
|
|
|
|
|
- Publication eligibility gates (timestamp freshness, identity verification, status flags).
|
|
|
|
|
- Statistical anomaly detection for ratio spikes and unnatural patterns.
|
|
|
|
|
"""
|
2026-10-22 13:06:10 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
def __init__(self):
|
|
|
|
|
self.MAX_SPOTIFY = 120_000_000.0
|
|
|
|
|
self.MAX_YT_SUBS = 70_000_000.0
|
|
|
|
|
self.MAX_YT_VELOCITY = 5_000_000.0
|
|
|
|
|
self.MAX_INSTAGRAM = 150_000_000.0
|
2026-10-22 13:06:10 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
self.w_sp = 0.45
|
|
|
|
|
self.w_yt = 0.35
|
|
|
|
|
self.w_ig = 0.20
|
2026-10-22 13:06:10 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
def _log_scale(self, val: float, max_bound: float) -> float:
|
|
|
|
|
if pd.isna(val) or val <= 0:
|
|
|
|
|
return 0.0
|
|
|
|
|
s = math.log10(float(val) + 1.0)
|
|
|
|
|
mx = math.log10(float(max_bound) + 1.0)
|
|
|
|
|
return float(np.clip((s / mx) * 100.0, 0.0, 100.0))
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
def calculate_breakout_velocity(self, df: pd.DataFrame) -> pd.Series:
|
2026-11-06 11:55:25 +05:30
|
|
|
if df.empty:
|
|
|
|
|
return pd.Series(dtype=float)
|
|
|
|
|
growth_7d = pd.to_numeric(df.get("growth_velocity_7d", 0.0), errors="coerce").fillna(0.0)
|
|
|
|
|
growth_30d = pd.to_numeric(df.get("growth_velocity_30d", 0.0), errors="coerce").fillna(0.0)
|
|
|
|
|
raw_bvi = (growth_7d * 0.6 + growth_30d * 0.4) * 10.0
|
|
|
|
|
return pd.Series(np.clip(raw_bvi, 0.0, 100.0), index=df.index).round(2)
|
2026-10-26 12:39:08 +05:30
|
|
|
|
|
|
|
|
def calculate_hiffi_xp(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
|
out = df.copy()
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
has_sp = out.get("spotify_monthly_listeners", pd.Series(0, index=out.index)).fillna(0).gt(0).astype(int)
|
|
|
|
|
has_yt = out.get("subscriber_count", pd.Series(0, index=out.index)).fillna(0).gt(0).astype(int)
|
|
|
|
|
has_ig = out.get("instagram_followers", pd.Series(0, index=out.index)).fillna(0).gt(0).astype(int)
|
2026-10-27 10:23:02 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
if "subscriber_count" not in out.columns:
|
|
|
|
|
has_yt = out.get("lifetime_views", pd.Series(0, index=out.index)).fillna(0).gt(0).astype(int)
|
|
|
|
|
if has_yt.sum() == 0 and "status_flag" in out.columns:
|
|
|
|
|
has_yt = (out["status_flag"] == "COMPLETE").astype(int)
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
out["platform_evidence_tier"] = has_sp + has_yt + has_ig
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
confidence_map = {3: 1.00, 2: 0.85, 1: 0.65, 0: 0.50}
|
|
|
|
|
out["confidence_score"] = out["platform_evidence_tier"].map(confidence_map).fillna(1.0)
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
scores = []
|
|
|
|
|
effective_weight_strings = []
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
for _, row in out.iterrows():
|
|
|
|
|
sp_val = row.get("spotify_monthly_listeners")
|
|
|
|
|
sp_clean = float(sp_val) if pd.notna(sp_val) and float(sp_val) > 0 else 0.0
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
yt_sub = row.get("subscriber_count")
|
|
|
|
|
yt_sub_clean = float(yt_sub) if pd.notna(yt_sub) and float(yt_sub) > 0 else 0.0
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
yt_vel = row.get("daily_view_velocity")
|
|
|
|
|
yt_vel_clean = float(yt_vel) if pd.notna(yt_vel) and float(yt_vel) > 0 else 0.0
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
ig_val = row.get("instagram_followers")
|
|
|
|
|
ig_clean = float(ig_val) if pd.notna(ig_val) and float(ig_val) > 0 else 0.0
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
s_sp = self._log_scale(sp_clean, self.MAX_SPOTIFY)
|
|
|
|
|
s_yt = self._log_scale(yt_sub_clean, self.MAX_YT_SUBS) * 0.70 + self._log_scale(yt_vel_clean, self.MAX_YT_VELOCITY) * 0.30
|
|
|
|
|
s_ig = self._log_scale(ig_clean, self.MAX_INSTAGRAM)
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
active_weights = {}
|
|
|
|
|
if sp_clean > 0:
|
|
|
|
|
active_weights["sp"] = self.w_sp
|
|
|
|
|
if yt_sub_clean > 0 or yt_vel_clean > 0:
|
|
|
|
|
active_weights["yt"] = self.w_yt
|
|
|
|
|
if ig_clean > 0:
|
|
|
|
|
active_weights["ig"] = self.w_ig
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
total_active = sum(active_weights.values())
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
if total_active > 0:
|
|
|
|
|
norm_sp = active_weights.get("sp", 0.0) / total_active
|
|
|
|
|
norm_yt = active_weights.get("yt", 0.0) / total_active
|
|
|
|
|
norm_ig = active_weights.get("ig", 0.0) / total_active
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
raw_score = (s_sp * norm_sp) + (s_yt * norm_yt) + (s_ig * norm_ig)
|
|
|
|
|
conf = confidence_map.get(row.get("platform_evidence_tier", 3), 1.0)
|
|
|
|
|
final_score = float(round(raw_score * conf, 2))
|
|
|
|
|
wt_str = f"sp:{round(norm_sp*100,1)}%,yt:{round(norm_yt*100,1)}%,ig:{round(norm_ig*100,1)}%"
|
|
|
|
|
else:
|
|
|
|
|
prod_score = row.get("production_hiffi_score")
|
|
|
|
|
if pd.notna(prod_score):
|
|
|
|
|
final_score = float(round(float(prod_score), 2))
|
|
|
|
|
else:
|
|
|
|
|
final_score = 0.0
|
|
|
|
|
wt_str = "unindexed"
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
scores.append(final_score)
|
|
|
|
|
effective_weight_strings.append(wt_str)
|
|
|
|
|
|
|
|
|
|
out["hiffi_xp_score"] = scores
|
|
|
|
|
out["effective_weights"] = effective_weight_strings
|
|
|
|
|
|
|
|
|
|
if "catalog_rank" not in out.columns or out["catalog_rank"].isna().any():
|
|
|
|
|
out["catalog_rank"] = out["hiffi_xp_score"].rank(ascending=False, method="min").astype(int)
|
|
|
|
|
out["hiffi_xp_published_rank"] = out["catalog_rank"]
|
2026-10-26 12:39:08 +05:30
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
def score_and_rank(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
|
return self.calculate_hiffi_xp(df)
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-10-26 12:48:15 +05:30
|
|
|
def apply_publication_eligibility_gates(
|
|
|
|
|
self,
|
|
|
|
|
df: pd.DataFrame,
|
2026-11-06 11:55:25 +05:30
|
|
|
reference_time: Optional[Any] = None,
|
2026-10-26 12:48:15 +05:30
|
|
|
return_metadata: bool = False
|
2026-11-06 11:55:25 +05:30
|
|
|
) -> Union[pd.DataFrame, Tuple[pd.DataFrame, Dict[str, Any]]]:
|
2026-10-26 12:39:08 +05:30
|
|
|
out = df.copy()
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
if reference_time is None:
|
|
|
|
|
ref_dt = datetime.now(timezone.utc)
|
|
|
|
|
elif isinstance(reference_time, datetime):
|
|
|
|
|
ref_dt = reference_time if reference_time.tzinfo else reference_time.replace(tzinfo=timezone.utc)
|
2026-10-26 12:48:15 +05:30
|
|
|
else:
|
2026-11-06 11:55:25 +05:30
|
|
|
ref_dt = pd.to_datetime(reference_time, utc=True).to_pydatetime()
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
max_staleness_days = 7.0
|
2026-10-27 10:23:02 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
def is_fresh(ts_val: Any) -> bool:
|
|
|
|
|
if pd.isna(ts_val) or ts_val is None:
|
|
|
|
|
return False
|
|
|
|
|
try:
|
|
|
|
|
dt = pd.to_datetime(ts_val, utc=True).to_pydatetime()
|
|
|
|
|
diff = (ref_dt - dt).total_seconds() / 86400.0
|
|
|
|
|
return 0.0 <= diff <= max_staleness_days
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
has_yt_col = "youtube_updated_at" in out.columns
|
|
|
|
|
has_sp_col = "spotify_updated_at" in out.columns
|
|
|
|
|
has_ig_col = "instagram_updated_at" in out.columns
|
|
|
|
|
has_ver_col = "instagram_verification_status" in out.columns
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
yt_credits = []
|
|
|
|
|
sp_credits = []
|
|
|
|
|
ig_credits = []
|
|
|
|
|
exclusions_list = []
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
for _, row in out.iterrows():
|
|
|
|
|
row_exclusions = []
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
# 1. YouTube Evaluation
|
|
|
|
|
status_flag = str(row.get("status_flag", "COMPLETE")).upper()
|
|
|
|
|
if status_flag == "FAILED_LOOKUP":
|
|
|
|
|
yt_ok = False
|
|
|
|
|
row_exclusions.append("FAILED_YOUTUBE_LOOKUP")
|
|
|
|
|
elif not has_yt_col:
|
|
|
|
|
yt_ok = False
|
|
|
|
|
row_exclusions.append("STALE_OR_MISSING_YOUTUBE_TIMESTAMP")
|
|
|
|
|
else:
|
|
|
|
|
yt_ok = is_fresh(row.get("youtube_updated_at"))
|
|
|
|
|
if not yt_ok:
|
|
|
|
|
row_exclusions.append("STALE_OR_MISSING_YOUTUBE_TIMESTAMP")
|
|
|
|
|
yt_credits.append(int(yt_ok))
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
# 2. Spotify Evaluation
|
|
|
|
|
sp_val = row.get("spotify_monthly_listeners")
|
|
|
|
|
has_sp_val = pd.notna(sp_val) and float(sp_val) > 0
|
|
|
|
|
if not has_sp_col:
|
|
|
|
|
sp_ok = False
|
|
|
|
|
row_exclusions.append("STALE_OR_MISSING_SPOTIFY_TIMESTAMP")
|
|
|
|
|
else:
|
|
|
|
|
sp_ok = has_sp_val and is_fresh(row.get("spotify_updated_at"))
|
|
|
|
|
if not sp_ok:
|
|
|
|
|
row_exclusions.append("STALE_OR_MISSING_SPOTIFY_TIMESTAMP")
|
|
|
|
|
sp_credits.append(int(sp_ok))
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
# 3. Instagram Evaluation
|
|
|
|
|
ig_val = row.get("instagram_followers")
|
|
|
|
|
has_ig_val = pd.notna(ig_val) and float(ig_val) > 0
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
if not has_ver_col:
|
|
|
|
|
ig_ver_ok = False
|
|
|
|
|
else:
|
|
|
|
|
ver_status = str(row.get("instagram_verification_status", "")).upper()
|
|
|
|
|
ig_ver_ok = ver_status in ("AUTOMATED_VERIFIED", "MANUAL_VERIFIED", "VERIFIED")
|
2026-10-26 14:48:11 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
if not has_ig_col:
|
|
|
|
|
ig_fresh = False
|
|
|
|
|
row_exclusions.append("STALE_OR_MISSING_INSTAGRAM_TIMESTAMP")
|
|
|
|
|
else:
|
|
|
|
|
ig_fresh = is_fresh(row.get("instagram_updated_at"))
|
|
|
|
|
if not ig_fresh:
|
|
|
|
|
row_exclusions.append("STALE_OR_MISSING_INSTAGRAM_TIMESTAMP")
|
2026-10-26 12:48:15 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
ig_ok = has_ig_val and ig_ver_ok and ig_fresh
|
|
|
|
|
ig_credits.append(int(ig_ok))
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
exclusions_list.append(";".join(row_exclusions) if row_exclusions else "NONE")
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
out["gated_yt_credit"] = yt_credits
|
|
|
|
|
out["gated_sp_credit"] = sp_credits
|
|
|
|
|
out["gated_ig_credit"] = ig_credits
|
|
|
|
|
out["platform_evidence_tier"] = [y + s + i for y, s, i in zip(yt_credits, sp_credits, ig_credits)]
|
|
|
|
|
out["evidence_exclusions"] = exclusions_list
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
if "hiffi_xp_published_rank" not in out.columns:
|
|
|
|
|
if "hiffi_xp_score" in out.columns:
|
|
|
|
|
out["hiffi_xp_published_rank"] = out["hiffi_xp_score"].rank(ascending=False, method="min").astype(int)
|
|
|
|
|
else:
|
|
|
|
|
out["hiffi_xp_published_rank"] = list(range(1, len(out) + 1))
|
2026-10-26 12:39:08 +05:30
|
|
|
|
2026-10-26 12:48:15 +05:30
|
|
|
if return_metadata:
|
2026-11-06 11:55:25 +05:30
|
|
|
top_10 = out.head(10)
|
|
|
|
|
top_50 = out.head(50)
|
|
|
|
|
metadata = {
|
|
|
|
|
"total_records": len(out),
|
|
|
|
|
"top_10_shortfall": bool((top_10["platform_evidence_tier"] < 3).any()),
|
|
|
|
|
"top_50_shortfall": bool((top_50["platform_evidence_tier"] < 3).any()),
|
|
|
|
|
"tier_distribution": out["platform_evidence_tier"].value_counts().to_dict(),
|
|
|
|
|
"reference_time": ref_dt.isoformat(),
|
|
|
|
|
}
|
|
|
|
|
return out, metadata
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
def detect_metric_anomalies(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
|
out = df.copy()
|
|
|
|
|
flags = []
|
|
|
|
|
|
|
|
|
|
for _, row in out.iterrows():
|
|
|
|
|
row_flags = []
|
|
|
|
|
views = float(row.get("lifetime_views") or 0.0)
|
|
|
|
|
ig = float(row.get("instagram_followers") or 0.0)
|
|
|
|
|
sp_month = float(row.get("spotify_monthly_listeners") or 0.0)
|
|
|
|
|
sp_change = float(row.get("spotify_daily_change") or 0.0)
|
|
|
|
|
|
|
|
|
|
if views > 0 and ig > 0:
|
|
|
|
|
if (ig / views) > 10.0:
|
|
|
|
|
row_flags.append("SUSPICIOUS_IG_TO_YT_RATIO")
|
|
|
|
|
|
|
|
|
|
if sp_month > 0 and sp_change > 0:
|
|
|
|
|
if (sp_change / sp_month) > 0.20:
|
|
|
|
|
row_flags.append("EXTREME_SPOTIFY_VELOCITY_RATIO")
|
|
|
|
|
|
|
|
|
|
flags.append(";".join(row_flags) if row_flags else "CLEAN")
|
|
|
|
|
|
|
|
|
|
out["anomaly_flags"] = flags
|
2026-10-26 12:39:08 +05:30
|
|
|
return out
|