fix: resolve governance gaps, patch credential exposure, and connect multi-platform dashboard
HIFFI Multi-Platform CI / test (3.12) (push) Successful in 33s
HIFFI Multi-Platform CI / test (3.11) (push) Successful in 40s

This commit is contained in:
Srilekha committed 2026-10-27 10:23:02 +05:30
1 parent 6f2e8e0c9d
commit 7fb7d775d8
5 files changed
+324 -387

No files matched your search

+188 -265
View File
@@ -1,30 +1,30 @@
""" """
app.py app.py
HIFFI Artist Intelligence - Executive Dashboard HIFFI Artist Intelligence - Multi-Platform Executive Dashboard
Clean dark-themed design with aligned dataframes, responsive cards, and scouting dossier. Clean dark-themed design with multi-platform evidence tiers, Spotify & Instagram telemetry,
breakout velocity index, and deep-dive scouting dossier.
""" """
import io import io
import os from pathlib import Path
import pandas as pd import pandas as pd
import plotly.express as px import plotly.express as px
import plotly.graph_objects as go
import streamlit as st import streamlit as st
st.set_page_config( st.set_page_config(
page_title="HIFFI | Artist Intelligence", page_title="HIFFI | Multi-Platform Intelligence",
page_icon="⚡", page_icon="⚡",
layout="wide", layout="wide",
initial_sidebar_state="expanded", initial_sidebar_state="expanded",
) )
DATA_PATH = "enriched_artists.csv" DATA_PATH = Path("data/hiffi_multiplatform_500.csv")
FALLBACK_DATA_PATH = Path("enriched_artists.csv")
# Global CSS Polish # Global CSS Polish
st.markdown( st.markdown(
""" """
<style> <style>
/* Global Container Spacing */
.block-container { .block-container {
padding-top: 1.8rem; padding-top: 1.8rem;
padding-bottom: 2.5rem; padding-bottom: 2.5rem;
@@ -71,7 +71,7 @@ st.markdown(
.podium-silver { border-top: 4px solid #94a3b8; } .podium-silver { border-top: 4px solid #94a3b8; }
.podium-bronze { border-top: 4px solid #f97316; } .podium-bronze { border-top: 4px solid #f97316; }
/* Health Badges */ /* Health & Tier Badges */
.badge { .badge {
display: inline-block; display: inline-block;
padding: 3px 8px; padding: 3px 8px;
@@ -80,11 +80,11 @@ st.markdown(
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
} }
.badge-active { background: rgba(16, 185, 129, 0.15); color: #10b981; } .badge-tier3 { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.badge-evergreen { background: rgba(56, 189, 248, 0.15); color: #38bdf8; } .badge-tier2 { background: rgba(56, 189, 248, 0.2); color: #38bdf8; }
.badge-dormant { background: rgba(239, 68, 68, 0.15); color: #ef4444; } .badge-tier1 { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.badge-dormant { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
/* Clean Section Dividers */
.custom-divider { .custom-divider {
height: 1px; height: 1px;
background: #1f2937; background: #1f2937;
@@ -98,24 +98,44 @@ st.markdown(
@st.cache_data(ttl=60) @st.cache_data(ttl=60)
def load_data(): def load_data():
if not os.path.exists(DATA_PATH): target = DATA_PATH if DATA_PATH.exists() else FALLBACK_DATA_PATH
if not target.exists():
return pd.DataFrame() return pd.DataFrame()
df = pd.read_csv(DATA_PATH)
df = pd.read_csv(target)
# Normalize primary score & rank columns if running from legacy source
if "hiffi_xp_published_rank" not in df.columns:
df["hiffi_xp_published_rank"] = df.get("hiffi_xp_rank", df.get("hiffi_rank", df.index + 1))
if "hiffi_xp_score" not in df.columns:
df["hiffi_xp_score"] = df.get("production_hiffi_score", df.get("hiffi_score", 0.0))
if "platform_evidence_tier" not in df.columns:
has_yt = df.get("production_hiffi_score", df.get("hiffi_score")).notna().astype(int)
has_sp = df.get("spotify_monthly_listeners", pd.Series(index=df.index)).notna().astype(int)
has_ig = df.get("instagram_followers", pd.Series(index=df.index)).notna().astype(int)
df["platform_evidence_tier"] = has_yt + has_sp + has_ig
numeric_cols = [ numeric_cols = [
"hiffi_score", "hiffi_xp_published_rank",
"hiffi_xp_rank",
"hiffi_xp_score",
"production_hiffi_rank",
"production_hiffi_score",
"platform_evidence_tier",
"breakout_velocity_index", "breakout_velocity_index",
"subscriber_count", "subscriber_count",
"daily_view_velocity", "daily_view_velocity",
"momentum_30d", "momentum_30d",
"spotify_monthly_listeners",
"spotify_daily_change",
"instagram_followers",
"confidence_score", "confidence_score",
"hiffi_rank", "breakout_rank"
"breakout_rank",
] ]
for col in numeric_cols: for col in numeric_cols:
if col in df.columns: if col in df.columns:
df[col] = pd.to_numeric(df[col], errors="coerce") df[col] = pd.to_numeric(df[col], errors="coerce")
# Fallback assignment for subgenre if not yet computed in pipeline
if "subgenre" not in df.columns: if "subgenre" not in df.columns:
def infer_subgenre(r): def infer_subgenre(r):
subs = r.get("subscriber_count", 0) or 0 subs = r.get("subscriber_count", 0) or 0
@@ -132,7 +152,6 @@ def load_data():
def to_excel_bytes(dataframe: pd.DataFrame) -> bytes: def to_excel_bytes(dataframe: pd.DataFrame) -> bytes:
"""Converts dataframe to downloadable in-memory Excel spreadsheet."""
output = io.BytesIO() output = io.BytesIO()
with pd.ExcelWriter(output, engine="openpyxl") as writer: with pd.ExcelWriter(output, engine="openpyxl") as writer:
dataframe.to_excel(writer, index=False, sheet_name="HIFFI_Cohort") dataframe.to_excel(writer, index=False, sheet_name="HIFFI_Cohort")
@@ -143,92 +162,71 @@ df = load_data()
# ----------------- Sidebar Controls ----------------- # ----------------- Sidebar Controls -----------------
with st.sidebar: with st.sidebar:
st.markdown("### ⚡ HIFFI Intelligence") st.markdown("### ⚡ HIFFI Multi-Platform")
st.caption("Hip-Hop Market Scoring & Breakout Detection") st.caption("YouTube • Spotify • Instagram Analytics")
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True) st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
if df.empty: if df.empty:
st.error(f"'{DATA_PATH}' not found. Please run 'python main.py' first!") st.error("No dataset found. Run 'python run_pipeline.py' to generate master data.")
st.stop() st.stop()
view_mode = st.radio( view_mode = st.radio(
"Leaderboard Perspective:", "Leaderboard Perspective:",
["🏆 Overall HIFFI Rank", "🚀 Breakout Velocity (BVI)", "🔍 Master Audit Grid"], ["🏆 Published Multi-Platform Rank", "🚀 Breakout Velocity (BVI)", "🔍 Full Cohort Audit Grid"],
index=0, index=0,
) )
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True) st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
st.markdown("##### 🛡️ Filters") st.markdown("##### 🛡️ Cohort Filters")
# Subgenre / Vertical Filter
all_verticals = sorted(df["subgenre"].dropna().unique().tolist()) all_verticals = sorted(df["subgenre"].dropna().unique().tolist())
target_presets = ["Legends", "Trap & Street", "UK Drill", "Lyricists", "Breakout / Emerging"] selected_verticals = st.multiselect("Vertical", options=all_verticals, default=all_verticals)
vertical_choices = [v for v in target_presets if v in all_verticals] + [v for v in all_verticals if v not in target_presets]
selected_verticals = st.multiselect(
"Hip-Hop Vertical",
options=vertical_choices,
default=vertical_choices,
)
available_statuses = df["health_status"].dropna().unique().tolist() tier_options = sorted(df["platform_evidence_tier"].dropna().unique().astype(int).tolist(), reverse=True)
selected_statuses = st.multiselect( selected_tiers = st.multiselect("Evidence Tier", options=tier_options, default=tier_options)
"Channel Status",
options=available_statuses,
default=available_statuses,
)
min_conf = st.slider( min_xp = st.slider("Min HIFFI-XP Score", 0.0, 100.0, 0.0, 5.0)
"Min Confidence",
min_value=0.0,
max_value=1.0,
value=0.50,
step=0.05,
)
# Filter logic
filtered_df = df[ filtered_df = df[
(df["health_status"].isin(selected_statuses)) (df["subgenre"].isin(selected_verticals))
& (df["subgenre"].isin(selected_verticals)) & (df["platform_evidence_tier"].isin(selected_tiers))
& (df["confidence_score"] >= min_conf) & (df["hiffi_xp_score"].fillna(0.0) >= min_xp)
].copy() ].copy()
# Sidebar Data Export Buttons (Filters applied)
with st.sidebar: with st.sidebar:
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True) st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
st.markdown("##### 💾 Export Filtered Cohort") st.markdown("##### 💾 Export Filtered Cohort")
col_csv, col_xlsx = st.columns(2) col_csv, col_xlsx = st.columns(2)
csv_payload = filtered_df.to_csv(index=False).encode("utf-8") csv_payload = filtered_df.to_csv(index=False).encode("utf-8")
col_csv.download_button( col_csv.download_button(
label="📥 CSV", "📥 CSV",
data=csv_payload, data=csv_payload,
file_name="hiffi_filtered_cohort.csv", file_name="hiffi_multiplatform_filtered.csv",
mime="text/csv", mime="text/csv",
use_container_width=True, width="stretch",
) )
excel_payload = to_excel_bytes(filtered_df) excel_payload = to_excel_bytes(filtered_df)
col_xlsx.download_button( col_xlsx.download_button(
label="📊 Excel", "📊 Excel",
data=excel_payload, data=excel_payload,
file_name="hiffi_filtered_cohort.xlsx", file_name="hiffi_multiplatform_filtered.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
use_container_width=True, width="stretch",
) )
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True) st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
st.caption("Momentum (35%) • Velocity (30%) • Reach (20%) • Cadence (15%)") st.caption("Weights: YouTube (50%) • Spotify (30%) • Instagram (20%)")
# ----------------- Main Header ----------------- # ----------------- Main Header -----------------
head_col1, head_col2 = st.columns([3, 1]) head_col1, head_col2 = st.columns([3, 1])
with head_col1: with head_col1:
st.markdown("<h2 style='margin-bottom: 2px;'>HIFFI Artist Intelligence Hub</h2>", unsafe_allow_html=True) st.markdown("<h2 style='margin-bottom: 2px;'>HIFFI Multi-Platform Intelligence Hub</h2>", unsafe_allow_html=True)
st.markdown("<p style='color: #9ca3af; font-size: 0.92rem; margin-top: 0;'>Cross-platform tracking of catalog sustainability and audience breakout velocity.</p>", unsafe_allow_html=True) st.markdown("<p style='color: #9ca3af; font-size: 0.92rem; margin-top: 0;'>Multi-platform composite rankings with zero-imputation policy and dynamic weight redistribution.</p>", unsafe_allow_html=True)
with head_col2: with head_col2:
st.markdown( st.markdown(
f"<div style='text-align: right; color: #6b7280; font-size: 0.8rem; padding-top: 10px;'>Active Cohort: <b>{len(filtered_df)} / {len(df)}</b> artists</div>", f"<div style='text-align: right; color: #6b7280; font-size: 0.8rem; padding-top: 10px;'>Filtered Cohort: <b>{len(filtered_df)} / {len(df)}</b> artists</div>",
unsafe_allow_html=True, unsafe_allow_html=True,
) )
@@ -241,46 +239,46 @@ with k1:
<div class="kpi-box"> <div class="kpi-box">
<div class="kpi-title">Tracked Artists</div> <div class="kpi-title">Tracked Artists</div>
<div class="kpi-value">{len(df)}</div> <div class="kpi-value">{len(df)}</div>
<div class="kpi-subtitle">Monitored pipeline size</div> <div class="kpi-subtitle">Monitored Cohort</div>
</div> </div>
""", """,
unsafe_allow_html=True, unsafe_allow_html=True,
) )
with k2: with k2:
active_count = len(df[df["health_status"] == "ACTIVE_HEALTHY"]) t3_count = len(df[df["platform_evidence_tier"] == 3])
st.markdown( st.markdown(
f""" f"""
<div class="kpi-box"> <div class="kpi-box">
<div class="kpi-title" style="color: #10b981;">Active Healthy</div> <div class="kpi-title" style="color: #10b981;">Tier 3 (Complete)</div>
<div class="kpi-value" style="color: #10b981;">{active_count}</div> <div class="kpi-value" style="color: #10b981;">{t3_count}</div>
<div class="kpi-subtitle">Recent uploads & steady traction</div> <div class="kpi-subtitle">YouTube + Spotify + Instagram</div>
</div> </div>
""", """,
unsafe_allow_html=True, unsafe_allow_html=True,
) )
with k3: with k3:
evergreen_count = len(df[df["health_status"] == "CATALOG_EVERGREEN"]) sp_count = df["spotify_monthly_listeners"].notna().sum()
st.markdown( st.markdown(
f""" f"""
<div class="kpi-box"> <div class="kpi-box">
<div class="kpi-title" style="color: #38bdf8;">Catalog Evergreen</div> <div class="kpi-title" style="color: #38bdf8;">Spotify Telemetry</div>
<div class="kpi-value" style="color: #38bdf8;">{evergreen_count}</div> <div class="kpi-value" style="color: #38bdf8;">{sp_count}</div>
<div class="kpi-subtitle">Strong passive catalog streams</div> <div class="kpi-subtitle">Verified monthly listeners</div>
</div> </div>
""", """,
unsafe_allow_html=True, unsafe_allow_html=True,
) )
with k4: with k4:
zombies = len(df[df["health_status"] == "DORMANT_CATALOG"]) ig_count = df["instagram_followers"].notna().sum()
st.markdown( st.markdown(
f""" f"""
<div class="kpi-box"> <div class="kpi-box">
<div class="kpi-title" style="color: #ef4444;">Dormant / Excluded</div> <div class="kpi-title" style="color: #f59e0b;">Instagram Audience</div>
<div class="kpi-value" style="color: #ef4444;">{zombies}</div> <div class="kpi-value" style="color: #f59e0b;">{ig_count}</div>
<div class="kpi-subtitle">Disqualified (&lt;NA&gt; Rank)</div> <div class="kpi-subtitle">Verified public followings</div>
</div> </div>
""", """,
unsafe_allow_html=True, unsafe_allow_html=True,
@@ -288,31 +286,24 @@ with k4:
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True) st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
# ----------------- Top 3 Leaders Podium ----------------- # ----------------- Podium -----------------
top3 = ( top3 = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).head(3)
filtered_df[filtered_df["is_rank_eligible"] == True]
.sort_values(by="hiffi_rank", ascending=True)
.head(3)
)
if len(top3) >= 3: if len(top3) >= 3:
st.markdown("#### 👑 Market Leaders") st.markdown("#### 👑 Market Leaders (Published Rankings)")
p1, p2, p3 = st.columns(3) p1, p2, p3 = st.columns(3)
podium_data = [ podium_data = [
(p1, top3.iloc[0], "🥇 RANK #1", "podium-gold", "#f59e0b"), (p1, top3.iloc[0], "🥇 PUBLISHED #1", "podium-gold", "#f59e0b"),
(p2, top3.iloc[1], "🥈 RANK #2", "podium-silver", "#94a3b8"), (p2, top3.iloc[1], "🥈 PUBLISHED #2", "podium-silver", "#94a3b8"),
(p3, top3.iloc[2], "🥉 RANK #3", "podium-bronze", "#f97316"), (p3, top3.iloc[2], "🥉 PUBLISHED #3", "podium-bronze", "#f97316"),
] ]
for col, row_data, rank_label, css_class, accent_color in podium_data: for col, row_data, rank_label, css_class, accent_color in podium_data:
badge_cls = ( tier_val = int(row_data.get("platform_evidence_tier", 1))
"badge-active" badge_cls = f"badge-tier{tier_val}"
if row_data["health_status"] == "ACTIVE_HEALTHY" sp_str = f"{int(row_data['spotify_monthly_listeners']):,}" if pd.notna(row_data.get("spotify_monthly_listeners")) else "NaN"
else "badge-evergreen" ig_str = f"{int(row_data['instagram_followers']):,}" if pd.notna(row_data.get("instagram_followers")) else "NaN"
)
bvi_str = f"{row_data['breakout_velocity_index']:.1f}" if pd.notna(row_data.get("breakout_velocity_index")) else "N/A"
vel_str = f"{int(row_data['daily_view_velocity']):,}" if pd.notna(row_data.get("daily_view_velocity")) else "N/A"
with col: with col:
st.markdown( st.markdown(
@@ -320,22 +311,22 @@ if len(top3) >= 3:
<div class="podium-tile {css_class}"> <div class="podium-tile {css_class}">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<span style="font-weight: 800; color: {accent_color}; font-size: 0.85rem;">{rank_label}</span> <span style="font-weight: 800; color: {accent_color}; font-size: 0.85rem;">{rank_label}</span>
<span class="badge {badge_cls}">{row_data['health_status'].replace('_', ' ')}</span> <span class="badge {badge_cls}">Tier {tier_val} Evidence</span>
</div> </div>
<div style="font-size: 1.35rem; font-weight: 800; color: #ffffff; margin-bottom: 2px;">{row_data['name']}</div> <div style="font-size: 1.35rem; font-weight: 800; color: #ffffff; margin-bottom: 2px;">{row_data['name']}</div>
<div style="font-size: 0.8rem; color: #6b7280; margin-bottom: 12px;">{row_data.get('channel_title', '')}</div> <div style="font-size: 0.8rem; color: #6b7280; margin-bottom: 12px;">{row_data.get('channel_title', '')}</div>
<div style="display: flex; justify-content: space-between; background: #0b0f17; padding: 10px 14px; border-radius: 8px;"> <div style="display: flex; justify-content: space-between; background: #0b0f17; padding: 10px 14px; border-radius: 8px;">
<div> <div>
<div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">HIFFI Score</div> <div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">HIFFI-XP</div>
<div style="font-size: 1.25rem; font-weight: 800; color: {accent_color};">{row_data['hiffi_score']:.1f}</div> <div style="font-size: 1.25rem; font-weight: 800; color: {accent_color};">{row_data['hiffi_xp_score']:.2f}</div>
</div> </div>
<div> <div>
<div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">Breakout Index</div> <div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">Spotify Monthly</div>
<div style="font-size: 1.25rem; font-weight: 800; color: #a855f7;">{bvi_str}</div> <div style="font-size: 1.05rem; font-weight: 700; color: #38bdf8;">{sp_str}</div>
</div> </div>
<div> <div>
<div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">Daily Views</div> <div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">IG Followers</div>
<div style="font-size: 1.05rem; font-weight: 700; color: #e5e7eb;">{vel_str}</div> <div style="font-size: 1.05rem; font-weight: 700; color: #e5e7eb;">{ig_str}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -348,233 +339,165 @@ if len(top3) >= 3:
# ----------------- Leaderboard Grid ----------------- # ----------------- Leaderboard Grid -----------------
st.markdown(f"#### 📋 {view_mode}") st.markdown(f"#### 📋 {view_mode}")
if view_mode == "🏆 Overall HIFFI Rank": if view_mode == "🏆 Published Multi-Platform Rank":
table_df = ( table_df = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).copy()
filtered_df[filtered_df["is_rank_eligible"] == True]
.sort_values(by="hiffi_rank", ascending=True)
.copy()
)
cols = [ cols = [
"hiffi_rank", "hiffi_xp_published_rank",
"hiffi_xp_rank",
"name", "name",
"subgenre", "platform_evidence_tier",
"hiffi_score", "hiffi_xp_score",
"breakout_rank", "spotify_monthly_listeners",
"breakout_velocity_index", "instagram_followers",
"daily_view_velocity", "effective_weights",
"momentum_30d", "anomaly_flags"
"health_status",
] ]
elif view_mode == "🚀 Breakout Velocity (BVI)": elif view_mode == "🚀 Breakout Velocity (BVI)":
table_df = ( table_df = (
filtered_df[ filtered_df[filtered_df["breakout_velocity_index"].notna()]
(filtered_df["is_rank_eligible"] == True) .sort_values(by="breakout_velocity_index", ascending=False)
& (filtered_df["breakout_rank"].notna())
]
.sort_values(by="breakout_rank", ascending=True)
.copy() .copy()
) )
cols = [ cols = [
"breakout_rank",
"name", "name",
"subgenre",
"breakout_velocity_index", "breakout_velocity_index",
"hiffi_rank", "hiffi_xp_published_rank",
"hiffi_score",
"daily_view_velocity", "daily_view_velocity",
"momentum_30d",
"subscriber_count", "subscriber_count",
"health_status", "subgenre"
] ]
else: else:
table_df = filtered_df.sort_values(by=["is_rank_eligible", "hiffi_score"], ascending=[False, False]).copy() table_df = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).copy()
cols = [ cols = [
"hiffi_xp_published_rank",
"name", "name",
"subgenre", "production_hiffi_rank",
"hiffi_score", "hiffi_xp_rank",
"breakout_velocity_index", "rank_delta",
"daily_view_velocity", "platform_evidence_tier",
"subscriber_count", "hiffi_xp_score",
"health_status", "spotify_monthly_listeners",
"confidence_score", "instagram_followers",
"is_rank_eligible", "evidence_exclusions",
"anomaly_flags"
] ]
# Keep only columns that exist in DataFrame to prevent KeyError
render_cols = [c for c in cols if c in table_df.columns] render_cols = [c for c in cols if c in table_df.columns]
st.dataframe( st.dataframe(
table_df[render_cols].reset_index(drop=True), table_df[render_cols].reset_index(drop=True),
use_container_width=True, width="stretch",
height=290, height=320,
column_config={ column_config={
"hiffi_rank": st.column_config.NumberColumn("Rank", format="%d", width="small"), "hiffi_xp_published_rank": st.column_config.NumberColumn("Pub Rank", format="%d", width="small"),
"breakout_rank": st.column_config.NumberColumn("BVI Rank", format="%d", width="small"), "hiffi_xp_rank": st.column_config.NumberColumn("Raw XP Rank", format="%d", width="small"),
"production_hiffi_rank": st.column_config.NumberColumn("YT Rank", format="%d", width="small"),
"platform_evidence_tier": st.column_config.NumberColumn("Tier", format="%d", width="small"),
"name": st.column_config.TextColumn("Artist", width="medium"), "name": st.column_config.TextColumn("Artist", width="medium"),
"subgenre": st.column_config.TextColumn("Vertical", width="medium"), "hiffi_xp_score": st.column_config.ProgressColumn("HIFFI-XP Score", format="%.2f", min_value=0, max_value=100, width="medium"),
"hiffi_score": st.column_config.ProgressColumn( "breakout_velocity_index": st.column_config.ProgressColumn("BVI", format="%.1f", min_value=0, max_value=100, width="small"),
"HIFFI Score", "spotify_monthly_listeners": st.column_config.NumberColumn("Spotify Monthly", format="%,d", width="medium"),
format="%.1f", "instagram_followers": st.column_config.NumberColumn("IG Followers", format="%,d", width="medium"),
min_value=0, "effective_weights": st.column_config.TextColumn("Dynamic Redistribution", width="large"),
max_value=100, "anomaly_flags": st.column_config.TextColumn("Anomaly Status", width="medium"),
width="medium", "evidence_exclusions": st.column_config.TextColumn("Exclusions", width="large"),
),
"breakout_velocity_index": st.column_config.ProgressColumn(
"Breakout (BVI)",
format="%.1f",
min_value=0,
max_value=100,
width="medium",
),
"daily_view_velocity": st.column_config.NumberColumn(
"Daily Views / Day",
format="%,d",
width="medium",
),
"momentum_30d": st.column_config.NumberColumn(
"30d Momentum",
format="%.1f%%",
width="small",
),
"subscriber_count": st.column_config.NumberColumn(
"Subscribers",
format="%,d",
width="medium",
),
"health_status": st.column_config.TextColumn("Status", width="medium"),
"confidence_score": st.column_config.NumberColumn("Confidence", format="%.2f", width="small"),
"is_rank_eligible": st.column_config.CheckboxColumn("Eligible", width="small"),
}, },
) )
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True) st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
# ----------------- Visual Insights Charts ----------------- # ----------------- Visual Insights Charts -----------------
st.markdown("#### 📊 Market Landscape Visualizer") st.markdown("#### 📊 Multi-Platform Analytics")
c_left, c_right = st.columns(2) c_left, c_right = st.columns(2)
with c_left: with c_left:
scatter_df = filtered_df[filtered_df["is_rank_eligible"] == True].copy() chart_df = filtered_df.dropna(subset=["spotify_monthly_listeners", "instagram_followers", "hiffi_xp_score"]).copy()
fig_scatter = px.scatter( if not chart_df.empty:
scatter_df, fig_scatter = px.scatter(
x="daily_view_velocity", chart_df,
y="momentum_30d", x="spotify_monthly_listeners",
size="subscriber_count", y="instagram_followers",
color="health_status", size="hiffi_xp_score",
color_discrete_map={ color="platform_evidence_tier",
"ACTIVE_HEALTHY": "#10b981", hover_name="name",
"CATALOG_EVERGREEN": "#38bdf8", log_x=True,
"DORMANT_CATALOG": "#ef4444", log_y=True,
}, title="Spotify Monthly Listeners vs. Instagram Followers",
hover_name="name", labels={
hover_data={"daily_view_velocity": ":,d", "momentum_30d": ":.1f%"}, "spotify_monthly_listeners": "Spotify Monthly Listeners (Log Scale)",
log_x=True, "instagram_followers": "Instagram Followers (Log Scale)",
title="View Velocity vs. 30-Day Momentum", "platform_evidence_tier": "Tier",
labels={ },
"daily_view_velocity": "Daily Views / Day (Log Scale)", template="plotly_dark",
"momentum_30d": "30-Day Growth (%)", )
"health_status": "Vitality", fig_scatter.update_layout(
}, paper_bgcolor="#111827",
template="plotly_dark", plot_bgcolor="#111827",
) font=dict(color="#9ca3af"),
fig_scatter.update_layout( margin=dict(l=20, r=20, t=40, b=20),
paper_bgcolor="#111827", height=320,
plot_bgcolor="#111827", )
font=dict(color="#9ca3af"), st.plotly_chart(fig_scatter, width="stretch")
margin=dict(l=20, r=20, t=40, b=20), else:
height=320, st.info("Insufficient complete data points to render multi-platform scatter plot.")
)
st.plotly_chart(fig_scatter, use_container_width=True)
with c_right: with c_right:
bvi_df = ( tier_counts = filtered_df["platform_evidence_tier"].value_counts().reset_index()
filtered_df[filtered_df["breakout_velocity_index"].notna()] tier_counts.columns = ["Tier", "Count"]
.sort_values(by="breakout_velocity_index", ascending=True) fig_pie = px.pie(
) tier_counts,
fig_bar = px.bar( values="Count",
bvi_df, names="Tier",
x="breakout_velocity_index", title="Evidence Tier Cohort Distribution",
y="name",
orientation="h",
color="breakout_velocity_index",
color_continuous_scale="Purples",
title="Breakout Velocity Index (BVI)",
labels={"breakout_velocity_index": "BVI Score", "name": "Artist"},
template="plotly_dark", template="plotly_dark",
color="Tier",
color_discrete_map={3: "#10b981", 2: "#38bdf8", 1: "#f59e0b", 0: "#ef4444"}
) )
fig_bar.update_layout( fig_pie.update_layout(
paper_bgcolor="#111827", paper_bgcolor="#111827",
plot_bgcolor="#111827", plot_bgcolor="#111827",
font=dict(color="#9ca3af"), font=dict(color="#9ca3af"),
coloraxis_showscale=False,
margin=dict(l=20, r=20, t=40, b=20), margin=dict(l=20, r=20, t=40, b=20),
height=320, height=320,
) )
st.plotly_chart(fig_bar, use_container_width=True) st.plotly_chart(fig_pie, width="stretch")
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
# ----------------- Deep Dive Scouting Dossier ----------------- # ----------------- Deep Dive Scouting Dossier -----------------
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
st.markdown("#### 🔎 Artist Dossier") st.markdown("#### 🔎 Artist Dossier")
selected_name = st.selectbox("Inspect Artist Details:", df["name"].unique(), label_visibility="collapsed") selected_name = st.selectbox("Inspect Artist Details:", df["name"].unique(), label_visibility="collapsed")
row = df[df["name"] == selected_name].iloc[0] row = df[df["name"] == selected_name].iloc[0]
# Resolve YouTube channel direct link
yt_url = str(row.get("youtube_url", "")).strip()
yt_id = str(row.get("channel_id", row.get("youtube_channel_id", ""))).strip()
if (not yt_url or yt_url == "nan") and yt_id and yt_id != "nan":
if yt_id.startswith("UC"):
yt_url = f"https://www.youtube.com/channel/{yt_id}"
elif yt_id.startswith("@"):
yt_url = f"https://www.youtube.com/{yt_id}"
else:
yt_url = f"https://www.youtube.com/@{yt_id}"
d1, d2, d3, d4 = st.columns(4) d1, d2, d3, d4 = st.columns(4)
with d1: with d1:
h_val = row.get("hiffi_score") h_val = row.get("hiffi_xp_score")
st.metric("HIFFI Score", f"{h_val:.1f}" if pd.notna(h_val) else "N/A") st.metric("HIFFI-XP Score", f"{h_val:.2f}" if pd.notna(h_val) else "N/A")
with d2: with d2:
b_val = row.get("breakout_velocity_index") pub_rank = row.get("hiffi_xp_published_rank")
st.metric("Breakout Index (BVI)", f"{b_val:.1f}" if pd.notna(b_val) else "N/A") st.metric("Published Rank", f"#{int(pub_rank)}" if pd.notna(pub_rank) else "Unranked")
with d3: with d3:
s_val = row.get("subscriber_count") sp_l = row.get("spotify_monthly_listeners")
st.metric("Subscribers", f"{int(s_val):,}" if pd.notna(s_val) else "N/A") st.metric("Spotify Listeners", f"{int(sp_l):,}" if pd.notna(sp_l) else "Unindexed (NaN)")
with d4: with d4:
conf = row.get("confidence_score", 0.0) ig_f = row.get("instagram_followers")
tier = row.get("confidence_tier", "Standard") st.metric("Instagram Followers", f"{int(ig_f):,}" if pd.notna(ig_f) else "Unindexed (NaN)")
st.metric("Entity Confidence", f"{conf * 100:.0f}%", delta=tier, delta_color="normal")
badge_cls = (
"badge-active"
if row.get("health_status") == "ACTIVE_HEALTHY"
else "badge-evergreen"
if row.get("health_status") == "CATALOG_EVERGREEN"
else "badge-dormant"
)
# Dossier info box with integrated Official YouTube Button
st.markdown( st.markdown(
f""" f"""
<div style="background: #111827; padding: 14px 18px; border-radius: 8px; border: 1px solid #1f2937; margin-top: 10px; display: flex; justify-content: space-between; align-items: center; font-size: 0.85rem;"> <div style="background: #111827; padding: 14px 18px; border-radius: 8px; border: 1px solid #1f2937; margin-top: 10px; font-size: 0.85rem;">
<div style="display: flex; gap: 24px; align-items: center;"> <div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div><b>Artist:</b> {row['name']}</div> <div><b>Artist:</b> {row['name']}</div>
<div><b>Vertical:</b> <span style="color: #a855f7;">{row.get('subgenre', 'N/A')}</span></div> <div><b>Evidence Tier:</b> Tier {int(row.get('platform_evidence_tier', 1))}</div>
<div><b>Channel:</b> {row.get('channel_title', 'Unknown')}</div> <div><b>Instagram Status:</b> {row.get('instagram_verification_status', 'N/A')}</div>
<div><b>Status:</b> <span class="badge {badge_cls}">{row.get('health_status', 'UNKNOWN')}</span></div> <div><b>Spotify Source:</b> {row.get('spotify_data_source', 'N/A')}</div>
<div><b>Zombie Flag:</b> {'⚠️ Yes' if row.get('is_zombie') else '✅ Clean'}</div> <div><b>Effective Weights:</b> {row.get('effective_weights', 'N/A')}</div>
<div><b>Eligible for Rank:</b> {'✅ Yes' if row.get('is_rank_eligible') else '❌ Disqualified'}</div> <div><b>Anomaly Flags:</b> {row.get('anomaly_flags', 'CLEAN')}</div>
</div> </div>
</div> </div>
""", """,
unsafe_allow_html=True, unsafe_allow_html=True,
) )
st.write("")
if yt_url and yt_url != "nan":
st.link_button("▶ Open Official YouTube Channel", yt_url, use_container_width=False)
else:
st.caption("⚠️ No verified YouTube channel link available for this artist.")
+20 -4
View File
@@ -4,16 +4,28 @@ Direct diagnostic to test Spotify OAuth2 Client Credentials flow
and probe endpoint permissions for Artist data, Top Tracks, and Search. and probe endpoint permissions for Artist data, Top Tracks, and Search.
""" """
import os
import requests
import base64 import base64
import os
from pathlib import Path
import sys
import requests
from dotenv import load_dotenv
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID", "a4a008ec790d45fba6a37315a1a3d847") PROJECT_ROOT = Path(__file__).resolve().parent.parent
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET", "a5c67d9a63d340b6ae4a5bec83bca241") if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
load_dotenv(PROJECT_ROOT / ".env")
CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID", "").strip()
CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET", "").strip()
def get_token(client_id: str, client_secret: str): def get_token(client_id: str, client_secret: str):
"""Requests a bearer token via standard OAuth2 Client Credentials.""" """Requests a bearer token via standard OAuth2 Client Credentials."""
if not client_id or not client_secret:
return 0, {"error": "SPOTIFY_CLIENT_ID or SPOTIFY_CLIENT_SECRET not configured"}
auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
url = "https://accounts.spotify.com/api/token" url = "https://accounts.spotify.com/api/token"
res = requests.post( res = requests.post(
@@ -50,6 +62,10 @@ if __name__ == "__main__":
print("SPOTIFY API AUTHENTICATION & ENDPOINT PERMISSION DIAGNOSTIC") print("SPOTIFY API AUTHENTICATION & ENDPOINT PERMISSION DIAGNOSTIC")
print("=" * 80) print("=" * 80)
if not CLIENT_ID or not CLIENT_SECRET:
print("Error: Missing credentials in environment. Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in .env.")
sys.exit(1)
status, token_resp = get_token(CLIENT_ID, CLIENT_SECRET) status, token_resp = get_token(CLIENT_ID, CLIENT_SECRET)
print(f"Token Endpoint Status: {status}") print(f"Token Endpoint Status: {status}")
+39 -22
View File
@@ -12,8 +12,8 @@ Ranks artists according to HIFFI formula stages:
and effective weight serialization. and effective weight serialization.
- Publication Eligibility Gates: Decoupled public chart assigning consecutive ranks - Publication Eligibility Gates: Decoupled public chart assigning consecutive ranks
while enforcing multi-platform evidence tiers (Top 10 = Tier 3, Top 50 = Tier >= 2) while enforcing multi-platform evidence tiers (Top 10 = Tier 3, Top 50 = Tier >= 2)
conditioned on strict identity verification and UTC data freshness thresholds. conditioned on strict identity verification, status_flag validation, and UTC data freshness thresholds.
Fail-closed on missing timestamp columns or unverified statuses. Fail-closed on missing timestamp columns, failed lookups, or unverified statuses.
""" """
from typing import Optional, Tuple, Union from typing import Optional, Tuple, Union
@@ -26,7 +26,7 @@ class HIFFIRankingEngine:
@staticmethod @staticmethod
def _log_scale(series: pd.Series) -> pd.Series: def _log_scale(series: pd.Series) -> pd.Series:
valid = series.dropna() valid = series.dropna()
log_vals = np.log10(1.0 + np.maximum(valid, 0.0)) log_vals = np.log1p(np.maximum(valid.astype(float), 0.0))
s_min, s_max = log_vals.min(), log_vals.max() s_min, s_max = log_vals.min(), log_vals.max()
if np.isclose(s_max, s_min) or pd.isna(s_min): if np.isclose(s_max, s_min) or pd.isna(s_min):
return pd.Series(0.0, index=series.index) return pd.Series(0.0, index=series.index)
@@ -70,13 +70,15 @@ class HIFFIRankingEngine:
""" """
out_bvi = pd.Series(np.nan, index=df.index, dtype=float) out_bvi = pd.Series(np.nan, index=df.index, dtype=float)
status_col = df["status_flag"] if "status_flag" in df.columns else pd.Series("COMPLETE", index=df.index)
valid_mask = ( valid_mask = (
df["subscriber_count"].notna() df["subscriber_count"].notna()
& (df["subscriber_count"] >= 10000) & (df["subscriber_count"] >= 10000)
& df["daily_view_velocity"].notna() & df["daily_view_velocity"].notna()
& (df["daily_view_velocity"] > 0.0) & (df["daily_view_velocity"] > 0.0)
& (df["status_flag"] != "INACTIVE_OR_DORMANT") & (status_col != "INACTIVE_OR_DORMANT")
& (df["status_flag"] != "FAILED_LOOKUP") & (status_col != "FAILED_LOOKUP")
) )
if not valid_mask.any(): if not valid_mask.any():
@@ -123,29 +125,31 @@ class HIFFIRankingEngine:
out["breakout_velocity_index"] = self.calculate_breakout_velocity(out) out["breakout_velocity_index"] = self.calculate_breakout_velocity(out)
status_col = out["status_flag"] if "status_flag" in out.columns else pd.Series("COMPLETE", index=out.index)
has_baseline = ( has_baseline = (
out["subscriber_count"].notna() out["subscriber_count"].notna()
& out["lifetime_views"].notna() & out["lifetime_views"].notna()
& (out["status_flag"] != "FAILED_LOOKUP") & (status_col != "FAILED_LOOKUP")
) )
out["is_rank_eligible"] = ( out["is_rank_eligible"] = (
has_baseline has_baseline
& (out["confidence_score"] >= min_confidence) & (out.get("confidence_score", pd.Series(1.0, index=out.index)) >= min_confidence)
& (out["status_flag"].isin(["COMPLETE", "CATALOG_ACTIVE_NO_RECENT_UPLOAD"])) & (status_col.isin(["COMPLETE", "CATALOG_ACTIVE_NO_RECENT_UPLOAD"]))
) )
filled_momentum = out["momentum_30d"].fillna(0.0).clip(lower=-100.0, upper=300.0) filled_momentum = out["momentum_30d"].fillna(0.0).clip(lower=-100.0, upper=300.0) if "momentum_30d" in out.columns else pd.Series(0.0, index=out.index)
score_momentum = self._min_max_scale(filled_momentum) score_momentum = self._min_max_scale(filled_momentum)
filled_velocity = out["daily_view_velocity"].fillna(0.0) filled_velocity = out["daily_view_velocity"].fillna(0.0) if "daily_view_velocity" in out.columns else pd.Series(0.0, index=out.index)
score_velocity = self._log_scale(filled_velocity) score_velocity = self._log_scale(filled_velocity)
log_subs = self._log_scale(out["subscriber_count"].fillna(0.0)) log_subs = self._log_scale(out["subscriber_count"].fillna(0.0)) if "subscriber_count" in out.columns else pd.Series(0.0, index=out.index)
log_lifetime = self._log_scale(out["lifetime_views"].fillna(0.0)) log_lifetime = self._log_scale(out["lifetime_views"].fillna(0.0)) if "lifetime_views" in out.columns else pd.Series(0.0, index=out.index)
score_reach = 0.60 * log_subs + 0.40 * log_lifetime score_reach = 0.60 * log_subs + 0.40 * log_lifetime
score_consistency = self._log_scale(out["video_count"].fillna(0.0)) score_consistency = self._log_scale(out["video_count"].fillna(0.0)) if "video_count" in out.columns else pd.Series(0.0, index=out.index)
hiffi_raw = ( hiffi_raw = (
0.35 * score_momentum 0.35 * score_momentum
@@ -154,7 +158,7 @@ class HIFFIRankingEngine:
+ 0.15 * score_consistency + 0.15 * score_consistency
) * 100.0 ) * 100.0
conf_multiplier = 0.8 + 0.2 * out["confidence_score"].clip(0.0, 1.0) conf_multiplier = 0.8 + 0.2 * out.get("confidence_score", pd.Series(1.0, index=out.index)).clip(0.0, 1.0)
out["production_hiffi_score"] = (hiffi_raw * conf_multiplier).round(2) out["production_hiffi_score"] = (hiffi_raw * conf_multiplier).round(2)
eligible_mask = out["is_rank_eligible"] eligible_mask = out["is_rank_eligible"]
@@ -183,6 +187,12 @@ class HIFFIRankingEngine:
""" """
out = df.copy() out = df.copy()
# Schema aliasing for raw input fallbacks
if "production_hiffi_score" not in out.columns and "hiffi_score" in out.columns:
out["production_hiffi_score"] = out["hiffi_score"]
if "production_hiffi_rank" not in out.columns and "hiffi_rank" in out.columns:
out["production_hiffi_rank"] = out["hiffi_rank"]
base_weights = { base_weights = {
"yt": 0.50, "yt": 0.50,
"sp_monthly": 0.25, "sp_monthly": 0.25,
@@ -311,7 +321,8 @@ class HIFFIRankingEngine:
- Top 10 requires all 3 platforms (Tier 3), strictly verified and fresh. - Top 10 requires all 3 platforms (Tier 3), strictly verified and fresh.
- Top 50 requires at least 2 platforms (Tier >= 2), strictly verified and fresh. - Top 50 requires at least 2 platforms (Tier >= 2), strictly verified and fresh.
- Freshness Thresholds: YouTube <= 7d, Spotify <= 7d, Instagram <= 14d. - Freshness Thresholds: YouTube <= 7d, Spotify <= 7d, Instagram <= 14d.
- Fail-closed: missing timestamp columns or unverified statuses evaluate - Integrity Gate: Artists with status_flag == 'FAILED_LOOKUP' are disqualified from YouTube credit.
- Fail-closed: missing timestamp columns, stale records, or unverified statuses evaluate
strictly to False and are logged to evidence_exclusions. strictly to False and are logged to evidence_exclusions.
- Preserves raw hiffi_xp_score and raw hiffi_xp_rank completely unchanged. - Preserves raw hiffi_xp_score and raw hiffi_xp_rank completely unchanged.
- Produces consecutive, unique ranks in 'hiffi_xp_published_rank' (1 to N). - Produces consecutive, unique ranks in 'hiffi_xp_published_rank' (1 to N).
@@ -321,8 +332,11 @@ class HIFFIRankingEngine:
exclusions = [[] for _ in range(len(out))] exclusions = [[] for _ in range(len(out))]
# 1. YouTube Validation (Score present + Freshness <= 7 days) # 1. YouTube Validation (Score present + status not FAILED_LOOKUP + Freshness <= 7 days)
has_yt_val = out["production_hiffi_score"].notna() status_col = out["status_flag"] if "status_flag" in out.columns else pd.Series("COMPLETE", index=out.index)
has_yt_val = out["production_hiffi_score"].notna() if "production_hiffi_score" in out.columns else pd.Series(False, index=out.index)
is_lookup_failed = status_col == "FAILED_LOOKUP"
if "youtube_updated_at" in out.columns: if "youtube_updated_at" in out.columns:
yt_fresh = out["youtube_updated_at"].apply( yt_fresh = out["youtube_updated_at"].apply(
lambda x: self._is_signal_fresh(x, max_days=7.0, reference_time=ref_time) lambda x: self._is_signal_fresh(x, max_days=7.0, reference_time=ref_time)
@@ -330,13 +344,16 @@ class HIFFIRankingEngine:
else: else:
yt_fresh = pd.Series(False, index=out.index) yt_fresh = pd.Series(False, index=out.index)
for idx, (val, fresh) in enumerate(zip(has_yt_val, yt_fresh)): for idx, (val, failed, fresh) in enumerate(zip(has_yt_val, is_lookup_failed, yt_fresh)):
if val and not fresh: if failed:
exclusions[idx].append("FAILED_YOUTUBE_LOOKUP")
elif val and not fresh:
exclusions[idx].append("STALE_OR_MISSING_YOUTUBE_TIMESTAMP") exclusions[idx].append("STALE_OR_MISSING_YOUTUBE_TIMESTAMP")
valid_yt = has_yt_val & yt_fresh
valid_yt = has_yt_val & (~is_lookup_failed) & yt_fresh
# 2. Spotify Validation (Telemetry present + Freshness <= 7 days) # 2. Spotify Validation (Telemetry present + Freshness <= 7 days)
has_sp_val = out["spotify_monthly_listeners"].notna() has_sp_val = out["spotify_monthly_listeners"].notna() if "spotify_monthly_listeners" in out.columns else pd.Series(False, index=out.index)
if "spotify_updated_at" in out.columns: if "spotify_updated_at" in out.columns:
sp_fresh = out["spotify_updated_at"].apply( sp_fresh = out["spotify_updated_at"].apply(
lambda x: self._is_signal_fresh(x, max_days=7.0, reference_time=ref_time) lambda x: self._is_signal_fresh(x, max_days=7.0, reference_time=ref_time)
@@ -350,7 +367,7 @@ class HIFFIRankingEngine:
valid_sp = has_sp_val & sp_fresh valid_sp = has_sp_val & sp_fresh
# 3. Instagram Validation (Followers present + Approved Status + Freshness <= 14 days) # 3. Instagram Validation (Followers present + Approved Status + Freshness <= 14 days)
has_ig_val = out["instagram_followers"].notna() has_ig_val = out["instagram_followers"].notna() if "instagram_followers" in out.columns else pd.Series(False, index=out.index)
verified_statuses = {"AUTOMATED_VERIFIED", "MANUAL_VERIFIED"} verified_statuses = {"AUTOMATED_VERIFIED", "MANUAL_VERIFIED"}
if "instagram_verification_status" in out.columns: if "instagram_verification_status" in out.columns:
+18 -66
View File
@@ -11,7 +11,6 @@ from pathlib import Path
import sys import sys
from typing import Dict, Optional from typing import Dict, Optional
import numpy as np
import pandas as pd import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parent.parent PROJECT_ROOT = Path(__file__).resolve().parent.parent
@@ -22,6 +21,7 @@ from collectors.instagram_verifier import InstagramCandidateVerifier
from collectors.kworb_spotify_collector import KworbStreamingCollector from collectors.kworb_spotify_collector import KworbStreamingCollector
from collectors.spotify_api_collector import SpotifyAPICollector from collectors.spotify_api_collector import SpotifyAPICollector
from collectors.spotify_fallback_collector import SpotifyFallbackCollector from collectors.spotify_fallback_collector import SpotifyFallbackCollector
from core.ranking_engine import HIFFIRankingEngine
DATA_DIR = PROJECT_ROOT / "data" DATA_DIR = PROJECT_ROOT / "data"
BASE_DATASET = DATA_DIR / "hiffi_multiplatform_500.csv" BASE_DATASET = DATA_DIR / "hiffi_multiplatform_500.csv"
@@ -39,70 +39,16 @@ def load_handle_overrides() -> Dict[str, object]:
return {} return {}
def _normalize_metric(series: pd.Series) -> pd.Series:
"""
Direct array-based log-scaling and min-max normalization to [0.0, 1.0].
Preserves index alignment and avoids dictionary/map collisions.
"""
clean_series = pd.to_numeric(series, errors="coerce")
out = pd.Series(0.0, index=series.index, dtype=float)
valid_mask = clean_series.notna() & (clean_series > 0)
if not valid_mask.any():
return out
log_vals = np.log1p(clean_series[valid_mask])
min_v, max_v = log_vals.min(), log_vals.max()
if max_v > min_v:
out.loc[valid_mask] = (log_vals - min_v) / (max_v - min_v)
else:
out.loc[valid_mask] = 1.0
return out
def calculate_hiffi_xp_scores(df: pd.DataFrame) -> pd.DataFrame: def calculate_hiffi_xp_scores(df: pd.DataFrame) -> pd.DataFrame:
""" """
Computes experimental multi-platform HIFFI-XP scores and ranks. Delegates to the canonical HIFFIRankingEngine to enforce zero-imputation,
Strictly preserves production_hiffi_rank and production_hiffi_score. proportional dynamic redistribution, anomaly detection, and publication eligibility gates.
Weights: 50% YouTube production baseline, 30% Spotify telemetry, 20% Instagram reach.
""" """
out = df.copy() engine = HIFFIRankingEngine()
scored = engine.calculate_hiffi_xp(df)
# Ensure baseline columns exist flagged = engine.detect_metric_anomalies(scored)
if "production_hiffi_score" not in out.columns and "hiffi_score" in out.columns: gated = engine.apply_publication_eligibility_gates(flagged)
out["production_hiffi_score"] = out["hiffi_score"] return gated
if "production_hiffi_rank" not in out.columns and "hiffi_rank" in out.columns:
out["production_hiffi_rank"] = out["hiffi_rank"]
# 1. YouTube production baseline signal (0.0 to 1.0)
yt_score = pd.to_numeric(out.get("production_hiffi_score", out.get("hiffi_score", 0)), errors="coerce").fillna(0.0)
# If production scores are already on a 0-100 scale, normalize directly
if yt_score.max() > 1.0:
yt_norm = (yt_score / 100.0).clip(0.0, 1.0)
else:
yt_norm = _normalize_metric(yt_score)
# 2. Spotify streaming signal (0.0 to 1.0)
sp_norm = _normalize_metric(out.get("spotify_monthly_listeners"))
# 3. Instagram reach signal (0.0 to 1.0)
ig_norm = _normalize_metric(out.get("instagram_followers"))
# Multi-platform composite weighting: 50% YT + 30% SP + 20% IG
xp_composite = (0.50 * yt_norm + 0.30 * sp_norm + 0.20 * ig_norm) * 100.0
out["hiffi_xp_score"] = xp_composite.clip(lower=0.0, upper=100.0).round(2)
# Monotonic ranking (higher score -> lower rank integer)
out["hiffi_xp_rank"] = out["hiffi_xp_score"].rank(ascending=False, method="min").astype(int)
# Calculate rank delta compared to YouTube production baseline
if "production_hiffi_rank" in out.columns:
prod_rank = pd.to_numeric(out["production_hiffi_rank"], errors="coerce")
out["rank_delta"] = prod_rank - out["hiffi_xp_rank"]
return out
def run_multiplatform_pipeline( def run_multiplatform_pipeline(
@@ -125,6 +71,12 @@ def run_multiplatform_pipeline(
initial_ig_verified = df["instagram_followers"].notna().sum() if "instagram_followers" in df.columns else 0 initial_ig_verified = df["instagram_followers"].notna().sum() if "instagram_followers" in df.columns else 0
print(f"[Loader] Loaded {len(df)} checkpoints ({initial_ig_verified} verified follower records).") print(f"[Loader] Loaded {len(df)} checkpoints ({initial_ig_verified} verified follower records).")
now_utc = datetime.now(timezone.utc).isoformat()
# Ensure baseline YouTube timestamps exist for freshness validation
if "youtube_updated_at" not in df.columns or df["youtube_updated_at"].isna().all():
df["youtube_updated_at"] = df.get("updated_at", now_utc)
# 1. Instagram Candidate Verification & Overrides Ingestion # 1. Instagram Candidate Verification & Overrides Ingestion
print("\n[Phase 1] Executing Automated Instagram Verification & Provenance...") print("\n[Phase 1] Executing Automated Instagram Verification & Provenance...")
overrides = load_handle_overrides() overrides = load_handle_overrides()
@@ -137,8 +89,6 @@ def run_multiplatform_pipeline(
ig_evidence = [] ig_evidence = []
ig_dates = [] ig_dates = []
now_utc = datetime.now(timezone.utc).isoformat()
for _, row in df.iterrows(): for _, row in df.iterrows():
name = str(row["name"]).strip() name = str(row["name"]).strip()
override = overrides.get(name) override = overrides.get(name)
@@ -169,7 +119,7 @@ def run_multiplatform_pipeline(
ver_res = verifier.evaluate_candidate( ver_res = verifier.evaluate_candidate(
artist_name=name, artist_name=name,
candidate_handle=cand_handle, candidate_handle=cand_handle,
follower_count=cand_followers if pd.notna(cand_followers) else 0 follower_count=cand_followers if pd.notna(cand_followers) else None
) )
elif hasattr(verifier, "verify_candidate"): elif hasattr(verifier, "verify_candidate"):
ver_res = verifier.verify_candidate( ver_res = verifier.verify_candidate(
@@ -199,6 +149,7 @@ def run_multiplatform_pipeline(
df["instagram_verification_status"] = ig_status df["instagram_verification_status"] = ig_status
df["instagram_audit_evidence"] = ig_evidence df["instagram_audit_evidence"] = ig_evidence
df["instagram_collected_at"] = ig_dates df["instagram_collected_at"] = ig_dates
df["instagram_updated_at"] = ig_dates
# 2. Spotify Multi-Tier Telemetry Ingestion (Tier 1: Kworb -> Tier 2: Official API -> Tier 3: Embed Fallback) # 2. Spotify Multi-Tier Telemetry Ingestion (Tier 1: Kworb -> Tier 2: Official API -> Tier 3: Embed Fallback)
print("\n[Phase 2] Ingesting Spotify Telemetry (Tier 1: Kworb -> Tier 2: Official API -> Tier 3: Embed Fallback)...") print("\n[Phase 2] Ingesting Spotify Telemetry (Tier 1: Kworb -> Tier 2: Official API -> Tier 3: Embed Fallback)...")
@@ -264,8 +215,9 @@ def run_multiplatform_pipeline(
df["spotify_data_source"] = sp_source df["spotify_data_source"] = sp_source
df["spotify_status"] = sp_status df["spotify_status"] = sp_status
df["spotify_collected_at"] = sp_dates df["spotify_collected_at"] = sp_dates
df["spotify_updated_at"] = sp_dates
# 3. HIFFI-XP Score Normalization & Monotonic Ranking # 3. Canonical Scoring & Governance
scored_df = calculate_hiffi_xp_scores(df) scored_df = calculate_hiffi_xp_scores(df)
# Save finalized cohort # Save finalized cohort
+59 -30
View File
@@ -19,6 +19,7 @@ if str(PROJECT_ROOT) not in sys.path:
from core.ranking_engine import HIFFIRankingEngine from core.ranking_engine import HIFFIRankingEngine
DATASET_PATH = PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv" DATASET_PATH = PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv"
DATASET_ANCHOR_TIME = pd.Timestamp("2026-10-26T12:00:00Z")
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -170,15 +171,22 @@ def test_instagram_verifier_suspicious_follower_count():
assert followers is None or status != "AUTOMATED_VERIFIED" assert followers is None or status != "AUTOMATED_VERIFIED"
def test_spotify_fallback_null_safety(): def test_spotify_fallback_null_safety(monkeypatch):
"""Verify Spotify embed fallback safely returns structured null dictionary on unindexed items.""" """Verify Spotify embed fallback safely returns structured null dictionary on unindexed items."""
from collectors.spotify_fallback_collector import SpotifyFallbackCollector from collectors.spotify_fallback_collector import SpotifyFallbackCollector
# Mock unindexed response to avoid brittle offline network failures
class MockResponse:
status_code = 404
text = "Not Found"
monkeypatch.setattr("requests.Session.get", lambda *args, **kwargs: MockResponse())
fallback = SpotifyFallbackCollector() fallback = SpotifyFallbackCollector()
res = fallback.fetch_artist_fallback("Unknown Artist", "spotify:artist:invalid_or_missing_id_999999") res = fallback.fetch_artist_fallback("Unknown Artist", "spotify:artist:invalid_or_missing_id_999999")
assert res["spotify_monthly_listeners"] is None assert res["spotify_monthly_listeners"] is None
assert res["spotify_daily_change"] is None assert res["spotify_daily_change"] is None
assert "UNAVAILABLE" in str(res["spotify_status"]) or "EMBED" in str(res["spotify_status"]) assert "NOT_FOUND" in str(res["spotify_status"]) or "UNAVAILABLE" in str(res["spotify_status"])
def test_spotify_fallback_mocked_network_error(monkeypatch): def test_spotify_fallback_mocked_network_error(monkeypatch):
@@ -294,9 +302,7 @@ def test_score_dominance(dataset):
def test_kendall_tau_diagnostic(dataset): def test_kendall_tau_diagnostic(dataset):
""" """Evaluates rank correlation between baseline and composite rank as a diagnostic."""
Evaluates rank correlation between baseline and composite rank as a diagnostic.
"""
from scipy.stats import kendalltau from scipy.stats import kendalltau
valid = dataset.dropna(subset=["production_hiffi_rank", "hiffi_xp_rank"]).copy() valid = dataset.dropna(subset=["production_hiffi_rank", "hiffi_xp_rank"]).copy()
@@ -316,10 +322,7 @@ def test_score_quantile_spread(dataset):
def test_raw_dataset_strict_nan_retention(dataset): def test_raw_dataset_strict_nan_retention(dataset):
""" """Verifies that unindexed secondary fields preserve genuine nulls and guard against synthetic zeroes."""
Verifies that unindexed secondary fields preserve genuine nulls
and guards against synthetic zeroes or string-serialized nulls ('nan', 'none', 'null').
"""
secondary_metric_cols = [ secondary_metric_cols = [
"spotify_monthly_listeners", "spotify_monthly_listeners",
"spotify_daily_change", "spotify_daily_change",
@@ -329,16 +332,13 @@ def test_raw_dataset_strict_nan_retention(dataset):
for col in secondary_metric_cols: for col in secondary_metric_cols:
if col in dataset.columns: if col in dataset.columns:
series = dataset[col] series = dataset[col]
# 1. Ensure genuine NaN values exist for unindexed artists
assert series.isna().any(), f"Column '{col}' contains zero null values across cohort." assert series.isna().any(), f"Column '{col}' contains zero null values across cohort."
# 2. Guard against string serialization traps ('nan', 'None', 'null')
string_nulls = series.dropna().astype(str).str.strip().str.lower().isin( string_nulls = series.dropna().astype(str).str.strip().str.lower().isin(
["nan", "none", "null", "undefined", "n/a"] ["nan", "none", "null", "undefined", "n/a"]
) )
assert not string_nulls.any(), f"Column '{col}' contains string-serialized null tokens." assert not string_nulls.any(), f"Column '{col}' contains string-serialized null tokens."
# 3. Guard against numerical zero substitution
numeric_vals = pd.to_numeric(series.dropna(), errors="coerce") numeric_vals = pd.to_numeric(series.dropna(), errors="coerce")
if col in ["spotify_monthly_listeners", "instagram_followers"]: if col in ["spotify_monthly_listeners", "instagram_followers"]:
zero_records = (numeric_vals == 0).sum() zero_records = (numeric_vals == 0).sum()
@@ -349,9 +349,9 @@ def test_raw_dataset_strict_nan_retention(dataset):
def test_publication_eligibility_gates(dataset): def test_publication_eligibility_gates(dataset):
"""Verifies that publication gates enforce evidence tiers without altering raw scores.""" """Verifies that publication gates enforce evidence tiers using anchored timestamps without mutating raw scores."""
engine = HIFFIRankingEngine() engine = HIFFIRankingEngine()
gated = engine.apply_publication_eligibility_gates(dataset) gated = engine.apply_publication_eligibility_gates(dataset, reference_time=DATASET_ANCHOR_TIME)
# 1. Raw scores must remain unchanged # 1. Raw scores must remain unchanged
assert (gated["hiffi_xp_score"] == dataset["hiffi_xp_score"]).all(), ( assert (gated["hiffi_xp_score"] == dataset["hiffi_xp_score"]).all(), (
@@ -380,14 +380,44 @@ def test_publication_eligibility_gates(dataset):
assert ranks == expected_ranks, "Published ranks contain gaps or duplicates." assert ranks == expected_ranks, "Published ranks contain gaps or duplicates."
def test_publication_gate_blocks_failed_youtube_lookup():
"""Confirms artists with status_flag == 'FAILED_LOOKUP' are disqualified from YouTube evidence credit."""
engine = HIFFIRankingEngine()
now_iso = DATASET_ANCHOR_TIME.isoformat()
test_df = pd.DataFrame([
{
"name": "Failed Lookup Artist",
"status_flag": "FAILED_LOOKUP",
"production_hiffi_score": 7.98,
"youtube_updated_at": now_iso,
"spotify_monthly_listeners": 25_000_000,
"spotify_updated_at": now_iso,
"instagram_followers": 10_000_000,
"instagram_verification_status": "AUTOMATED_VERIFIED",
"instagram_updated_at": now_iso,
"hiffi_xp_score": 85.0,
"hiffi_xp_rank": 1
}
])
gated = engine.apply_publication_eligibility_gates(test_df, reference_time=DATASET_ANCHOR_TIME)
row = gated.iloc[0]
# Blocked from YouTube -> only Spotify + IG valid -> Tier 2
assert row["platform_evidence_tier"] == 2
assert "FAILED_YOUTUBE_LOOKUP" in row["evidence_exclusions"]
def test_publication_gate_blocks_unverified_identity(): def test_publication_gate_blocks_unverified_identity():
"""Confirms that UNCERTAIN_NEEDS_REVIEW status does not count toward evidence tiers.""" """Confirms that UNCERTAIN_NEEDS_REVIEW status does not count toward evidence tiers."""
engine = HIFFIRankingEngine() engine = HIFFIRankingEngine()
now_iso = pd.Timestamp.now(tz="UTC").isoformat() now_iso = DATASET_ANCHOR_TIME.isoformat()
test_df = pd.DataFrame([ test_df = pd.DataFrame([
{ {
"name": "Artist Pending Verification", "name": "Artist Pending Verification",
"status_flag": "COMPLETE",
"production_hiffi_score": 95.0, "production_hiffi_score": 95.0,
"youtube_updated_at": now_iso, "youtube_updated_at": now_iso,
"spotify_monthly_listeners": 40_000_000, "spotify_monthly_listeners": 40_000_000,
@@ -400,6 +430,7 @@ def test_publication_gate_blocks_unverified_identity():
}, },
{ {
"name": "Artist Fully Verified", "name": "Artist Fully Verified",
"status_flag": "COMPLETE",
"production_hiffi_score": 90.0, "production_hiffi_score": 90.0,
"youtube_updated_at": now_iso, "youtube_updated_at": now_iso,
"spotify_monthly_listeners": 35_000_000, "spotify_monthly_listeners": 35_000_000,
@@ -412,7 +443,7 @@ def test_publication_gate_blocks_unverified_identity():
} }
]) ])
gated = engine.apply_publication_eligibility_gates(test_df) gated = engine.apply_publication_eligibility_gates(test_df, reference_time=DATASET_ANCHOR_TIME)
pending_row = gated[gated["name"] == "Artist Pending Verification"].iloc[0] pending_row = gated[gated["name"] == "Artist Pending Verification"].iloc[0]
verified_row = gated[gated["name"] == "Artist Fully Verified"].iloc[0] verified_row = gated[gated["name"] == "Artist Fully Verified"].iloc[0]
@@ -423,11 +454,12 @@ def test_publication_gate_blocks_unverified_identity():
def test_publication_gate_missing_column_fails_closed(): def test_publication_gate_missing_column_fails_closed():
"""Confirms that omitting the verification column fails closed (gives 0 IG credit).""" """Confirms that omitting the verification column fails closed (gives 0 IG credit)."""
engine = HIFFIRankingEngine() engine = HIFFIRankingEngine()
now_iso = pd.Timestamp.now(tz="UTC").isoformat() now_iso = DATASET_ANCHOR_TIME.isoformat()
test_df = pd.DataFrame([ test_df = pd.DataFrame([
{ {
"name": "Artist No Status Column", "name": "Artist No Status Column",
"status_flag": "COMPLETE",
"production_hiffi_score": 95.0, "production_hiffi_score": 95.0,
"youtube_updated_at": now_iso, "youtube_updated_at": now_iso,
"spotify_monthly_listeners": 40_000_000, "spotify_monthly_listeners": 40_000_000,
@@ -439,18 +471,19 @@ def test_publication_gate_missing_column_fails_closed():
} }
]) ])
gated = engine.apply_publication_eligibility_gates(test_df) gated = engine.apply_publication_eligibility_gates(test_df, reference_time=DATASET_ANCHOR_TIME)
assert gated.loc[0, "platform_evidence_tier"] == 2 assert gated.loc[0, "platform_evidence_tier"] == 2
def test_publication_gate_zero_multiplatform_cohort(): def test_publication_gate_zero_multiplatform_cohort():
"""Confirms an entirely Tier 1 cohort produces consecutive ranks and flags an audited shortfall.""" """Confirms an entirely Tier 1 cohort produces consecutive ranks and flags an audited shortfall."""
engine = HIFFIRankingEngine() engine = HIFFIRankingEngine()
now_iso = pd.Timestamp.now(tz="UTC").isoformat() now_iso = DATASET_ANCHOR_TIME.isoformat()
records = [ records = [
{ {
"name": f"Solo Artist {i+1}", "name": f"Solo Artist {i+1}",
"status_flag": "COMPLETE",
"production_hiffi_score": 85.0 - i, "production_hiffi_score": 85.0 - i,
"youtube_updated_at": now_iso, "youtube_updated_at": now_iso,
"spotify_monthly_listeners": np.nan, "spotify_monthly_listeners": np.nan,
@@ -465,13 +498,10 @@ def test_publication_gate_zero_multiplatform_cohort():
] ]
tier1_df = pd.DataFrame(records) tier1_df = pd.DataFrame(records)
gated, audit = engine.apply_publication_eligibility_gates(tier1_df, return_metadata=True) gated, audit = engine.apply_publication_eligibility_gates(tier1_df, reference_time=DATASET_ANCHOR_TIME, return_metadata=True)
# Must retain unique contiguous ordering 1 to 25
assert gated["hiffi_xp_published_rank"].tolist() == list(range(1, 26)) assert gated["hiffi_xp_published_rank"].tolist() == list(range(1, 26))
# All records remain Tier 1
assert (gated["platform_evidence_tier"] == 1).all() assert (gated["platform_evidence_tier"] == 1).all()
# Shortfall must be explicitly reported
assert audit["top_10_shortfall"] is True assert audit["top_10_shortfall"] is True
assert audit["top_50_shortfall"] is True assert audit["top_50_shortfall"] is True
@@ -479,11 +509,11 @@ def test_publication_gate_zero_multiplatform_cohort():
def test_freshness_gate_stale_timestamps_demote_tier(): def test_freshness_gate_stale_timestamps_demote_tier():
"""Validates that metrics older than threshold lose evidence tier credit while retaining raw score.""" """Validates that metrics older than threshold lose evidence tier credit while retaining raw score."""
engine = HIFFIRankingEngine() engine = HIFFIRankingEngine()
ref_time = pd.Timestamp("2026-10-26T12:00:00Z") ref_time = DATASET_ANCHOR_TIME
# Artist has all metrics present, but Spotify is 10 days old (>7d) and IG is 20 days old (>14d)
test_df = pd.DataFrame([{ test_df = pd.DataFrame([{
"name": "Artist Stale Telemetry", "name": "Artist Stale Telemetry",
"status_flag": "COMPLETE",
"production_hiffi_score": 90.0, "production_hiffi_score": 90.0,
"youtube_updated_at": "2026-10-25T12:00:00Z", # 1 day old (Fresh) "youtube_updated_at": "2026-10-25T12:00:00Z", # 1 day old (Fresh)
"spotify_monthly_listeners": 25_000_000, "spotify_monthly_listeners": 25_000_000,
@@ -498,7 +528,6 @@ def test_freshness_gate_stale_timestamps_demote_tier():
gated = engine.apply_publication_eligibility_gates(test_df, reference_time=ref_time) gated = engine.apply_publication_eligibility_gates(test_df, reference_time=ref_time)
row = gated.iloc[0] row = gated.iloc[0]
# Only YouTube is fresh; Tier must demote to 1
assert row["platform_evidence_tier"] == 1 assert row["platform_evidence_tier"] == 1
assert row["hiffi_xp_score"] == 88.5 assert row["hiffi_xp_score"] == 88.5
assert "STALE_OR_MISSING_SPOTIFY_TIMESTAMP" in row["evidence_exclusions"] assert "STALE_OR_MISSING_SPOTIFY_TIMESTAMP" in row["evidence_exclusions"]
@@ -508,10 +537,11 @@ def test_freshness_gate_stale_timestamps_demote_tier():
def test_freshness_gate_missing_or_malformed_timestamp(): def test_freshness_gate_missing_or_malformed_timestamp():
"""Fails closed when timestamp string is empty, null, or malformed.""" """Fails closed when timestamp string is empty, null, or malformed."""
engine = HIFFIRankingEngine() engine = HIFFIRankingEngine()
ref_time = pd.Timestamp("2026-10-26T12:00:00Z") ref_time = DATASET_ANCHOR_TIME
test_df = pd.DataFrame([{ test_df = pd.DataFrame([{
"name": "Artist Malformed Timestamp", "name": "Artist Malformed Timestamp",
"status_flag": "COMPLETE",
"production_hiffi_score": 85.0, "production_hiffi_score": 85.0,
"youtube_updated_at": "invalid_date_format", "youtube_updated_at": "invalid_date_format",
"spotify_monthly_listeners": 15_000_000, "spotify_monthly_listeners": 15_000_000,
@@ -526,7 +556,6 @@ def test_freshness_gate_missing_or_malformed_timestamp():
gated = engine.apply_publication_eligibility_gates(test_df, reference_time=ref_time) gated = engine.apply_publication_eligibility_gates(test_df, reference_time=ref_time)
row = gated.iloc[0] row = gated.iloc[0]
# Only Instagram is valid & fresh -> Tier 1
assert row["platform_evidence_tier"] == 1 assert row["platform_evidence_tier"] == 1
assert "STALE_OR_MISSING_YOUTUBE_TIMESTAMP" in row["evidence_exclusions"] assert "STALE_OR_MISSING_YOUTUBE_TIMESTAMP" in row["evidence_exclusions"]
assert "STALE_OR_MISSING_SPOTIFY_TIMESTAMP" in row["evidence_exclusions"] assert "STALE_OR_MISSING_SPOTIFY_TIMESTAMP" in row["evidence_exclusions"]
@@ -538,6 +567,7 @@ def test_freshness_gate_missing_timestamp_columns_fail_closed():
test_df = pd.DataFrame([{ test_df = pd.DataFrame([{
"name": "Artist No Timestamps", "name": "Artist No Timestamps",
"status_flag": "COMPLETE",
"production_hiffi_score": 90.0, "production_hiffi_score": 90.0,
"spotify_monthly_listeners": 20_000_000, "spotify_monthly_listeners": 20_000_000,
"instagram_followers": 10_000_000, "instagram_followers": 10_000_000,
@@ -546,7 +576,7 @@ def test_freshness_gate_missing_timestamp_columns_fail_closed():
"hiffi_xp_rank": 1 "hiffi_xp_rank": 1
}]) }])
gated = engine.apply_publication_eligibility_gates(test_df) gated = engine.apply_publication_eligibility_gates(test_df, reference_time=DATASET_ANCHOR_TIME)
row = gated.iloc[0] row = gated.iloc[0]
assert row["platform_evidence_tier"] == 0 assert row["platform_evidence_tier"] == 0
@@ -591,5 +621,4 @@ def test_anomaly_detection_flags_suspicious_patterns():
assert flagged.loc[0, "anomaly_flags"] == "CLEAN" assert flagged.loc[0, "anomaly_flags"] == "CLEAN"
assert "SUSPICIOUS_IG_TO_YT_RATIO" in flagged.loc[1, "anomaly_flags"] assert "SUSPICIOUS_IG_TO_YT_RATIO" in flagged.loc[1, "anomaly_flags"]
assert "EXTREME_SPOTIFY_VELOCITY_RATIO" in flagged.loc[2, "anomaly_flags"] assert "EXTREME_SPOTIFY_VELOCITY_RATIO" in flagged.loc[2, "anomaly_flags"]
# Raw scores must remain unaffected
assert flagged.loc[1, "hiffi_xp_score"] == 70.0 assert flagged.loc[1, "hiffi_xp_score"] == 70.0