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
|
|
|
|
|
Clean dark-themed design with multi-platform evidence tiers, Spotify & Instagram telemetry,
|
2026-10-28 17:07:19 +05:30
|
|
|
ChartMasters record sales (CSPC/EAS), breakout velocity index, and deep-dive scouting dossier.
|
2026-10-22 17:20:44 +05:30
|
|
|
"""
|
|
|
|
|
|
2026-10-23 10:52:54 +05:30
|
|
|
import io
|
2026-10-27 10:23:02 +05:30
|
|
|
from pathlib import Path
|
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
|
|
|
|
|
|
|
|
|
|
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-10-27 10:23:02 +05:30
|
|
|
DATA_PATH = Path("data/hiffi_multiplatform_500.csv")
|
|
|
|
|
FALLBACK_DATA_PATH = Path("enriched_artists.csv")
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
# Global CSS Polish
|
|
|
|
|
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 Cards */
|
|
|
|
|
.kpi-box {
|
|
|
|
|
background: #111827;
|
|
|
|
|
border: 1px solid #1f2937;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
.kpi-title {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #9ca3af;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
}
|
|
|
|
|
.kpi-value {
|
|
|
|
|
font-size: 1.9rem;
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
color: #f9fafb;
|
|
|
|
|
margin: 4px 0;
|
|
|
|
|
}
|
|
|
|
|
.kpi-subtitle {
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
color: #6b7280;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Podium Cards */
|
|
|
|
|
.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; }
|
|
|
|
|
|
2026-10-27 10:23:02 +05:30
|
|
|
/* Health & Tier Badges */
|
2026-10-22 17:20:44 +05:30
|
|
|
.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; }
|
|
|
|
|
.badge-dormant { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
.custom-divider {
|
|
|
|
|
height: 1px;
|
|
|
|
|
background: #1f2937;
|
|
|
|
|
margin: 24px 0;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
""",
|
|
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@st.cache_data(ttl=60)
|
|
|
|
|
def load_data():
|
2026-10-27 10:23:02 +05:30
|
|
|
target = DATA_PATH if DATA_PATH.exists() else FALLBACK_DATA_PATH
|
|
|
|
|
if not target.exists():
|
2026-10-22 17:20:44 +05:30
|
|
|
return pd.DataFrame()
|
2026-10-27 10:23:02 +05:30
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
numeric_cols = [
|
2026-10-27 10:23:02 +05:30
|
|
|
"hiffi_xp_published_rank",
|
|
|
|
|
"hiffi_xp_rank",
|
|
|
|
|
"hiffi_xp_score",
|
|
|
|
|
"production_hiffi_rank",
|
|
|
|
|
"production_hiffi_score",
|
|
|
|
|
"platform_evidence_tier",
|
2026-10-22 17:20:44 +05:30
|
|
|
"breakout_velocity_index",
|
|
|
|
|
"subscriber_count",
|
|
|
|
|
"daily_view_velocity",
|
|
|
|
|
"momentum_30d",
|
2026-10-27 10:23:02 +05:30
|
|
|
"spotify_monthly_listeners",
|
|
|
|
|
"spotify_daily_change",
|
|
|
|
|
"instagram_followers",
|
2026-10-22 17:20:44 +05:30
|
|
|
"confidence_score",
|
2026-10-28 17:07:19 +05:30
|
|
|
"breakout_rank",
|
|
|
|
|
"chartmasters_sales_rank",
|
|
|
|
|
"cohort_sales_rank",
|
|
|
|
|
"pure_album_sales",
|
|
|
|
|
"physical_singles_sales",
|
|
|
|
|
"digital_singles_sales",
|
|
|
|
|
"streaming_eas",
|
|
|
|
|
"total_eas_cspc",
|
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
|
|
|
|
|
|
|
|
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-23 10:52:54 +05:30
|
|
|
def to_excel_bytes(dataframe: pd.DataFrame) -> bytes:
|
|
|
|
|
output = io.BytesIO()
|
|
|
|
|
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
|
|
|
|
dataframe.to_excel(writer, index=False, sheet_name="HIFFI_Cohort")
|
|
|
|
|
return output.getvalue()
|
|
|
|
|
|
|
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
df = load_data()
|
|
|
|
|
|
|
|
|
|
# ----------------- Sidebar Controls -----------------
|
|
|
|
|
with st.sidebar:
|
2026-10-27 10:23:02 +05:30
|
|
|
st.markdown("### ⚡ HIFFI Multi-Platform")
|
2026-10-28 17:07:19 +05:30
|
|
|
st.caption("YouTube • Spotify • Instagram • Sales Analytics")
|
2026-10-22 17:20:44 +05:30
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
if df.empty:
|
2026-10-27 10:23:02 +05:30
|
|
|
st.error("No dataset found. Run 'python run_pipeline.py' to generate master data.")
|
2026-10-22 17:20:44 +05:30
|
|
|
st.stop()
|
|
|
|
|
|
|
|
|
|
view_mode = st.radio(
|
|
|
|
|
"Leaderboard Perspective:",
|
2026-10-28 17:07:19 +05:30
|
|
|
[
|
|
|
|
|
"🏆 Published Multi-Platform Rank",
|
|
|
|
|
"🚀 Breakout Velocity (BVI)",
|
|
|
|
|
"💿 Record Sales & CSPC (ChartMasters)",
|
|
|
|
|
"🔍 Full Cohort Audit Grid",
|
|
|
|
|
],
|
2026-10-22 17:20:44 +05:30
|
|
|
index=0,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
2026-10-27 10:23:02 +05:30
|
|
|
st.markdown("##### 🛡️ Cohort Filters")
|
2026-10-22 17:20:44 +05:30
|
|
|
|
2026-10-23 10:52:54 +05:30
|
|
|
all_verticals = sorted(df["subgenre"].dropna().unique().tolist())
|
2026-10-27 10:23:02 +05:30
|
|
|
selected_verticals = st.multiselect("Vertical", options=all_verticals, default=all_verticals)
|
2026-10-23 10:52:54 +05:30
|
|
|
|
2026-10-27 10:23:02 +05:30
|
|
|
tier_options = sorted(df["platform_evidence_tier"].dropna().unique().astype(int).tolist(), reverse=True)
|
|
|
|
|
selected_tiers = st.multiselect("Evidence Tier", options=tier_options, default=tier_options)
|
2026-10-22 17:20:44 +05:30
|
|
|
|
2026-10-27 10:23:02 +05:30
|
|
|
min_xp = st.slider("Min HIFFI-XP Score", 0.0, 100.0, 0.0, 5.0)
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
filtered_df = df[
|
2026-10-27 10:23:02 +05:30
|
|
|
(df["subgenre"].isin(selected_verticals))
|
|
|
|
|
& (df["platform_evidence_tier"].isin(selected_tiers))
|
|
|
|
|
& (df["hiffi_xp_score"].fillna(0.0) >= min_xp)
|
2026-10-22 17:20:44 +05:30
|
|
|
].copy()
|
|
|
|
|
|
2026-10-23 10:52:54 +05:30
|
|
|
with st.sidebar:
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
st.markdown("##### 💾 Export Filtered Cohort")
|
|
|
|
|
col_csv, col_xlsx = st.columns(2)
|
2026-10-27 10:23:02 +05:30
|
|
|
|
2026-10-23 10:52:54 +05:30
|
|
|
csv_payload = filtered_df.to_csv(index=False).encode("utf-8")
|
|
|
|
|
col_csv.download_button(
|
2026-10-27 10:23:02 +05:30
|
|
|
"📥 CSV",
|
2026-10-23 10:52:54 +05:30
|
|
|
data=csv_payload,
|
2026-10-27 10:23:02 +05:30
|
|
|
file_name="hiffi_multiplatform_filtered.csv",
|
2026-10-23 10:52:54 +05:30
|
|
|
mime="text/csv",
|
2026-10-27 10:23:02 +05:30
|
|
|
width="stretch",
|
2026-10-23 10:52:54 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
excel_payload = to_excel_bytes(filtered_df)
|
|
|
|
|
col_xlsx.download_button(
|
2026-10-27 10:23:02 +05:30
|
|
|
"📊 Excel",
|
2026-10-23 10:52:54 +05:30
|
|
|
data=excel_payload,
|
2026-10-27 10:23:02 +05:30
|
|
|
file_name="hiffi_multiplatform_filtered.xlsx",
|
2026-10-23 10:52:54 +05:30
|
|
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
2026-10-27 10:23:02 +05:30
|
|
|
width="stretch",
|
2026-10-23 10:52:54 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
2026-10-28 17:07:19 +05:30
|
|
|
st.caption("Governance: Multi-Platform Signals + Verified ChartMasters Provenance")
|
2026-10-23 10:52:54 +05:30
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
# ----------------- Main Header -----------------
|
|
|
|
|
head_col1, head_col2 = st.columns([3, 1])
|
|
|
|
|
with head_col1:
|
2026-10-27 10:23:02 +05:30
|
|
|
st.markdown("<h2 style='margin-bottom: 2px;'>HIFFI Multi-Platform Intelligence Hub</h2>", unsafe_allow_html=True)
|
2026-10-28 17:07:19 +05:30
|
|
|
st.markdown("<p style='color: #9ca3af; font-size: 0.92rem; margin-top: 0;'>Multi-platform composite rankings with zero-imputation policy, dynamic weight redistribution, and verified record sales.</p>", unsafe_allow_html=True)
|
2026-10-22 17:20:44 +05:30
|
|
|
with head_col2:
|
|
|
|
|
st.markdown(
|
2026-10-27 10:23:02 +05:30
|
|
|
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>",
|
2026-10-22 17:20:44 +05:30
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-28 17:07:19 +05:30
|
|
|
# ----------------- KPI Cards Row (5 Columns) -----------------
|
|
|
|
|
k1, k2, k3, k4, k5 = st.columns(5)
|
2026-10-22 17:20:44 +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>
|
2026-10-27 10:23:02 +05:30
|
|
|
<div class="kpi-subtitle">Monitored Cohort</div>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
|
|
|
|
""",
|
|
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with k2:
|
2026-10-27 10:23:02 +05:30
|
|
|
t3_count = len(df[df["platform_evidence_tier"] == 3])
|
2026-10-22 17:20:44 +05:30
|
|
|
st.markdown(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="kpi-box">
|
2026-10-27 10:23:02 +05:30
|
|
|
<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">YouTube + Spotify + Instagram</div>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
|
|
|
|
""",
|
|
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with k3:
|
2026-10-27 10:23:02 +05:30
|
|
|
sp_count = df["spotify_monthly_listeners"].notna().sum()
|
2026-10-22 17:20:44 +05:30
|
|
|
st.markdown(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="kpi-box">
|
2026-10-27 10:23:02 +05:30
|
|
|
<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>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
|
|
|
|
""",
|
|
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with k4:
|
2026-10-27 10:23:02 +05:30
|
|
|
ig_count = df["instagram_followers"].notna().sum()
|
2026-10-22 17:20:44 +05:30
|
|
|
st.markdown(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="kpi-box">
|
2026-10-27 10:23:02 +05:30
|
|
|
<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>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
|
|
|
|
""",
|
|
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-28 17:07:19 +05:30
|
|
|
with k5:
|
|
|
|
|
sales_indexed = df["total_eas_cspc"].notna().sum() if "total_eas_cspc" in df.columns else 0
|
|
|
|
|
st.markdown(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="kpi-box">
|
|
|
|
|
<div class="kpi-title" style="color: #ec4899;">Record Sales</div>
|
|
|
|
|
<div class="kpi-value" style="color: #ec4899;">{sales_indexed}</div>
|
|
|
|
|
<div class="kpi-subtitle">ChartMasters CSPC records</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-27 10:23:02 +05:30
|
|
|
# ----------------- Podium -----------------
|
|
|
|
|
top3 = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).head(3)
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
if len(top3) >= 3:
|
2026-10-27 10:23:02 +05:30
|
|
|
st.markdown("#### 👑 Market Leaders (Published Rankings)")
|
2026-10-22 17:20:44 +05:30
|
|
|
p1, p2, p3 = st.columns(3)
|
|
|
|
|
|
|
|
|
|
podium_data = [
|
2026-10-27 10:23:02 +05:30
|
|
|
(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-22 17:20:44 +05:30
|
|
|
]
|
|
|
|
|
|
2026-10-23 10:52:54 +05:30
|
|
|
for col, row_data, rank_label, css_class, accent_color in podium_data:
|
2026-10-27 10:23:02 +05:30
|
|
|
tier_val = int(row_data.get("platform_evidence_tier", 1))
|
|
|
|
|
badge_cls = f"badge-tier{tier_val}"
|
|
|
|
|
sp_str = f"{int(row_data['spotify_monthly_listeners']):,}" if pd.notna(row_data.get("spotify_monthly_listeners")) else "NaN"
|
|
|
|
|
ig_str = f"{int(row_data['instagram_followers']):,}" if pd.notna(row_data.get("instagram_followers")) else "NaN"
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
with col:
|
|
|
|
|
st.markdown(
|
|
|
|
|
f"""
|
|
|
|
|
<div class="podium-tile {css_class}">
|
|
|
|
|
<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>
|
2026-10-27 10:23:02 +05:30
|
|
|
<span class="badge {badge_cls}">Tier {tier_val} Evidence</span>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
2026-10-23 10:52:54 +05:30
|
|
|
<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>
|
2026-10-22 17:20:44 +05:30
|
|
|
<div style="display: flex; justify-content: space-between; background: #0b0f17; padding: 10px 14px; border-radius: 8px;">
|
|
|
|
|
<div>
|
2026-10-27 10:23:02 +05:30
|
|
|
<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_xp_score']:.2f}</div>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
|
|
|
|
<div>
|
2026-10-27 10:23:02 +05:30
|
|
|
<div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">Spotify Monthly</div>
|
|
|
|
|
<div style="font-size: 1.05rem; font-weight: 700; color: #38bdf8;">{sp_str}</div>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
|
|
|
|
<div>
|
2026-10-27 10:23:02 +05:30
|
|
|
<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;">{ig_str}</div>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
""",
|
|
|
|
|
unsafe_allow_html=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
# ----------------- Leaderboard Grid -----------------
|
|
|
|
|
st.markdown(f"#### 📋 {view_mode}")
|
|
|
|
|
|
2026-10-27 10:23:02 +05:30
|
|
|
if view_mode == "🏆 Published Multi-Platform Rank":
|
|
|
|
|
table_df = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).copy()
|
2026-10-22 17:20:44 +05:30
|
|
|
cols = [
|
2026-10-27 10:23:02 +05:30
|
|
|
"hiffi_xp_published_rank",
|
|
|
|
|
"hiffi_xp_rank",
|
2026-10-22 17:20:44 +05:30
|
|
|
"name",
|
2026-10-27 10:23:02 +05:30
|
|
|
"platform_evidence_tier",
|
|
|
|
|
"hiffi_xp_score",
|
|
|
|
|
"spotify_monthly_listeners",
|
|
|
|
|
"instagram_followers",
|
|
|
|
|
"effective_weights",
|
|
|
|
|
"anomaly_flags"
|
2026-10-22 17:20:44 +05:30
|
|
|
]
|
|
|
|
|
elif view_mode == "🚀 Breakout Velocity (BVI)":
|
|
|
|
|
table_df = (
|
2026-10-27 10:23:02 +05:30
|
|
|
filtered_df[filtered_df["breakout_velocity_index"].notna()]
|
|
|
|
|
.sort_values(by="breakout_velocity_index", ascending=False)
|
2026-10-22 17:20:44 +05:30
|
|
|
.copy()
|
|
|
|
|
)
|
|
|
|
|
cols = [
|
|
|
|
|
"name",
|
|
|
|
|
"breakout_velocity_index",
|
2026-10-27 10:23:02 +05:30
|
|
|
"hiffi_xp_published_rank",
|
2026-10-22 17:20:44 +05:30
|
|
|
"daily_view_velocity",
|
2026-10-27 10:23:02 +05:30
|
|
|
"momentum_30d",
|
2026-10-22 17:20:44 +05:30
|
|
|
"subscriber_count",
|
2026-10-27 10:23:02 +05:30
|
|
|
"subgenre"
|
2026-10-22 17:20:44 +05:30
|
|
|
]
|
2026-10-28 17:07:19 +05:30
|
|
|
elif view_mode == "💿 Record Sales & CSPC (ChartMasters)":
|
|
|
|
|
table_df = (
|
|
|
|
|
filtered_df[filtered_df["total_eas_cspc"].notna()]
|
|
|
|
|
.sort_values(by="total_eas_cspc", ascending=False)
|
|
|
|
|
.copy() if "total_eas_cspc" in filtered_df.columns else filtered_df.copy()
|
|
|
|
|
)
|
|
|
|
|
cols = [
|
|
|
|
|
"cohort_sales_rank",
|
|
|
|
|
"chartmasters_sales_rank",
|
|
|
|
|
"name",
|
|
|
|
|
"total_eas_cspc",
|
|
|
|
|
"pure_album_sales",
|
|
|
|
|
"streaming_eas",
|
|
|
|
|
"physical_singles_sales",
|
|
|
|
|
"digital_singles_sales",
|
|
|
|
|
"sales_data_status",
|
|
|
|
|
"sales_collected_at"
|
|
|
|
|
]
|
2026-10-22 17:20:44 +05:30
|
|
|
else:
|
2026-10-27 10:23:02 +05:30
|
|
|
table_df = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).copy()
|
2026-10-22 17:20:44 +05:30
|
|
|
cols = [
|
2026-10-27 10:23:02 +05:30
|
|
|
"hiffi_xp_published_rank",
|
2026-10-22 17:20:44 +05:30
|
|
|
"name",
|
2026-10-27 10:23:02 +05:30
|
|
|
"production_hiffi_rank",
|
|
|
|
|
"hiffi_xp_rank",
|
|
|
|
|
"rank_delta",
|
|
|
|
|
"platform_evidence_tier",
|
|
|
|
|
"hiffi_xp_score",
|
|
|
|
|
"spotify_monthly_listeners",
|
|
|
|
|
"instagram_followers",
|
2026-10-28 17:07:19 +05:30
|
|
|
"total_eas_cspc",
|
|
|
|
|
"sales_data_status",
|
2026-10-27 10:23:02 +05:30
|
|
|
"evidence_exclusions",
|
|
|
|
|
"anomaly_flags"
|
2026-10-22 17:20:44 +05:30
|
|
|
]
|
|
|
|
|
|
2026-10-23 10:52:54 +05:30
|
|
|
render_cols = [c for c in cols if c in table_df.columns]
|
|
|
|
|
|
2026-10-22 17:20:44 +05:30
|
|
|
st.dataframe(
|
2026-10-23 10:52:54 +05:30
|
|
|
table_df[render_cols].reset_index(drop=True),
|
2026-10-27 10:23:02 +05:30
|
|
|
width="stretch",
|
2026-10-28 17:07:19 +05:30
|
|
|
height=330,
|
2026-10-22 17:20:44 +05:30
|
|
|
column_config={
|
2026-10-27 10:23:02 +05:30
|
|
|
"hiffi_xp_published_rank": st.column_config.NumberColumn("Pub 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"),
|
2026-10-28 17:07:19 +05:30
|
|
|
"cohort_sales_rank": st.column_config.NumberColumn("Cohort #", format="#%d", width="small"),
|
|
|
|
|
"chartmasters_sales_rank": st.column_config.NumberColumn("Global #", format="#%d", width="small"),
|
2026-10-27 10:23:02 +05:30
|
|
|
"platform_evidence_tier": st.column_config.NumberColumn("Tier", format="%d", width="small"),
|
2026-10-22 17:20:44 +05:30
|
|
|
"name": st.column_config.TextColumn("Artist", width="medium"),
|
2026-10-27 10:23:02 +05:30
|
|
|
"hiffi_xp_score": st.column_config.ProgressColumn("HIFFI-XP Score", format="%.2f", min_value=0, max_value=100, width="medium"),
|
|
|
|
|
"breakout_velocity_index": st.column_config.ProgressColumn("BVI", format="%.1f", min_value=0, max_value=100, width="small"),
|
2026-10-28 17:07:19 +05:30
|
|
|
"total_eas_cspc": st.column_config.NumberColumn("Total EAS (CSPC)", format="%,d", width="medium"),
|
|
|
|
|
"pure_album_sales": st.column_config.NumberColumn("Pure Albums", format="%,d", width="medium"),
|
|
|
|
|
"streaming_eas": st.column_config.NumberColumn("Streaming EAS", format="%,d", width="medium"),
|
|
|
|
|
"physical_singles_sales": st.column_config.NumberColumn("Physical", format="%,d", width="small"),
|
|
|
|
|
"digital_singles_sales": st.column_config.NumberColumn("Digital", format="%,d", width="small"),
|
|
|
|
|
"sales_data_status": st.column_config.TextColumn("Status", width="medium"),
|
|
|
|
|
"sales_collected_at": st.column_config.TextColumn("Collected Date", width="medium"),
|
2026-10-27 10:23:02 +05:30
|
|
|
"spotify_monthly_listeners": st.column_config.NumberColumn("Spotify Monthly", format="%,d", width="medium"),
|
|
|
|
|
"instagram_followers": st.column_config.NumberColumn("IG Followers", format="%,d", width="medium"),
|
|
|
|
|
"effective_weights": st.column_config.TextColumn("Dynamic Redistribution", width="large"),
|
|
|
|
|
"anomaly_flags": st.column_config.TextColumn("Anomaly Status", width="medium"),
|
|
|
|
|
"evidence_exclusions": st.column_config.TextColumn("Exclusions", width="large"),
|
2026-10-22 17:20:44 +05:30
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
|
|
|
|
|
2026-10-28 17:07:19 +05:30
|
|
|
# ----------------- Visual Insights & Format Analysis -----------------
|
|
|
|
|
st.markdown("#### 📊 Commercial & Multi-Platform Analytics")
|
2026-10-22 17:20:44 +05:30
|
|
|
|
2026-10-28 17:07:19 +05:30
|
|
|
if view_mode == "💿 Record Sales & CSPC (ChartMasters)":
|
|
|
|
|
c_sales1, c_sales2 = st.columns(2)
|
2026-10-22 17:20:44 +05:30
|
|
|
|
2026-10-28 17:07:19 +05:30
|
|
|
with c_sales1:
|
|
|
|
|
top_sales = (
|
|
|
|
|
filtered_df[filtered_df["total_eas_cspc"].notna()]
|
|
|
|
|
.sort_values(by="total_eas_cspc", ascending=False)
|
|
|
|
|
.head(15)
|
2026-10-27 10:23:02 +05:30
|
|
|
)
|
2026-10-28 17:07:19 +05:30
|
|
|
if not top_sales.empty:
|
|
|
|
|
fig_format = go.Figure()
|
|
|
|
|
fig_format.add_trace(go.Bar(
|
|
|
|
|
name="Pure Albums",
|
|
|
|
|
x=top_sales["name"],
|
|
|
|
|
y=top_sales["pure_album_sales"],
|
|
|
|
|
marker_color="#8b5cf6"
|
|
|
|
|
))
|
|
|
|
|
fig_format.add_trace(go.Bar(
|
|
|
|
|
name="Digital Singles",
|
|
|
|
|
x=top_sales["name"],
|
|
|
|
|
y=top_sales["digital_singles_sales"],
|
|
|
|
|
marker_color="#38bdf8"
|
|
|
|
|
))
|
|
|
|
|
fig_format.add_trace(go.Bar(
|
|
|
|
|
name="Streaming EAS",
|
|
|
|
|
x=top_sales["name"],
|
|
|
|
|
y=top_sales["streaming_eas"],
|
|
|
|
|
marker_color="#10b981"
|
|
|
|
|
))
|
|
|
|
|
fig_format.update_layout(
|
|
|
|
|
barmode="stack",
|
|
|
|
|
title=dict(
|
|
|
|
|
text="Format Breakdown: Pure Albums vs Singles vs Streaming (Top 15)",
|
|
|
|
|
font=dict(size=14),
|
|
|
|
|
),
|
|
|
|
|
template="plotly_dark",
|
|
|
|
|
paper_bgcolor="#111827",
|
|
|
|
|
plot_bgcolor="#111827",
|
|
|
|
|
font=dict(color="#9ca3af"),
|
|
|
|
|
margin=dict(l=20, r=20, t=60, b=20),
|
|
|
|
|
height=350,
|
|
|
|
|
legend=dict(
|
|
|
|
|
orientation="h",
|
|
|
|
|
yanchor="bottom",
|
|
|
|
|
y=1.04,
|
|
|
|
|
xanchor="right",
|
|
|
|
|
x=1.0,
|
|
|
|
|
font=dict(size=11),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
st.plotly_chart(fig_format, width="stretch")
|
|
|
|
|
else:
|
|
|
|
|
st.info("No sales data available for current filter selection.")
|
|
|
|
|
|
|
|
|
|
with c_sales2:
|
|
|
|
|
sales_scatter = filtered_df.dropna(subset=["pure_album_sales", "streaming_eas"]).copy()
|
|
|
|
|
if not sales_scatter.empty:
|
|
|
|
|
fig_scatter_sales = px.scatter(
|
|
|
|
|
sales_scatter,
|
|
|
|
|
x="pure_album_sales",
|
|
|
|
|
y="streaming_eas",
|
|
|
|
|
size="total_eas_cspc",
|
|
|
|
|
hover_name="name",
|
|
|
|
|
color="total_eas_cspc",
|
|
|
|
|
color_continuous_scale="Viridis",
|
|
|
|
|
log_x=True,
|
|
|
|
|
log_y=True,
|
|
|
|
|
title="Commercial Shift: Pure Album Sales vs Streaming Conversions",
|
|
|
|
|
labels={
|
|
|
|
|
"pure_album_sales": "Pure Album Sales (Log Scale)",
|
|
|
|
|
"streaming_eas": "Streaming Equivalent Sales (Log Scale)",
|
|
|
|
|
"total_eas_cspc": "Total EAS"
|
|
|
|
|
},
|
|
|
|
|
template="plotly_dark",
|
|
|
|
|
)
|
|
|
|
|
fig_scatter_sales.update_layout(
|
|
|
|
|
paper_bgcolor="#111827",
|
|
|
|
|
plot_bgcolor="#111827",
|
|
|
|
|
font=dict(color="#9ca3af"),
|
|
|
|
|
margin=dict(l=20, r=20, t=40, b=20),
|
|
|
|
|
height=350,
|
|
|
|
|
)
|
|
|
|
|
st.plotly_chart(fig_scatter_sales, width="stretch")
|
|
|
|
|
else:
|
|
|
|
|
st.info("Insufficient records with both pure sales and streaming figures.")
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
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 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 Monthly Listeners (Log Scale)",
|
|
|
|
|
"instagram_followers": "Instagram Followers (Log Scale)",
|
|
|
|
|
"platform_evidence_tier": "Tier",
|
|
|
|
|
},
|
|
|
|
|
template="plotly_dark",
|
|
|
|
|
)
|
|
|
|
|
fig_scatter.update_layout(
|
|
|
|
|
paper_bgcolor="#111827",
|
|
|
|
|
plot_bgcolor="#111827",
|
|
|
|
|
font=dict(color="#9ca3af"),
|
|
|
|
|
margin=dict(l=20, r=20, t=40, b=20),
|
|
|
|
|
height=320,
|
|
|
|
|
)
|
|
|
|
|
st.plotly_chart(fig_scatter, width="stretch")
|
|
|
|
|
else:
|
|
|
|
|
st.info("Insufficient complete data points to render multi-platform scatter plot.")
|
|
|
|
|
|
|
|
|
|
with c_right:
|
|
|
|
|
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(
|
2026-10-27 10:23:02 +05:30
|
|
|
paper_bgcolor="#111827",
|
|
|
|
|
plot_bgcolor="#111827",
|
|
|
|
|
font=dict(color="#9ca3af"),
|
|
|
|
|
margin=dict(l=20, r=20, t=40, b=20),
|
|
|
|
|
height=320,
|
|
|
|
|
)
|
2026-10-28 17:07:19 +05:30
|
|
|
st.plotly_chart(fig_pie, width="stretch")
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
# ----------------- Deep Dive Scouting Dossier -----------------
|
2026-10-27 10:23:02 +05:30
|
|
|
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
2026-10-22 17:20:44 +05:30
|
|
|
st.markdown("#### 🔎 Artist Dossier")
|
|
|
|
|
|
|
|
|
|
selected_name = st.selectbox("Inspect Artist Details:", df["name"].unique(), label_visibility="collapsed")
|
|
|
|
|
row = df[df["name"] == selected_name].iloc[0]
|
|
|
|
|
|
2026-10-28 17:07:19 +05:30
|
|
|
d1, d2, d3, d4, d5 = st.columns(5)
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
with d1:
|
2026-10-27 10:23:02 +05:30
|
|
|
h_val = row.get("hiffi_xp_score")
|
|
|
|
|
st.metric("HIFFI-XP Score", f"{h_val:.2f}" if pd.notna(h_val) else "N/A")
|
2026-10-22 17:20:44 +05:30
|
|
|
with d2:
|
2026-10-27 10:23:02 +05:30
|
|
|
pub_rank = row.get("hiffi_xp_published_rank")
|
|
|
|
|
st.metric("Published Rank", f"#{int(pub_rank)}" if pd.notna(pub_rank) else "Unranked")
|
2026-10-22 17:20:44 +05:30
|
|
|
with d3:
|
2026-10-27 10:23:02 +05:30
|
|
|
sp_l = row.get("spotify_monthly_listeners")
|
|
|
|
|
st.metric("Spotify Listeners", f"{int(sp_l):,}" if pd.notna(sp_l) else "Unindexed (NaN)")
|
2026-10-22 17:20:44 +05:30
|
|
|
with d4:
|
2026-10-27 10:23:02 +05:30
|
|
|
ig_f = row.get("instagram_followers")
|
|
|
|
|
st.metric("Instagram Followers", f"{int(ig_f):,}" if pd.notna(ig_f) else "Unindexed (NaN)")
|
2026-10-28 17:07:19 +05:30
|
|
|
with d5:
|
|
|
|
|
sales_val = row.get("total_eas_cspc")
|
|
|
|
|
st.metric("Total EAS Sales", f"{int(sales_val):,}" if pd.notna(sales_val) else "Uncataloged (NaN)")
|
2026-10-22 17:20:44 +05:30
|
|
|
|
|
|
|
|
st.markdown(
|
|
|
|
|
f"""
|
2026-10-27 10:23:02 +05:30
|
|
|
<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: 20px; flex-wrap: wrap;">
|
2026-10-23 10:52:54 +05:30
|
|
|
<div><b>Artist:</b> {row['name']}</div>
|
2026-10-27 10:23:02 +05:30
|
|
|
<div><b>Evidence Tier:</b> Tier {int(row.get('platform_evidence_tier', 1))}</div>
|
2026-10-28 17:07:19 +05:30
|
|
|
<div><b>Global Sales Rank:</b> {f"#{int(row.get('chartmasters_sales_rank'))}" if pd.notna(row.get('chartmasters_sales_rank')) else "Unranked"}</div>
|
|
|
|
|
<div><b>Cohort Sales Rank:</b> {f"#{int(row.get('cohort_sales_rank'))}" if pd.notna(row.get('cohort_sales_rank')) else "Uncataloged"}</div>
|
|
|
|
|
<div><b>Pure Albums:</b> {f"{int(row.get('pure_album_sales')):,}" if pd.notna(row.get('pure_album_sales')) else "N/A"}</div>
|
|
|
|
|
<div><b>Streaming EAS:</b> {f"{int(row.get('streaming_eas')):,}" if pd.notna(row.get('streaming_eas')) else "N/A"}</div>
|
|
|
|
|
<div><b>Sales Status:</b> {row.get('sales_data_status', 'NOT_CATALOGED_YET')}</div>
|
|
|
|
|
<div><b>Provenance:</b> <a href="{row.get('sales_source_url', 'https://chartmasters.org')}" target="_blank" style="color: #38bdf8; text-decoration: underline;">ChartMasters Verification</a></div>
|
2026-10-27 10:23:02 +05:30
|
|
|
<div><b>Effective Weights:</b> {row.get('effective_weights', 'N/A')}</div>
|
|
|
|
|
<div><b>Anomaly Flags:</b> {row.get('anomaly_flags', 'CLEAN')}</div>
|
2026-10-23 10:52:54 +05:30
|
|
|
</div>
|
2026-10-22 17:20:44 +05:30
|
|
|
</div>
|
|
|
|
|
""",
|
|
|
|
|
unsafe_allow_html=True,
|
2026-10-27 10:23:02 +05:30
|
|
|
)
|