fix: resolve governance gaps, patch credential exposure, and connect multi-platform dashboard
This commit is contained in:
1 parent
6f2e8e0c9d
commit
7fb7d775d8
5 files changed
+324
-387
No files matched your search
@@ -1,30 +1,30 @@
|
||||
"""
|
||||
app.py
|
||||
HIFFI Artist Intelligence - Executive Dashboard
|
||||
Clean dark-themed design with aligned dataframes, responsive cards, and scouting dossier.
|
||||
HIFFI Artist Intelligence - Multi-Platform Executive Dashboard
|
||||
Clean dark-themed design with multi-platform evidence tiers, Spotify & Instagram telemetry,
|
||||
breakout velocity index, and deep-dive scouting dossier.
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
import streamlit as st
|
||||
|
||||
st.set_page_config(
|
||||
page_title="HIFFI | Artist Intelligence",
|
||||
page_title="HIFFI | Multi-Platform Intelligence",
|
||||
page_icon="⚡",
|
||||
layout="wide",
|
||||
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
|
||||
st.markdown(
|
||||
"""
|
||||
<style>
|
||||
/* Global Container Spacing */
|
||||
.block-container {
|
||||
padding-top: 1.8rem;
|
||||
padding-bottom: 2.5rem;
|
||||
@@ -71,7 +71,7 @@ st.markdown(
|
||||
.podium-silver { border-top: 4px solid #94a3b8; }
|
||||
.podium-bronze { border-top: 4px solid #f97316; }
|
||||
|
||||
/* Health Badges */
|
||||
/* Health & Tier Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
@@ -80,11 +80,11 @@ st.markdown(
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.badge-active { background: rgba(16, 185, 129, 0.15); color: #10b981; }
|
||||
.badge-evergreen { background: rgba(56, 189, 248, 0.15); color: #38bdf8; }
|
||||
.badge-dormant { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
|
||||
.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; }
|
||||
|
||||
/* Clean Section Dividers */
|
||||
.custom-divider {
|
||||
height: 1px;
|
||||
background: #1f2937;
|
||||
@@ -98,24 +98,44 @@ st.markdown(
|
||||
|
||||
@st.cache_data(ttl=60)
|
||||
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()
|
||||
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 = [
|
||||
"hiffi_score",
|
||||
"hiffi_xp_published_rank",
|
||||
"hiffi_xp_rank",
|
||||
"hiffi_xp_score",
|
||||
"production_hiffi_rank",
|
||||
"production_hiffi_score",
|
||||
"platform_evidence_tier",
|
||||
"breakout_velocity_index",
|
||||
"subscriber_count",
|
||||
"daily_view_velocity",
|
||||
"momentum_30d",
|
||||
"spotify_monthly_listeners",
|
||||
"spotify_daily_change",
|
||||
"instagram_followers",
|
||||
"confidence_score",
|
||||
"hiffi_rank",
|
||||
"breakout_rank",
|
||||
"breakout_rank"
|
||||
]
|
||||
for col in numeric_cols:
|
||||
if col in df.columns:
|
||||
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:
|
||||
def infer_subgenre(r):
|
||||
subs = r.get("subscriber_count", 0) or 0
|
||||
@@ -132,7 +152,6 @@ def load_data():
|
||||
|
||||
|
||||
def to_excel_bytes(dataframe: pd.DataFrame) -> bytes:
|
||||
"""Converts dataframe to downloadable in-memory Excel spreadsheet."""
|
||||
output = io.BytesIO()
|
||||
with pd.ExcelWriter(output, engine="openpyxl") as writer:
|
||||
dataframe.to_excel(writer, index=False, sheet_name="HIFFI_Cohort")
|
||||
@@ -143,92 +162,71 @@ df = load_data()
|
||||
|
||||
# ----------------- Sidebar Controls -----------------
|
||||
with st.sidebar:
|
||||
st.markdown("### ⚡ HIFFI Intelligence")
|
||||
st.caption("Hip-Hop Market Scoring & Breakout Detection")
|
||||
st.markdown("### ⚡ HIFFI Multi-Platform")
|
||||
st.caption("YouTube • Spotify • Instagram Analytics")
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
|
||||
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()
|
||||
|
||||
view_mode = st.radio(
|
||||
"Leaderboard Perspective:",
|
||||
["🏆 Overall HIFFI Rank", "🚀 Breakout Velocity (BVI)", "🔍 Master Audit Grid"],
|
||||
["🏆 Published Multi-Platform Rank", "🚀 Breakout Velocity (BVI)", "🔍 Full Cohort Audit Grid"],
|
||||
index=0,
|
||||
)
|
||||
|
||||
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())
|
||||
target_presets = ["Legends", "Trap & Street", "UK Drill", "Lyricists", "Breakout / Emerging"]
|
||||
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,
|
||||
)
|
||||
selected_verticals = st.multiselect("Vertical", options=all_verticals, default=all_verticals)
|
||||
|
||||
available_statuses = df["health_status"].dropna().unique().tolist()
|
||||
selected_statuses = st.multiselect(
|
||||
"Channel Status",
|
||||
options=available_statuses,
|
||||
default=available_statuses,
|
||||
)
|
||||
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)
|
||||
|
||||
min_conf = st.slider(
|
||||
"Min Confidence",
|
||||
min_value=0.0,
|
||||
max_value=1.0,
|
||||
value=0.50,
|
||||
step=0.05,
|
||||
)
|
||||
min_xp = st.slider("Min HIFFI-XP Score", 0.0, 100.0, 0.0, 5.0)
|
||||
|
||||
# Filter logic
|
||||
filtered_df = df[
|
||||
(df["health_status"].isin(selected_statuses))
|
||||
& (df["subgenre"].isin(selected_verticals))
|
||||
& (df["confidence_score"] >= min_conf)
|
||||
(df["subgenre"].isin(selected_verticals))
|
||||
& (df["platform_evidence_tier"].isin(selected_tiers))
|
||||
& (df["hiffi_xp_score"].fillna(0.0) >= min_xp)
|
||||
].copy()
|
||||
|
||||
# Sidebar Data Export Buttons (Filters applied)
|
||||
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)
|
||||
|
||||
|
||||
csv_payload = filtered_df.to_csv(index=False).encode("utf-8")
|
||||
col_csv.download_button(
|
||||
label="📥 CSV",
|
||||
"📥 CSV",
|
||||
data=csv_payload,
|
||||
file_name="hiffi_filtered_cohort.csv",
|
||||
file_name="hiffi_multiplatform_filtered.csv",
|
||||
mime="text/csv",
|
||||
use_container_width=True,
|
||||
width="stretch",
|
||||
)
|
||||
|
||||
excel_payload = to_excel_bytes(filtered_df)
|
||||
col_xlsx.download_button(
|
||||
label="📊 Excel",
|
||||
"📊 Excel",
|
||||
data=excel_payload,
|
||||
file_name="hiffi_filtered_cohort.xlsx",
|
||||
file_name="hiffi_multiplatform_filtered.xlsx",
|
||||
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.caption("Momentum (35%) • Velocity (30%) • Reach (20%) • Cadence (15%)")
|
||||
st.caption("Weights: YouTube (50%) • Spotify (30%) • Instagram (20%)")
|
||||
|
||||
# ----------------- Main Header -----------------
|
||||
head_col1, head_col2 = st.columns([3, 1])
|
||||
with head_col1:
|
||||
st.markdown("<h2 style='margin-bottom: 2px;'>HIFFI Artist 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("<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;'>Multi-platform composite rankings with zero-imputation policy and dynamic weight redistribution.</p>", unsafe_allow_html=True)
|
||||
with head_col2:
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -241,46 +239,46 @@ with k1:
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-title">Tracked Artists</div>
|
||||
<div class="kpi-value">{len(df)}</div>
|
||||
<div class="kpi-subtitle">Monitored pipeline size</div>
|
||||
<div class="kpi-subtitle">Monitored Cohort</div>
|
||||
</div>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
with k2:
|
||||
active_count = len(df[df["health_status"] == "ACTIVE_HEALTHY"])
|
||||
t3_count = len(df[df["platform_evidence_tier"] == 3])
|
||||
st.markdown(
|
||||
f"""
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-title" style="color: #10b981;">Active Healthy</div>
|
||||
<div class="kpi-value" style="color: #10b981;">{active_count}</div>
|
||||
<div class="kpi-subtitle">Recent uploads & steady traction</div>
|
||||
<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>
|
||||
</div>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
with k3:
|
||||
evergreen_count = len(df[df["health_status"] == "CATALOG_EVERGREEN"])
|
||||
sp_count = df["spotify_monthly_listeners"].notna().sum()
|
||||
st.markdown(
|
||||
f"""
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-title" style="color: #38bdf8;">Catalog Evergreen</div>
|
||||
<div class="kpi-value" style="color: #38bdf8;">{evergreen_count}</div>
|
||||
<div class="kpi-subtitle">Strong passive catalog streams</div>
|
||||
<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,
|
||||
)
|
||||
|
||||
with k4:
|
||||
zombies = len(df[df["health_status"] == "DORMANT_CATALOG"])
|
||||
ig_count = df["instagram_followers"].notna().sum()
|
||||
st.markdown(
|
||||
f"""
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-title" style="color: #ef4444;">Dormant / Excluded</div>
|
||||
<div class="kpi-value" style="color: #ef4444;">{zombies}</div>
|
||||
<div class="kpi-subtitle">Disqualified (<NA> Rank)</div>
|
||||
<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,
|
||||
@@ -288,31 +286,24 @@ with k4:
|
||||
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
|
||||
# ----------------- Top 3 Leaders Podium -----------------
|
||||
top3 = (
|
||||
filtered_df[filtered_df["is_rank_eligible"] == True]
|
||||
.sort_values(by="hiffi_rank", ascending=True)
|
||||
.head(3)
|
||||
)
|
||||
# ----------------- Podium -----------------
|
||||
top3 = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).head(3)
|
||||
|
||||
if len(top3) >= 3:
|
||||
st.markdown("#### 👑 Market Leaders")
|
||||
st.markdown("#### 👑 Market Leaders (Published Rankings)")
|
||||
p1, p2, p3 = st.columns(3)
|
||||
|
||||
podium_data = [
|
||||
(p1, top3.iloc[0], "🥇 RANK #1", "podium-gold", "#f59e0b"),
|
||||
(p2, top3.iloc[1], "🥈 RANK #2", "podium-silver", "#94a3b8"),
|
||||
(p3, top3.iloc[2], "🥉 RANK #3", "podium-bronze", "#f97316"),
|
||||
(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"),
|
||||
]
|
||||
|
||||
for col, row_data, rank_label, css_class, accent_color in podium_data:
|
||||
badge_cls = (
|
||||
"badge-active"
|
||||
if row_data["health_status"] == "ACTIVE_HEALTHY"
|
||||
else "badge-evergreen"
|
||||
)
|
||||
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"
|
||||
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"
|
||||
|
||||
with col:
|
||||
st.markdown(
|
||||
@@ -320,22 +311,22 @@ if len(top3) >= 3:
|
||||
<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>
|
||||
<span class="badge {badge_cls}">{row_data['health_status'].replace('_', ' ')}</span>
|
||||
<span class="badge {badge_cls}">Tier {tier_val} Evidence</span>
|
||||
</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="display: flex; justify-content: space-between; background: #0b0f17; padding: 10px 14px; border-radius: 8px;">
|
||||
<div>
|
||||
<div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">HIFFI Score</div>
|
||||
<div style="font-size: 1.25rem; font-weight: 800; color: {accent_color};">{row_data['hiffi_score']:.1f}</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_xp_score']:.2f}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">Breakout Index</div>
|
||||
<div style="font-size: 1.25rem; font-weight: 800; color: #a855f7;">{bvi_str}</div>
|
||||
<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>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.68rem; color: #9ca3af; text-transform: uppercase;">Daily Views</div>
|
||||
<div style="font-size: 1.05rem; font-weight: 700; color: #e5e7eb;">{vel_str}</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;">{ig_str}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,233 +339,165 @@ if len(top3) >= 3:
|
||||
# ----------------- Leaderboard Grid -----------------
|
||||
st.markdown(f"#### 📋 {view_mode}")
|
||||
|
||||
if view_mode == "🏆 Overall HIFFI Rank":
|
||||
table_df = (
|
||||
filtered_df[filtered_df["is_rank_eligible"] == True]
|
||||
.sort_values(by="hiffi_rank", ascending=True)
|
||||
.copy()
|
||||
)
|
||||
if view_mode == "🏆 Published Multi-Platform Rank":
|
||||
table_df = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).copy()
|
||||
cols = [
|
||||
"hiffi_rank",
|
||||
"hiffi_xp_published_rank",
|
||||
"hiffi_xp_rank",
|
||||
"name",
|
||||
"subgenre",
|
||||
"hiffi_score",
|
||||
"breakout_rank",
|
||||
"breakout_velocity_index",
|
||||
"daily_view_velocity",
|
||||
"momentum_30d",
|
||||
"health_status",
|
||||
"platform_evidence_tier",
|
||||
"hiffi_xp_score",
|
||||
"spotify_monthly_listeners",
|
||||
"instagram_followers",
|
||||
"effective_weights",
|
||||
"anomaly_flags"
|
||||
]
|
||||
elif view_mode == "🚀 Breakout Velocity (BVI)":
|
||||
table_df = (
|
||||
filtered_df[
|
||||
(filtered_df["is_rank_eligible"] == True)
|
||||
& (filtered_df["breakout_rank"].notna())
|
||||
]
|
||||
.sort_values(by="breakout_rank", ascending=True)
|
||||
filtered_df[filtered_df["breakout_velocity_index"].notna()]
|
||||
.sort_values(by="breakout_velocity_index", ascending=False)
|
||||
.copy()
|
||||
)
|
||||
cols = [
|
||||
"breakout_rank",
|
||||
"name",
|
||||
"subgenre",
|
||||
"breakout_velocity_index",
|
||||
"hiffi_rank",
|
||||
"hiffi_score",
|
||||
"hiffi_xp_published_rank",
|
||||
"daily_view_velocity",
|
||||
"momentum_30d",
|
||||
"subscriber_count",
|
||||
"health_status",
|
||||
"subgenre"
|
||||
]
|
||||
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 = [
|
||||
"hiffi_xp_published_rank",
|
||||
"name",
|
||||
"subgenre",
|
||||
"hiffi_score",
|
||||
"breakout_velocity_index",
|
||||
"daily_view_velocity",
|
||||
"subscriber_count",
|
||||
"health_status",
|
||||
"confidence_score",
|
||||
"is_rank_eligible",
|
||||
"production_hiffi_rank",
|
||||
"hiffi_xp_rank",
|
||||
"rank_delta",
|
||||
"platform_evidence_tier",
|
||||
"hiffi_xp_score",
|
||||
"spotify_monthly_listeners",
|
||||
"instagram_followers",
|
||||
"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]
|
||||
|
||||
st.dataframe(
|
||||
table_df[render_cols].reset_index(drop=True),
|
||||
use_container_width=True,
|
||||
height=290,
|
||||
width="stretch",
|
||||
height=320,
|
||||
column_config={
|
||||
"hiffi_rank": st.column_config.NumberColumn("Rank", format="%d", width="small"),
|
||||
"breakout_rank": st.column_config.NumberColumn("BVI Rank", format="%d", width="small"),
|
||||
"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"),
|
||||
"platform_evidence_tier": st.column_config.NumberColumn("Tier", format="%d", width="small"),
|
||||
"name": st.column_config.TextColumn("Artist", width="medium"),
|
||||
"subgenre": st.column_config.TextColumn("Vertical", width="medium"),
|
||||
"hiffi_score": st.column_config.ProgressColumn(
|
||||
"HIFFI Score",
|
||||
format="%.1f",
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
width="medium",
|
||||
),
|
||||
"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"),
|
||||
"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"),
|
||||
"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"),
|
||||
},
|
||||
)
|
||||
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
|
||||
# ----------------- Visual Insights Charts -----------------
|
||||
st.markdown("#### 📊 Market Landscape Visualizer")
|
||||
st.markdown("#### 📊 Multi-Platform Analytics")
|
||||
|
||||
c_left, c_right = st.columns(2)
|
||||
|
||||
with c_left:
|
||||
scatter_df = filtered_df[filtered_df["is_rank_eligible"] == True].copy()
|
||||
fig_scatter = px.scatter(
|
||||
scatter_df,
|
||||
x="daily_view_velocity",
|
||||
y="momentum_30d",
|
||||
size="subscriber_count",
|
||||
color="health_status",
|
||||
color_discrete_map={
|
||||
"ACTIVE_HEALTHY": "#10b981",
|
||||
"CATALOG_EVERGREEN": "#38bdf8",
|
||||
"DORMANT_CATALOG": "#ef4444",
|
||||
},
|
||||
hover_name="name",
|
||||
hover_data={"daily_view_velocity": ":,d", "momentum_30d": ":.1f%"},
|
||||
log_x=True,
|
||||
title="View Velocity vs. 30-Day Momentum",
|
||||
labels={
|
||||
"daily_view_velocity": "Daily Views / Day (Log Scale)",
|
||||
"momentum_30d": "30-Day Growth (%)",
|
||||
"health_status": "Vitality",
|
||||
},
|
||||
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, use_container_width=True)
|
||||
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:
|
||||
bvi_df = (
|
||||
filtered_df[filtered_df["breakout_velocity_index"].notna()]
|
||||
.sort_values(by="breakout_velocity_index", ascending=True)
|
||||
)
|
||||
fig_bar = px.bar(
|
||||
bvi_df,
|
||||
x="breakout_velocity_index",
|
||||
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"},
|
||||
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_bar.update_layout(
|
||||
fig_pie.update_layout(
|
||||
paper_bgcolor="#111827",
|
||||
plot_bgcolor="#111827",
|
||||
font=dict(color="#9ca3af"),
|
||||
coloraxis_showscale=False,
|
||||
margin=dict(l=20, r=20, t=40, b=20),
|
||||
height=320,
|
||||
)
|
||||
st.plotly_chart(fig_bar, use_container_width=True)
|
||||
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
st.plotly_chart(fig_pie, width="stretch")
|
||||
|
||||
# ----------------- Deep Dive Scouting Dossier -----------------
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
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]
|
||||
|
||||
# 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)
|
||||
|
||||
with d1:
|
||||
h_val = row.get("hiffi_score")
|
||||
st.metric("HIFFI Score", f"{h_val:.1f}" if pd.notna(h_val) else "N/A")
|
||||
h_val = row.get("hiffi_xp_score")
|
||||
st.metric("HIFFI-XP Score", f"{h_val:.2f}" if pd.notna(h_val) else "N/A")
|
||||
with d2:
|
||||
b_val = row.get("breakout_velocity_index")
|
||||
st.metric("Breakout Index (BVI)", f"{b_val:.1f}" if pd.notna(b_val) else "N/A")
|
||||
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:
|
||||
s_val = row.get("subscriber_count")
|
||||
st.metric("Subscribers", f"{int(s_val):,}" if pd.notna(s_val) else "N/A")
|
||||
sp_l = row.get("spotify_monthly_listeners")
|
||||
st.metric("Spotify Listeners", f"{int(sp_l):,}" if pd.notna(sp_l) else "Unindexed (NaN)")
|
||||
with d4:
|
||||
conf = row.get("confidence_score", 0.0)
|
||||
tier = row.get("confidence_tier", "Standard")
|
||||
st.metric("Entity Confidence", f"{conf * 100:.0f}%", delta=tier, delta_color="normal")
|
||||
ig_f = row.get("instagram_followers")
|
||||
st.metric("Instagram Followers", f"{int(ig_f):,}" if pd.notna(ig_f) else "Unindexed (NaN)")
|
||||
|
||||
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(
|
||||
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="display: flex; gap: 24px; align-items: center;">
|
||||
<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;">
|
||||
<div><b>Artist:</b> {row['name']}</div>
|
||||
<div><b>Vertical:</b> <span style="color: #a855f7;">{row.get('subgenre', 'N/A')}</span></div>
|
||||
<div><b>Channel:</b> {row.get('channel_title', 'Unknown')}</div>
|
||||
<div><b>Status:</b> <span class="badge {badge_cls}">{row.get('health_status', 'UNKNOWN')}</span></div>
|
||||
<div><b>Zombie Flag:</b> {'⚠️ Yes' if row.get('is_zombie') else '✅ Clean'}</div>
|
||||
<div><b>Eligible for Rank:</b> {'✅ Yes' if row.get('is_rank_eligible') else '❌ Disqualified'}</div>
|
||||
<div><b>Evidence Tier:</b> Tier {int(row.get('platform_evidence_tier', 1))}</div>
|
||||
<div><b>Instagram Status:</b> {row.get('instagram_verification_status', 'N/A')}</div>
|
||||
<div><b>Spotify Source:</b> {row.get('spotify_data_source', 'N/A')}</div>
|
||||
<div><b>Effective Weights:</b> {row.get('effective_weights', 'N/A')}</div>
|
||||
<div><b>Anomaly Flags:</b> {row.get('anomaly_flags', 'CLEAN')}</div>
|
||||
</div>
|
||||
</div>
|
||||
""",
|
||||
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.")
|
||||
)
|
||||
Reference in new issue
Block a user