""" core/ranking_engine.py Ranks artists according to HIFFI formula stages: - Recent Momentum (35%) - View Velocity (30%) - Audience Reach (20%) - Consistency (15%) Calculates Breakout Velocity Index (BVI, 0-100) and breakout_rank with a 10,000 subscriber floor to prevent phantom accounts from ranking. """ import numpy as np import pandas as pd class HIFFIRankingEngine: @staticmethod def _log_scale(series: pd.Series) -> pd.Series: valid = series.dropna() log_vals = np.log10(1.0 + np.maximum(valid, 0.0)) s_min, s_max = log_vals.min(), log_vals.max() if np.isclose(s_max, s_min) or pd.isna(s_min): return pd.Series(0.0, index=series.index) scaled = (log_vals - s_min) / (s_max - s_min + 1e-9) return scaled.reindex(series.index) @staticmethod def _min_max_scale(series: pd.Series) -> pd.Series: valid = series.dropna() s_min, s_max = valid.min(), valid.max() if np.isclose(s_max, s_min) or pd.isna(s_min): return pd.Series(0.0, index=series.index) scaled = (valid - s_min) / (s_max - s_min + 1e-9) return scaled.reindex(series.index) def calculate_breakout_velocity(self, df: pd.DataFrame) -> pd.Series: """ Computes the Breakout Velocity Index (BVI, 0 - 100). Requires at least 10,000 subscribers and positive velocity to qualify. """ out_bvi = pd.Series(np.nan, index=df.index, dtype=float) # Qualification Gate: Must have real baseline audience (>= 10,000 subs) valid_mask = ( df["subscriber_count"].notna() & (df["subscriber_count"] >= 10000) & df["daily_view_velocity"].notna() & (df["daily_view_velocity"] > 0.0) & (df["status_flag"] != "INACTIVE_OR_DORMANT") & (df["status_flag"] != "FAILED_LOOKUP") ) if not valid_mask.any(): return out_bvi subset = df[valid_mask] subs = subset["subscriber_count"].astype(float) daily_vel = subset["daily_view_velocity"].astype(float) momentum = subset["momentum_30d"].fillna(0.0).clip(lower=-100.0, upper=300.0) # 1. Audience Velocity Ratio: Views per day relative to subscriber base v_aud = daily_vel / np.maximum(subs, 1000.0) # 2. Scaled Momentum Factor: Map [-100, 300] to [0, 1] m_signal = (momentum + 100.0) / 400.0 # 3. Log-compress velocity to manage power-law peaks log_v_aud = np.log10(1.0 + v_aud) # 4. Emergence Dampener: inverse log weight to favor emerging accounts w_emerge = 1.0 / (1.0 + np.log10(1.0 + np.maximum(subs, 1.0))) raw_bvi = (0.65 * log_v_aud + 0.35 * m_signal) * w_emerge # Min-max scale to 0-100 across valid records bvi_min = raw_bvi.min() bvi_max = raw_bvi.max() if np.isclose(bvi_max, bvi_min): scaled_bvi = pd.Series(50.0, index=subset.index) else: scaled_bvi = ((raw_bvi - bvi_min) / (bvi_max - bvi_min + 1e-9)) * 100.0 out_bvi.loc[valid_mask] = scaled_bvi.round(2) return out_bvi def score_and_rank( self, df: pd.DataFrame, min_confidence: float = 0.50, ) -> pd.DataFrame: out = df.copy() # Handle confidence score defensively if "confidence_score" in out.columns: out["confidence_score"] = pd.to_numeric( out["confidence_score"].apply( lambda x: x.get("composite_confidence", 0.0) if isinstance(x, dict) else x ), errors="coerce" ).fillna(0.0) # Compute BVI with the 10,000 sub floor out["breakout_velocity_index"] = self.calculate_breakout_velocity(out) # Verification & completeness gate has_baseline = ( out["subscriber_count"].notna() & out["lifetime_views"].notna() & (out["status_flag"] != "FAILED_LOOKUP") ) out["is_rank_eligible"] = ( has_baseline & (out["confidence_score"] >= min_confidence) & (out["status_flag"].isin(["COMPLETE", "CATALOG_ACTIVE_NO_RECENT_UPLOAD"])) ) # 1. Recent Momentum (35%) filled_momentum = out["momentum_30d"].fillna(0.0).clip(lower=-100.0, upper=300.0) score_momentum = self._min_max_scale(filled_momentum) # 2. View Velocity (30%) filled_velocity = out["daily_view_velocity"].fillna(0.0) score_velocity = self._log_scale(filled_velocity) # 3. Audience Reach (20%) log_subs = self._log_scale(out["subscriber_count"].fillna(0.0)) log_lifetime = self._log_scale(out["lifetime_views"].fillna(0.0)) score_reach = 0.60 * log_subs + 0.40 * log_lifetime # 4. Consistency (15%) score_consistency = self._log_scale(out["video_count"].fillna(0.0)) # Composite HIFFI Score (0 - 100) hiffi_raw = ( 0.35 * score_momentum + 0.30 * score_velocity + 0.20 * score_reach + 0.15 * score_consistency ) * 100.0 conf_multiplier = 0.8 + 0.2 * out["confidence_score"].clip(0.0, 1.0) out["hiffi_score"] = (hiffi_raw * conf_multiplier).round(2) # Primary HIFFI Rank eligible_mask = out["is_rank_eligible"] out["hiffi_rank"] = pd.NA out.loc[eligible_mask, "hiffi_rank"] = ( out.loc[eligible_mask, "hiffi_score"] .rank(ascending=False, method="min") .astype(int) ) # Breakout Rank (only for eligible artists that have a valid BVI) bvi_eligible_mask = eligible_mask & out["breakout_velocity_index"].notna() out["breakout_rank"] = pd.NA out.loc[bvi_eligible_mask, "breakout_rank"] = ( out.loc[bvi_eligible_mask, "breakout_velocity_index"] .rank(ascending=False, method="min") .astype(int) ) return out.sort_values( by=["is_rank_eligible", "hiffi_score"], ascending=[False, False] ).reset_index(drop=True)