2026-11-06 11:55:25 +05:30
|
|
|
"""
|
2026-10-22 17:20:44 +05:30
|
|
|
app.py
|
2026-10-27 10:23:02 +05:30
|
|
|
HIFFI Artist Intelligence - Multi-Platform Executive Dashboard
|
2026-11-06 11:55:25 +05:30
|
|
|
Dual-Engine Production Architecture:
|
|
|
|
|
1. HIFFI Hip-Hop 500: Pure Digital Telemetry (45% Spotify, 35% YouTube, 20% Instagram), Evidence Tiers, BVI.
|
|
|
|
|
2. Global Master Catalog (17k): Empirical registry verification (CSPC, RIAA, Last.fm).
|
2026-10-22 17:20:44 +05:30
|
|
|
"""
|
|
|
|
|
|
2026-10-27 10:23:02 +05:30
|
|
|
from pathlib import Path
|
2026-11-04 16:49:39 +05:30
|
|
|
import sqlite3
|
2026-10-22 17:20:44 +05:30
|
|
|
import pandas as pd
|
|
|
|
|
import plotly.express as px
|
2026-10-28 17:07:19 +05:30
|
|
|
import plotly.graph_objects as go
|
2026-10-22 17:20:44 +05:30
|
|
|
import streamlit as st
|
|
|
|
|
|
2026-11-04 16:49:39 +05:30
|
|
|
try:
|
|
|
|
|
from core.ranking_engine import HIFFIRankingEngine
|
|
|
|
|
except ImportError:
|
|
|
|
|
HIFFIRankingEngine = None
|
|
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
st.set_page_config(
|
2026-10-27 10:23:02 +05:30
|
|
|
page_title="HIFFI | Multi-Platform Intelligence",
|
2026-10-22 17:20:44 +05:30
|
|
|
page_icon="⚡",
|
|
|
|
|
layout="wide",
|
|
|
|
|
initial_sidebar_state="expanded",
|
|
|
|
|
)
|
|
|
|
|
|
2026-11-04 16:49:39 +05:30
|
|
|
PATH_DB = Path("data/hiffi_catalog.db")
|
2026-10-31 14:43:43 +05:30
|
|
|
PATH_500 = Path("data/hiffi_multiplatform_500.csv")
|
|
|
|
|
PATH_17K = Path("data/master_artists_intelligence_17k_final.csv")
|
|
|
|
|
FALLBACK_PATH = Path("enriched_artists.csv")
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
st.markdown(
|
|
|
|
|
"""
|
|
|
|
|
<style>
|
|
|
|
|
.block-container {
|
|
|
|
|
padding-top: 1.8rem;
|
|
|
|
|
padding-bottom: 2.5rem;
|
|
|
|
|
padding-left: 2.5rem;
|
|
|
|
|
padding-right: 2.5rem;
|
|
|
|
|
max-width: 100% !important;
|
|
|
|
|
}
|
|
|
|
|
.kpi-box {
|
|
|
|
|
background: #111827;
|
|
|
|
|
border: 1px solid #1f2937;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
.kpi-title {
|
2026-11-04 16:49:39 +05:30
|
|
|
font-size: 0.72rem;
|
2026-10-22 17:20:44 +05:30
|
|
|
font-weight: 700;
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
}
|
|
|
|
|
.kpi-value {
|
2026-11-04 16:49:39 +05:30
|
|
|
font-size: 1.85rem;
|
2026-10-22 17:20:44 +05:30
|
|
|
font-weight: 800;
|
|
|
|
|
color: #f9fafb;
|
|
|
|
|
margin: 4px 0;
|
|
|
|
|
}
|
|
|
|
|
.kpi-subtitle {
|
2026-11-04 16:49:39 +05:30
|
|
|
font-size: 0.72rem;
|
2026-10-22 17:20:44 +05:30
|
|
|
color: #6b7280;
|
|
|
|
|
}
|
|
|
|
|
.podium-tile {
|
|
|
|
|
background: #111827;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 18px 20px;
|
|
|
|
|
border: 1px solid #1f2937;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
.podium-gold { border-top: 4px solid #f59e0b; }
|
|
|
|
|
.podium-silver { border-top: 4px solid #94a3b8; }
|
|
|
|
|
.podium-bronze { border-top: 4px solid #f97316; }
|
|
|
|
|
.badge {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: 3px 8px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 0.70rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
}
|
2026-10-27 10:23:02 +05:30
|
|
|
.badge-tier3 { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
|
|
|
|
.badge-tier2 { background: rgba(56, 189, 248, 0.2); color: #38bdf8; }
|
|
|
|
|
.badge-tier1 { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
2026-10-22 17:20:44 +05:30
|
|
|
.custom-divider {
|
|
|
|
|
height: 1px;
|
|
|
|
|
background: #1f2937;
|
|
|
|
|
margin: 24px 0;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
""",
|
|
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-10-31 14:43:43 +05:30
|
|
|
def load_hiffi_500():
|
2026-11-04 16:49:39 +05:30
|
|
|
df = pd.DataFrame()
|
|
|
|
|
if PATH_DB.exists():
|
|
|
|
|
try:
|
|
|
|
|
conn = sqlite3.connect(PATH_DB)
|
|
|
|
|
query = """
|
|
|
|
|
WITH latest_telemetry AS (
|
|
|
|
|
SELECT t.*, ROW_NUMBER() OVER (
|
|
|
|
|
PARTITION BY t.artist_id
|
2026-11-06 11:55:25 +05:30
|
|
|
ORDER BY t.last_synced_at DESC, t.rowid DESC
|
2026-11-04 16:49:39 +05:30
|
|
|
) as rn
|
|
|
|
|
FROM telemetry_metrics t
|
|
|
|
|
),
|
|
|
|
|
latest_rankings AS (
|
|
|
|
|
SELECT r.*, ROW_NUMBER() OVER (
|
|
|
|
|
PARTITION BY r.artist_id
|
2026-11-06 11:55:25 +05:30
|
|
|
ORDER BY r.rowid DESC
|
2026-11-04 16:49:39 +05:30
|
|
|
) as rn
|
|
|
|
|
FROM rankings r
|
|
|
|
|
)
|
|
|
|
|
SELECT
|
|
|
|
|
a.artist_id,
|
|
|
|
|
a.artist_name as name,
|
|
|
|
|
r.catalog_rank as hiffi_xp_published_rank,
|
|
|
|
|
r.tier_rank,
|
2026-11-06 11:55:25 +05:30
|
|
|
COALESCE(t.subscribers_count, 0) as subscriber_count,
|
|
|
|
|
COALESCE(t.total_views, 0) as lifetime_views,
|
|
|
|
|
COALESCE(t.spotify_monthly_listeners, 0) as spotify_monthly_listeners,
|
|
|
|
|
COALESCE(t.instagram_followers, 0) as instagram_followers,
|
|
|
|
|
COALESCE(t.daily_view_velocity, 0) as daily_view_velocity,
|
|
|
|
|
COALESCE(t.growth_velocity_7d, 0.0) as momentum_7d,
|
|
|
|
|
COALESCE(t.growth_velocity_30d, 0.0) as momentum_30d,
|
|
|
|
|
COALESCE(t.spotify_daily_change, 0) as spotify_daily_change
|
|
|
|
|
FROM rankings r
|
|
|
|
|
JOIN artists a ON r.artist_id = a.artist_id
|
2026-11-04 16:49:39 +05:30
|
|
|
LEFT JOIN latest_telemetry t ON a.artist_id = t.artist_id AND t.rn = 1
|
2026-11-06 11:55:25 +05:30
|
|
|
WHERE r.catalog_rank <= 500
|
|
|
|
|
ORDER BY r.catalog_rank ASC;
|
2026-11-04 16:49:39 +05:30
|
|
|
"""
|
|
|
|
|
df = pd.read_sql_query(query, conn)
|
|
|
|
|
conn.close()
|
|
|
|
|
except Exception:
|
|
|
|
|
df = pd.DataFrame()
|
|
|
|
|
|
|
|
|
|
if df.empty:
|
|
|
|
|
target = PATH_500 if PATH_500.exists() else FALLBACK_PATH
|
|
|
|
|
if target.exists():
|
2026-11-06 11:55:25 +05:30
|
|
|
try:
|
|
|
|
|
df = pd.read_csv(target)
|
|
|
|
|
except Exception:
|
|
|
|
|
df = pd.DataFrame()
|
2026-11-04 16:49:39 +05:30
|
|
|
|
|
|
|
|
if df.empty:
|
2026-10-22 17:20:44 +05:30
|
|
|
return pd.DataFrame()
|
2026-10-27 10:23:02 +05:30
|
|
|
|
2026-10-31 14:43:43 +05:30
|
|
|
if "name" not in df.columns and "artist_name" in df.columns:
|
|
|
|
|
df["name"] = df["artist_name"]
|
2026-11-04 16:49:39 +05:30
|
|
|
|
|
|
|
|
if "subscriber_count" not in df.columns and "subscribers_count" in df.columns:
|
|
|
|
|
df["subscriber_count"] = df["subscribers_count"]
|
2026-11-06 11:55:25 +05:30
|
|
|
elif "subscriber_count" not in df.columns:
|
|
|
|
|
df["subscriber_count"] = 0
|
2026-11-04 16:49:39 +05:30
|
|
|
|
|
|
|
|
if "lifetime_views" not in df.columns and "total_views" in df.columns:
|
|
|
|
|
df["lifetime_views"] = df["total_views"]
|
2026-11-06 11:55:25 +05:30
|
|
|
elif "lifetime_views" not in df.columns:
|
|
|
|
|
df["lifetime_views"] = 0
|
|
|
|
|
|
|
|
|
|
if "spotify_monthly_listeners" not in df.columns:
|
|
|
|
|
df["spotify_monthly_listeners"] = 0
|
|
|
|
|
|
|
|
|
|
if "instagram_followers" not in df.columns:
|
|
|
|
|
df["instagram_followers"] = 0
|
2026-11-04 16:49:39 +05:30
|
|
|
|
2026-10-27 10:23:02 +05:30
|
|
|
if "hiffi_xp_published_rank" not in df.columns:
|
2026-11-06 11:55:25 +05:30
|
|
|
df["hiffi_xp_published_rank"] = df.get("catalog_rank", df.get("hiffi_xp_rank", df.index + 1))
|
2026-11-04 16:49:39 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
has_sp = pd.to_numeric(df["spotify_monthly_listeners"], errors="coerce").fillna(0).gt(0).astype(int)
|
|
|
|
|
has_yt = pd.to_numeric(df["subscriber_count"], errors="coerce").fillna(0).gt(0).astype(int)
|
|
|
|
|
has_ig = pd.to_numeric(df["instagram_followers"], errors="coerce").fillna(0).gt(0).astype(int)
|
|
|
|
|
df["platform_evidence_tier"] = has_sp + has_yt + has_ig
|
2026-10-27 10:23:02 +05:30
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
numeric_cols = [
|
2026-11-06 11:55:25 +05:30
|
|
|
"hiffi_xp_published_rank", "hiffi_xp_rank", "hiffi_xp_score", "platform_evidence_tier",
|
|
|
|
|
"subscriber_count", "lifetime_views", "spotify_monthly_listeners", "instagram_followers",
|
|
|
|
|
"daily_view_velocity", "momentum_7d", "momentum_30d", "breakout_velocity_index"
|
2026-10-22 17:20:44 +05:30
|
|
|
]
|
|
|
|
|
for col in numeric_cols:
|
|
|
|
|
if col in df.columns:
|
|
|
|
|
df[col] = pd.to_numeric(df[col], errors="coerce")
|
2026-10-23 10:52:54 +05:30
|
|
|
|
2026-11-04 16:49:39 +05:30
|
|
|
if HIFFIRankingEngine is not None and ("breakout_velocity_index" not in df.columns or df["breakout_velocity_index"].isna().all()):
|
|
|
|
|
try:
|
2026-11-06 11:55:25 +05:30
|
|
|
eng = HIFFIRankingEngine()
|
|
|
|
|
df["breakout_velocity_index"] = eng.calculate_breakout_velocity(df)
|
2026-11-04 16:49:39 +05:30
|
|
|
except Exception:
|
|
|
|
|
df["breakout_velocity_index"] = 0.0
|
|
|
|
|
|
|
|
|
|
if "breakout_velocity_index" not in df.columns:
|
|
|
|
|
df["breakout_velocity_index"] = 0.0
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
if HIFFIRankingEngine is not None:
|
|
|
|
|
try:
|
|
|
|
|
eng = HIFFIRankingEngine()
|
|
|
|
|
ranked_df = eng.calculate_hiffi_xp(df)
|
|
|
|
|
df["hiffi_xp_score"] = ranked_df["hiffi_xp_score"]
|
|
|
|
|
df["effective_weights"] = ranked_df.get("effective_weights", "sp:45.0%,yt:35.0%,ig:20.0%")
|
|
|
|
|
df["catalog_rank"] = ranked_df.get("catalog_rank", df["hiffi_xp_published_rank"])
|
|
|
|
|
df["hiffi_xp_published_rank"] = df["catalog_rank"]
|
|
|
|
|
except Exception:
|
|
|
|
|
sp_n = df["spotify_monthly_listeners"].fillna(0) / max(df["spotify_monthly_listeners"].max(), 1)
|
|
|
|
|
yt_n = df["subscriber_count"].fillna(0) / max(df["subscriber_count"].max(), 1)
|
|
|
|
|
ig_n = df["instagram_followers"].fillna(0) / max(df["instagram_followers"].max(), 1)
|
|
|
|
|
df["hiffi_xp_score"] = (sp_n * 45.0 + yt_n * 35.0 + ig_n * 20.0).round(2)
|
|
|
|
|
df["effective_weights"] = "sp:45.0%,yt:35.0%,ig:20.0%"
|
|
|
|
|
else:
|
|
|
|
|
sp_n = df["spotify_monthly_listeners"].fillna(0) / max(df["spotify_monthly_listeners"].max(), 1)
|
|
|
|
|
yt_n = df["subscriber_count"].fillna(0) / max(df["subscriber_count"].max(), 1)
|
|
|
|
|
ig_n = df["instagram_followers"].fillna(0) / max(df["instagram_followers"].max(), 1)
|
|
|
|
|
df["hiffi_xp_score"] = (sp_n * 45.0 + yt_n * 35.0 + ig_n * 20.0).round(2)
|
|
|
|
|
df["effective_weights"] = "sp:45.0%,yt:35.0%,ig:20.0%"
|
2026-11-04 16:49:39 +05:30
|
|
|
|
|
|
|
|
if "anomaly_flags" not in df.columns:
|
|
|
|
|
df["anomaly_flags"] = "CLEAN"
|
|
|
|
|
|
2026-10-23 10:52:54 +05:30
|
|
|
if "subgenre" not in df.columns:
|
|
|
|
|
def infer_subgenre(r):
|
|
|
|
|
subs = r.get("subscriber_count", 0) or 0
|
|
|
|
|
bvi = r.get("breakout_velocity_index", 0) or 0
|
|
|
|
|
if bvi >= 70:
|
|
|
|
|
return "Breakout / Emerging"
|
|
|
|
|
elif subs >= 15000000:
|
|
|
|
|
return "Legends"
|
|
|
|
|
else:
|
|
|
|
|
return "Trap & Street"
|
|
|
|
|
df["subgenre"] = df.apply(infer_subgenre, axis=1)
|
|
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
return df
|
|
|
|
|
|
|
|
|
|
|
2026-10-31 14:43:43 +05:30
|
|
|
def load_master_17k():
|
|
|
|
|
if not PATH_17K.exists():
|
|
|
|
|
return pd.DataFrame()
|
|
|
|
|
df = pd.read_csv(PATH_17K)
|
|
|
|
|
if "name" not in df.columns and "artist_name" in df.columns:
|
|
|
|
|
df["name"] = df["artist_name"]
|
|
|
|
|
return df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
df_500 = load_hiffi_500()
|
|
|
|
|
df_17k = load_master_17k()
|
2026-10-30 11:10:54 +05:30
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
with st.sidebar:
|
2026-10-31 14:43:43 +05:30
|
|
|
st.markdown("### ⚡ HIFFI Intelligence Suite")
|
|
|
|
|
st.caption("Hip-Hop 500 & Master Catalog Intelligence")
|
2026-10-22 17:20:44 +05:30
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
2026-10-31 14:43:43 +05:30
|
|
|
catalog_source = st.selectbox(
|
2026-11-06 11:55:25 +05:30
|
|
|
"Active Data Scope:",
|
|
|
|
|
["Hip-Hop 500 (Executive Telemetry)", "Global Master Catalog (17,000+ Acts)"],
|
2026-11-04 16:49:39 +05:30
|
|
|
index=0,
|
2026-10-22 17:20:44 +05:30
|
|
|
)
|
2026-10-31 14:43:43 +05:30
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
if "Global Master Catalog" in catalog_source:
|
|
|
|
|
with st.sidebar:
|
2026-11-06 11:55:25 +05:30
|
|
|
st.markdown("##### 17k Catalog Filters")
|
2026-10-31 14:43:43 +05:30
|
|
|
if df_17k.empty:
|
|
|
|
|
st.error("Master 17k dataset not found at data/master_artists_intelligence_17k_final.csv")
|
|
|
|
|
st.stop()
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
tier_list = ["All Tiers"] + sorted(df_17k.get("catalog_intelligence_tier", pd.Series(dtype=str)).dropna().unique().tolist())
|
2026-10-31 14:43:43 +05:30
|
|
|
sel_tier = st.selectbox("Intelligence Tier:", tier_list, index=0)
|
|
|
|
|
search_17k = st.text_input("Search Artist Name:", placeholder="e.g. Eminem, The Roots...")
|
|
|
|
|
|
|
|
|
|
filt_17k = df_17k.copy()
|
2026-11-06 11:55:25 +05:30
|
|
|
if sel_tier != "All Tiers" and "catalog_intelligence_tier" in filt_17k.columns:
|
2026-10-31 14:43:43 +05:30
|
|
|
filt_17k = filt_17k[filt_17k["catalog_intelligence_tier"] == sel_tier]
|
2026-11-06 11:55:25 +05:30
|
|
|
if search_17k and "name" in filt_17k.columns:
|
2026-10-31 14:43:43 +05:30
|
|
|
filt_17k = filt_17k[filt_17k["name"].str.contains(search_17k, case=False, na=False)]
|
|
|
|
|
|
|
|
|
|
head1, head2 = st.columns([3, 1])
|
|
|
|
|
with head1:
|
2026-11-06 11:55:25 +05:30
|
|
|
st.markdown("<h2 style='margin-bottom: 2px;'>Global Master Artist Catalog (17k)</h2>", unsafe_allow_html=True)
|
2026-10-31 14:43:43 +05:30
|
|
|
st.markdown("<p style='color: #9ca3af; font-size: 0.92rem;'>Empirical registry validation via CSPC sales, RIAA certifications, and verified Last.fm global scrobbles.</p>", unsafe_allow_html=True)
|
|
|
|
|
with head2:
|
|
|
|
|
st.markdown(f"<div style='text-align: right; color: #6b7280; font-size: 0.8rem; padding-top: 10px;'>Filtered: <b>{len(filt_17k):,} / {len(df_17k):,}</b> acts</div>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
mk1, mk2, mk3, mk4, mk5 = st.columns(5)
|
|
|
|
|
with mk1:
|
|
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title">Universe Size</div><div class="kpi-value">{len(df_17k):,}</div><div class="kpi-subtitle">Total Cataloged</div></div>', unsafe_allow_html=True)
|
|
|
|
|
with mk2:
|
2026-11-06 11:55:25 +05:30
|
|
|
ver_cnt = df_17k["catalog_rank"].notna().sum() if "catalog_rank" in df_17k.columns else 0
|
2026-10-31 14:43:43 +05:30
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title" style="color: #10b981;">Verified Entities</div><div class="kpi-value" style="color: #10b981;">{ver_cnt:,}</div><div class="kpi-subtitle">Registry Authenticated</div></div>', unsafe_allow_html=True)
|
|
|
|
|
with mk3:
|
2026-11-06 11:55:25 +05:30
|
|
|
t1_cnt = (df_17k.get("catalog_intelligence_tier") == "TIER_1_GLOBAL_SUPERSTAR_CSPC").sum()
|
2026-10-31 14:43:43 +05:30
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title" style="color: #f59e0b;">Tier 1 Superstars</div><div class="kpi-value" style="color: #f59e0b;">{t1_cnt:,}</div><div class="kpi-subtitle">Audited CSPC EAS</div></div>', unsafe_allow_html=True)
|
|
|
|
|
with mk4:
|
2026-11-06 11:55:25 +05:30
|
|
|
lfm_cnt = (df_17k.get("catalog_intelligence_tier") == "TIER_3_LASTFM_VERIFIED_ENTITY").sum()
|
2026-10-31 14:43:43 +05:30
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title" style="color: #38bdf8;">Last.fm Verified</div><div class="kpi-value" style="color: #38bdf8;">{lfm_cnt:,}</div><div class="kpi-subtitle">Active Platform Entities</div></div>', unsafe_allow_html=True)
|
|
|
|
|
with mk5:
|
|
|
|
|
unver_cnt = len(df_17k) - ver_cnt
|
|
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title" style="color: #ef4444;">Unaudited</div><div class="kpi-value" style="color: #ef4444;">{unver_cnt:,}</div><div class="kpi-subtitle">Zero-Registry Matches</div></div>', unsafe_allow_html=True)
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
2026-10-31 14:43:43 +05:30
|
|
|
render_cols = [
|
|
|
|
|
"catalog_rank", "tier_rank", "name", "catalog_intelligence_tier",
|
|
|
|
|
"audited_sales_units", "lastfm_listeners", "lastfm_playcount", "lastfm_url"
|
2026-10-22 17:20:44 +05:30
|
|
|
]
|
2026-10-31 14:43:43 +05:30
|
|
|
disp_cols = [c for c in render_cols if c in filt_17k.columns]
|
2026-10-22 17:20:44 +05:30
|
|
|
|
2026-10-30 11:10:54 +05:30
|
|
|
st.dataframe(
|
2026-10-31 14:43:43 +05:30
|
|
|
filt_17k[disp_cols].reset_index(drop=True),
|
2026-11-06 11:55:25 +05:30
|
|
|
width="stretch",
|
2026-10-31 14:43:43 +05:30
|
|
|
height=560,
|
2026-10-30 11:10:54 +05:30
|
|
|
column_config={
|
2026-10-31 14:43:43 +05:30
|
|
|
"catalog_rank": st.column_config.NumberColumn("Global Rank", format="#%d", width="small"),
|
|
|
|
|
"tier_rank": st.column_config.NumberColumn("Tier Rank", format="#%d", width="small"),
|
|
|
|
|
"name": st.column_config.TextColumn("Artist Name", width="medium"),
|
|
|
|
|
"catalog_intelligence_tier": st.column_config.TextColumn("Registry Tier", width="medium"),
|
|
|
|
|
"audited_sales_units": st.column_config.NumberColumn("Audited Sales Units", format="%,d", width="medium"),
|
|
|
|
|
"lastfm_listeners": st.column_config.NumberColumn("Verified Listeners", format="%,d", width="medium"),
|
|
|
|
|
"lastfm_playcount": st.column_config.NumberColumn("Global Scrobbles", format="%,d", width="medium"),
|
2026-11-04 16:49:39 +05:30
|
|
|
"lastfm_url": st.column_config.LinkColumn("Last.fm Profile", width="small"),
|
|
|
|
|
},
|
2026-10-30 11:10:54 +05:30
|
|
|
)
|
2026-10-23 10:52:54 +05:30
|
|
|
|
2026-10-31 14:43:43 +05:30
|
|
|
with st.sidebar:
|
2026-11-06 11:55:25 +05:30
|
|
|
st.markdown("##### 17k Data Export")
|
2026-10-31 14:43:43 +05:30
|
|
|
csv_17k = filt_17k.to_csv(index=False).encode("utf-8")
|
2026-11-06 11:55:25 +05:30
|
|
|
st.download_button("Export 17k Table CSV", data=csv_17k, file_name="master_17k_catalog.csv", mime="text/csv", width="stretch")
|
2026-10-28 17:07:19 +05:30
|
|
|
|
|
|
|
|
else:
|
2026-10-31 14:43:43 +05:30
|
|
|
df = df_500
|
2026-11-04 16:49:39 +05:30
|
|
|
if df.empty:
|
2026-11-06 11:55:25 +05:30
|
|
|
st.error("Hip-Hop 500 dataset not found in data/hiffi_catalog.db")
|
2026-11-04 16:49:39 +05:30
|
|
|
st.stop()
|
|
|
|
|
|
2026-10-31 14:43:43 +05:30
|
|
|
with st.sidebar:
|
|
|
|
|
view_mode = st.radio(
|
|
|
|
|
"Leaderboard Perspective:",
|
|
|
|
|
[
|
2026-11-06 11:55:25 +05:30
|
|
|
"Published Multi-Platform Rank",
|
|
|
|
|
"Breakout Velocity (BVI)",
|
|
|
|
|
"Full Cohort Audit Grid",
|
2026-10-31 14:43:43 +05:30
|
|
|
],
|
|
|
|
|
index=0,
|
|
|
|
|
)
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
st.markdown("##### Cohort Filters")
|
2026-10-31 14:43:43 +05:30
|
|
|
all_verticals = sorted(df["subgenre"].dropna().unique().tolist())
|
2026-11-06 11:55:25 +05:30
|
|
|
selected_verticals = st.multiselect("Vertical", options=all_verticals, default=all_verticals, key="v_sel_pure")
|
2026-10-31 14:43:43 +05:30
|
|
|
|
|
|
|
|
tier_options = sorted(df["platform_evidence_tier"].dropna().unique().astype(int).tolist(), reverse=True)
|
2026-11-06 11:55:25 +05:30
|
|
|
selected_tiers = st.multiselect("Evidence Tier", options=tier_options, default=tier_options, key="t_sel_pure")
|
2026-10-31 14:43:43 +05:30
|
|
|
min_xp = st.slider("Min HIFFI-XP Score", 0.0, 100.0, 0.0, 5.0)
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
st.markdown("##### Export")
|
|
|
|
|
csv_payload = df.to_csv(index=False).encode("utf-8")
|
|
|
|
|
st.download_button(
|
|
|
|
|
"Download Top 500 CSV",
|
|
|
|
|
data=csv_payload,
|
|
|
|
|
file_name="hiffi_top500_telemetry.csv",
|
|
|
|
|
mime="text/csv",
|
|
|
|
|
width="stretch",
|
|
|
|
|
)
|
|
|
|
|
st.caption("Telemetry Engine: 45% Spotify - 35% YouTube - 20% Instagram")
|
|
|
|
|
|
2026-10-31 14:43:43 +05:30
|
|
|
filtered_df = df[
|
|
|
|
|
(df["subgenre"].isin(selected_verticals))
|
|
|
|
|
& (df["platform_evidence_tier"].isin(selected_tiers))
|
|
|
|
|
& (df["hiffi_xp_score"].fillna(0.0) >= min_xp)
|
|
|
|
|
].copy()
|
|
|
|
|
|
|
|
|
|
head_col1, head_col2 = st.columns([3, 1])
|
|
|
|
|
with head_col1:
|
|
|
|
|
st.markdown("<h2 style='margin-bottom: 2px;'>HIFFI Hip-Hop 500 Intelligence Hub</h2>", unsafe_allow_html=True)
|
2026-11-06 11:55:25 +05:30
|
|
|
st.markdown("<p style='color: #9ca3af; font-size: 0.90rem; margin-top: 0;'>Pure multi-platform composite rankings (45% Spotify, 35% YouTube, 20% Instagram) with zero-imputation policy.</p>", unsafe_allow_html=True)
|
2026-10-31 14:43:43 +05:30
|
|
|
with head_col2:
|
|
|
|
|
st.markdown(
|
2026-11-06 11:55:25 +05:30
|
|
|
f"<div style='text-align: right; color: #6b7280; font-size: 0.85rem; padding-top: 10px;'>Active Cohort: <b>{len(filtered_df)} / {len(df)}</b> artists</div>",
|
2026-10-31 14:43:43 +05:30
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
2026-10-30 11:10:54 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
k1, k2, k3, k4, k5 = st.columns(5)
|
2026-10-31 14:43:43 +05:30
|
|
|
with k1:
|
|
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title">Tracked Artists</div><div class="kpi-value">{len(df)}</div><div class="kpi-subtitle">Monitored Cohort</div></div>', unsafe_allow_html=True)
|
|
|
|
|
with k2:
|
2026-11-06 11:55:25 +05:30
|
|
|
t3_count = int((df["platform_evidence_tier"] == 3).sum())
|
|
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title" style="color: #10b981;">Tier 3 (Complete)</div><div class="kpi-value" style="color: #10b981;">{t3_count}</div><div class="kpi-subtitle">Spotify + YT + IG</div></div>', unsafe_allow_html=True)
|
2026-10-31 14:43:43 +05:30
|
|
|
with k3:
|
2026-11-06 11:55:25 +05:30
|
|
|
yt_count = int(df["subscriber_count"].fillna(0).gt(0).sum())
|
2026-11-04 16:49:39 +05:30
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title" style="color: #ef4444;">YouTube Telemetry</div><div class="kpi-value" style="color: #ef4444;">{yt_count}</div><div class="kpi-subtitle">Verified video channels</div></div>', unsafe_allow_html=True)
|
2026-10-31 14:43:43 +05:30
|
|
|
with k4:
|
2026-11-06 11:55:25 +05:30
|
|
|
sp_count = int(df["spotify_monthly_listeners"].fillna(0).gt(0).sum())
|
2026-11-04 16:49:39 +05:30
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title" style="color: #38bdf8;">Spotify Telemetry</div><div class="kpi-value" style="color: #38bdf8;">{sp_count}</div><div class="kpi-subtitle">Verified monthly listeners</div></div>', unsafe_allow_html=True)
|
2026-10-31 14:43:43 +05:30
|
|
|
with k5:
|
2026-11-06 11:55:25 +05:30
|
|
|
ig_count = int(df["instagram_followers"].fillna(0).gt(0).sum())
|
2026-11-04 16:49:39 +05:30
|
|
|
st.markdown(f'<div class="kpi-box"><div class="kpi-title" style="color: #f59e0b;">Instagram Audience</div><div class="kpi-value" style="color: #f59e0b;">{ig_count}</div><div class="kpi-subtitle">Verified public followings</div></div>', unsafe_allow_html=True)
|
2026-10-30 11:10:54 +05:30
|
|
|
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
top3 = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).head(3)
|
|
|
|
|
if len(top3) >= 3:
|
|
|
|
|
st.markdown("#### Market Leaders (Audited Multi-Platform Baseline)")
|
|
|
|
|
p1, p2, p3 = st.columns(3)
|
|
|
|
|
podium_data = [
|
|
|
|
|
(p1, top3.iloc[0], "PUBLISHED #1", "podium-gold", "#f59e0b"),
|
|
|
|
|
(p2, top3.iloc[1], "PUBLISHED #2", "podium-silver", "#94a3b8"),
|
|
|
|
|
(p3, top3.iloc[2], "PUBLISHED #3", "podium-bronze", "#f97316"),
|
2026-10-31 14:43:43 +05:30
|
|
|
]
|
2026-11-06 11:55:25 +05:30
|
|
|
for col, row_data, rank_label, css_class, accent_color in podium_data:
|
|
|
|
|
tier_val = int(row_data.get("platform_evidence_tier", 3))
|
|
|
|
|
sp_num = int(row_data.get("spotify_monthly_listeners") or 0)
|
|
|
|
|
yt_num = int(row_data.get("subscriber_count") or 0)
|
|
|
|
|
ig_num = int(row_data.get("instagram_followers") or 0)
|
|
|
|
|
xp_val = float(row_data.get("hiffi_xp_score") or 0.0)
|
|
|
|
|
artist_name = str(row_data.get("name", "Unknown"))
|
2026-10-31 14:43:43 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
podium_html = (
|
|
|
|
|
f'<div class="podium-tile {css_class}">'
|
|
|
|
|
f'<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">'
|
|
|
|
|
f'<span style="font-weight: 800; color: {accent_color}; font-size: 0.85rem;">{rank_label}</span>'
|
|
|
|
|
f'<span class="badge badge-tier{tier_val}">Tier {tier_val} Evidence</span>'
|
|
|
|
|
f'</div>'
|
|
|
|
|
f'<div style="font-size: 1.45rem; font-weight: 800; color: #ffffff; margin-bottom: 14px;">{artist_name}</div>'
|
|
|
|
|
f'<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; background: #0b0f17; padding: 10px; border-radius: 8px; text-align: center;">'
|
|
|
|
|
f'<div><div style="font-size: 0.64rem; color: #9ca3af;">HIFFI-XP</div><div style="font-size: 1.12rem; font-weight: 800; color: {accent_color};">{xp_val:.2f}</div></div>'
|
|
|
|
|
f'<div><div style="font-size: 0.64rem; color: #9ca3af;">Spotify</div><div style="font-size: 0.90rem; font-weight: 700; color: #38bdf8;">{sp_num:,}</div></div>'
|
|
|
|
|
f'<div><div style="font-size: 0.64rem; color: #9ca3af;">YT Subs</div><div style="font-size: 0.90rem; font-weight: 700; color: #ef4444;">{yt_num:,}</div></div>'
|
|
|
|
|
f'<div><div style="font-size: 0.64rem; color: #9ca3af;">IG Follow</div><div style="font-size: 0.90rem; font-weight: 700; color: #e5e7eb;">{ig_num:,}</div></div>'
|
|
|
|
|
f'</div>'
|
|
|
|
|
f'</div>'
|
|
|
|
|
)
|
|
|
|
|
with col:
|
|
|
|
|
st.markdown(podium_html, unsafe_allow_html=True)
|
2026-10-31 14:43:43 +05:30
|
|
|
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
st.markdown(f"#### {view_mode}")
|
2026-10-31 14:43:43 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
if view_mode == "Published Multi-Platform Rank":
|
|
|
|
|
table_df = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).copy()
|
|
|
|
|
cols = [
|
|
|
|
|
"hiffi_xp_published_rank", "name", "platform_evidence_tier",
|
|
|
|
|
"hiffi_xp_score", "spotify_monthly_listeners", "subscriber_count",
|
|
|
|
|
"instagram_followers", "effective_weights", "anomaly_flags"
|
|
|
|
|
]
|
|
|
|
|
elif view_mode == "Breakout Velocity (BVI)":
|
|
|
|
|
bvi_series = filtered_df.get("breakout_velocity_index", pd.Series(0.0, index=filtered_df.index)).fillna(0.0)
|
|
|
|
|
table_df = filtered_df.copy()
|
|
|
|
|
table_df["breakout_velocity_index"] = bvi_series
|
|
|
|
|
table_df = table_df.sort_values(by="breakout_velocity_index", ascending=False)
|
|
|
|
|
cols = ["name", "breakout_velocity_index", "hiffi_xp_published_rank", "daily_view_velocity", "momentum_30d", "subscriber_count", "subgenre"]
|
2026-10-30 11:10:54 +05:30
|
|
|
else:
|
2026-11-06 11:55:25 +05:30
|
|
|
table_df = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).copy()
|
|
|
|
|
cols = ["hiffi_xp_published_rank", "name", "platform_evidence_tier", "hiffi_xp_score", "spotify_monthly_listeners", "subscriber_count", "instagram_followers", "daily_view_velocity", "anomaly_flags"]
|
2026-10-31 14:43:43 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
display_table = table_df.copy()
|
|
|
|
|
render_cols = [c for c in cols if c in display_table.columns]
|
2026-10-31 14:43:43 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
st.dataframe(
|
|
|
|
|
display_table[render_cols].reset_index(drop=True),
|
|
|
|
|
width="stretch",
|
|
|
|
|
height=420,
|
|
|
|
|
column_config={
|
|
|
|
|
"hiffi_xp_published_rank": st.column_config.NumberColumn("Rank", format="#%d", width="small"),
|
|
|
|
|
"name": st.column_config.TextColumn("Artist", width="medium"),
|
|
|
|
|
"platform_evidence_tier": st.column_config.NumberColumn("Evidence Tier", format="Tier %d", width="small"),
|
|
|
|
|
"hiffi_xp_score": st.column_config.ProgressColumn("HIFFI-XP Score", min_value=0, max_value=100, format="%.2f", width="medium"),
|
|
|
|
|
"spotify_monthly_listeners": st.column_config.NumberColumn("Spotify Listeners", format="%,d", width="medium"),
|
|
|
|
|
"subscriber_count": st.column_config.NumberColumn("YouTube Subscribers", format="%,d", width="medium"),
|
|
|
|
|
"instagram_followers": st.column_config.NumberColumn("Instagram Followers", format="%,d", width="medium"),
|
|
|
|
|
"effective_weights": st.column_config.TextColumn("Weights Distribution", width="medium"),
|
|
|
|
|
"anomaly_flags": st.column_config.TextColumn("Integrity Audit", width="small"),
|
|
|
|
|
"breakout_velocity_index": st.column_config.ProgressColumn("Breakout Velocity Index", format="%.2f", min_value=0, max_value=100, width="medium"),
|
|
|
|
|
"daily_view_velocity": st.column_config.NumberColumn("Daily Video Velocity", format="%,d", width="small"),
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-10-31 14:43:43 +05:30
|
|
|
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
2026-11-06 11:55:25 +05:30
|
|
|
c_left, c_right = st.columns(2)
|
|
|
|
|
with c_left:
|
|
|
|
|
chart_df = filtered_df.dropna(subset=["spotify_monthly_listeners", "instagram_followers", "hiffi_xp_score"]).copy() if all(c in filtered_df.columns for c in ["spotify_monthly_listeners", "instagram_followers", "hiffi_xp_score"]) else pd.DataFrame()
|
|
|
|
|
if not chart_df.empty:
|
|
|
|
|
fig_scatter = px.scatter(
|
|
|
|
|
chart_df, x="spotify_monthly_listeners", y="instagram_followers",
|
|
|
|
|
size="hiffi_xp_score", color="platform_evidence_tier", hover_name="name",
|
|
|
|
|
log_x=True, log_y=True, title="Spotify Monthly Listeners vs. Instagram Followers",
|
|
|
|
|
labels={"spotify_monthly_listeners": "Spotify Listeners (Log)", "instagram_followers": "Instagram Followers (Log)", "platform_evidence_tier": "Tier"},
|
|
|
|
|
template="plotly_dark",
|
|
|
|
|
)
|
|
|
|
|
fig_scatter.update_layout(paper_bgcolor="#111827", plot_bgcolor="#111827", font=dict(color="#9ca3af"), height=320, margin=dict(l=20, r=20, t=40, b=20))
|
|
|
|
|
st.plotly_chart(fig_scatter, width="stretch")
|
|
|
|
|
with c_right:
|
|
|
|
|
if "platform_evidence_tier" in filtered_df.columns:
|
|
|
|
|
tier_counts = filtered_df["platform_evidence_tier"].value_counts().reset_index()
|
|
|
|
|
tier_counts.columns = ["Tier", "Count"]
|
|
|
|
|
fig_pie = px.pie(tier_counts, values="Count", names="Tier", title="Evidence Tier Cohort Distribution", template="plotly_dark", color="Tier", color_discrete_map={3: "#10b981", 2: "#38bdf8", 1: "#f59e0b", 0: "#ef4444"})
|
|
|
|
|
fig_pie.update_layout(paper_bgcolor="#111827", plot_bgcolor="#111827", font=dict(color="#9ca3af"), height=320, margin=dict(l=20, r=20, t=40, b=20))
|
|
|
|
|
st.plotly_chart(fig_pie, width="stretch")
|
2026-10-22 17:20:44 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
st.markdown("#### Artist Dossier")
|
|
|
|
|
if "name" in df.columns and len(df["name"].unique()) > 0:
|
|
|
|
|
selected_name = st.selectbox("Inspect Artist Details:", df["name"].unique(), label_visibility="collapsed")
|
|
|
|
|
matched_rows = df[df["name"] == selected_name]
|
|
|
|
|
if not matched_rows.empty:
|
|
|
|
|
row = matched_rows.iloc[0]
|
2026-10-22 17:20:44 +05:30
|
|
|
|
2026-11-06 11:55:25 +05:30
|
|
|
d1, d2, d3, d4, d5 = st.columns(5)
|
|
|
|
|
with d1:
|
|
|
|
|
st.metric("HIFFI-XP Score", f"{float(row.get('hiffi_xp_score') or 0.0):.2f}")
|
|
|
|
|
with d2:
|
|
|
|
|
pub_rank = row.get("hiffi_xp_published_rank")
|
|
|
|
|
st.metric("Published Rank", f"#{int(pub_rank)}" if pd.notna(pub_rank) else "Unranked")
|
|
|
|
|
with d3:
|
|
|
|
|
sp_l = row.get("spotify_monthly_listeners")
|
|
|
|
|
st.metric("Spotify Listeners", f"{int(sp_l):,}" if pd.notna(sp_l) else "0")
|
|
|
|
|
with d4:
|
|
|
|
|
yt_s = row.get("subscriber_count")
|
|
|
|
|
st.metric("YouTube Subscribers", f"{int(yt_s):,}" if pd.notna(yt_s) else "0")
|
|
|
|
|
with d5:
|
|
|
|
|
ig_f = row.get("instagram_followers")
|
|
|
|
|
st.metric("Instagram Followers", f"{int(ig_f):,}" if pd.notna(ig_f) else "0")
|
|
|
|
|
|
|
|
|
|
artist_disp_name = str(row["name"])
|
|
|
|
|
tier_disp_num = int(row.get("platform_evidence_tier", 1))
|
|
|
|
|
bvi_val = float(row.get("breakout_velocity_index") or 0.0)
|
|
|
|
|
weights_disp = str(row.get("effective_weights", "sp:45.0%,yt:35.0%,ig:20.0%"))
|
|
|
|
|
|
|
|
|
|
dossier_html = (
|
|
|
|
|
f'<div style="background: #111827; padding: 14px 18px; border-radius: 8px; border: 1px solid #1f2937; margin-top: 10px; font-size: 0.85rem;">'
|
|
|
|
|
f'<div style="display: flex; gap: 24px; flex-wrap: wrap;">'
|
|
|
|
|
f'<div><b>Artist:</b> {artist_disp_name}</div>'
|
|
|
|
|
f'<div><b>Evidence Tier:</b> Tier {tier_disp_num}</div>'
|
|
|
|
|
f'<div><b>Breakout Velocity Index:</b> {bvi_val:.2f}</div>'
|
|
|
|
|
f'<div><b>Telemetry Weighting:</b> {weights_disp}</div>'
|
|
|
|
|
f'<div><b>Integrity Audit:</b> {row.get("anomaly_flags", "CLEAN")}</div>'
|
|
|
|
|
f'</div>'
|
|
|
|
|
f'</div>'
|
|
|
|
|
)
|
|
|
|
|
st.markdown(dossier_html, unsafe_allow_html=True)
|