feat(sales): implement Phase 1 & 2 commercial sales telemetry and dashboard analytics
This commit is contained in:
1 parent
6b567df015
commit
8f77295de9
7 files changed
+1371
-575
No files matched your search
@@ -2,13 +2,14 @@
|
||||
app.py
|
||||
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.
|
||||
ChartMasters record sales (CSPC/EAS), breakout velocity index, and deep-dive scouting dossier.
|
||||
"""
|
||||
|
||||
import io
|
||||
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(
|
||||
@@ -130,7 +131,14 @@ def load_data():
|
||||
"spotify_daily_change",
|
||||
"instagram_followers",
|
||||
"confidence_score",
|
||||
"breakout_rank"
|
||||
"breakout_rank",
|
||||
"chartmasters_sales_rank",
|
||||
"cohort_sales_rank",
|
||||
"pure_album_sales",
|
||||
"physical_singles_sales",
|
||||
"digital_singles_sales",
|
||||
"streaming_eas",
|
||||
"total_eas_cspc",
|
||||
]
|
||||
for col in numeric_cols:
|
||||
if col in df.columns:
|
||||
@@ -163,7 +171,7 @@ df = load_data()
|
||||
# ----------------- Sidebar Controls -----------------
|
||||
with st.sidebar:
|
||||
st.markdown("### ⚡ HIFFI Multi-Platform")
|
||||
st.caption("YouTube • Spotify • Instagram Analytics")
|
||||
st.caption("YouTube • Spotify • Instagram • Sales Analytics")
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
|
||||
if df.empty:
|
||||
@@ -172,7 +180,12 @@ with st.sidebar:
|
||||
|
||||
view_mode = st.radio(
|
||||
"Leaderboard Perspective:",
|
||||
["🏆 Published Multi-Platform Rank", "🚀 Breakout Velocity (BVI)", "🔍 Full Cohort Audit Grid"],
|
||||
[
|
||||
"🏆 Published Multi-Platform Rank",
|
||||
"🚀 Breakout Velocity (BVI)",
|
||||
"💿 Record Sales & CSPC (ChartMasters)",
|
||||
"🔍 Full Cohort Audit Grid",
|
||||
],
|
||||
index=0,
|
||||
)
|
||||
|
||||
@@ -217,21 +230,21 @@ with st.sidebar:
|
||||
)
|
||||
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
st.caption("Weights: YouTube (50%) • Spotify (30%) • Instagram (20%)")
|
||||
st.caption("Governance: Multi-Platform Signals + Verified ChartMasters Provenance")
|
||||
|
||||
# ----------------- Main Header -----------------
|
||||
head_col1, head_col2 = st.columns([3, 1])
|
||||
with head_col1:
|
||||
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)
|
||||
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)
|
||||
with head_col2:
|
||||
st.markdown(
|
||||
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,
|
||||
)
|
||||
|
||||
# ----------------- KPI Cards Row -----------------
|
||||
k1, k2, k3, k4 = st.columns(4)
|
||||
# ----------------- KPI Cards Row (5 Columns) -----------------
|
||||
k1, k2, k3, k4, k5 = st.columns(5)
|
||||
|
||||
with k1:
|
||||
st.markdown(
|
||||
@@ -284,6 +297,19 @@ with k4:
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
|
||||
# ----------------- Podium -----------------
|
||||
@@ -367,6 +393,24 @@ elif view_mode == "🚀 Breakout Velocity (BVI)":
|
||||
"subscriber_count",
|
||||
"subgenre"
|
||||
]
|
||||
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"
|
||||
]
|
||||
else:
|
||||
table_df = filtered_df.sort_values(by="hiffi_xp_published_rank", ascending=True).copy()
|
||||
cols = [
|
||||
@@ -379,6 +423,8 @@ else:
|
||||
"hiffi_xp_score",
|
||||
"spotify_monthly_listeners",
|
||||
"instagram_followers",
|
||||
"total_eas_cspc",
|
||||
"sales_data_status",
|
||||
"evidence_exclusions",
|
||||
"anomaly_flags"
|
||||
]
|
||||
@@ -388,15 +434,24 @@ render_cols = [c for c in cols if c in table_df.columns]
|
||||
st.dataframe(
|
||||
table_df[render_cols].reset_index(drop=True),
|
||||
width="stretch",
|
||||
height=320,
|
||||
height=330,
|
||||
column_config={
|
||||
"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"),
|
||||
"cohort_sales_rank": st.column_config.NumberColumn("Cohort #", format="#%d", width="small"),
|
||||
"chartmasters_sales_rank": st.column_config.NumberColumn("Global #", format="#%d", width="small"),
|
||||
"platform_evidence_tier": st.column_config.NumberColumn("Tier", format="%d", width="small"),
|
||||
"name": st.column_config.TextColumn("Artist", width="medium"),
|
||||
"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"),
|
||||
"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"),
|
||||
"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"),
|
||||
@@ -407,62 +462,149 @@ st.dataframe(
|
||||
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
|
||||
# ----------------- Visual Insights Charts -----------------
|
||||
st.markdown("#### 📊 Multi-Platform Analytics")
|
||||
# ----------------- Visual Insights & Format Analysis -----------------
|
||||
st.markdown("#### 📊 Commercial & Multi-Platform Analytics")
|
||||
|
||||
c_left, c_right = st.columns(2)
|
||||
if view_mode == "💿 Record Sales & CSPC (ChartMasters)":
|
||||
c_sales1, c_sales2 = 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",
|
||||
with c_sales1:
|
||||
top_sales = (
|
||||
filtered_df[filtered_df["total_eas_cspc"].notna()]
|
||||
.sort_values(by="total_eas_cspc", ascending=False)
|
||||
.head(15)
|
||||
)
|
||||
fig_scatter.update_layout(
|
||||
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(
|
||||
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(
|
||||
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_pie, width="stretch")
|
||||
st.plotly_chart(fig_pie, width="stretch")
|
||||
|
||||
# ----------------- Deep Dive Scouting Dossier -----------------
|
||||
st.markdown("<div class='custom-divider'></div>", unsafe_allow_html=True)
|
||||
@@ -471,7 +613,7 @@ 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]
|
||||
|
||||
d1, d2, d3, d4 = st.columns(4)
|
||||
d1, d2, d3, d4, d5 = st.columns(5)
|
||||
|
||||
with d1:
|
||||
h_val = row.get("hiffi_xp_score")
|
||||
@@ -485,6 +627,9 @@ with d3:
|
||||
with d4:
|
||||
ig_f = row.get("instagram_followers")
|
||||
st.metric("Instagram Followers", f"{int(ig_f):,}" if pd.notna(ig_f) else "Unindexed (NaN)")
|
||||
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)")
|
||||
|
||||
st.markdown(
|
||||
f"""
|
||||
@@ -492,8 +637,12 @@ st.markdown(
|
||||
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
|
||||
<div><b>Artist:</b> {row['name']}</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>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>
|
||||
<div><b>Effective Weights:</b> {row.get('effective_weights', 'N/A')}</div>
|
||||
<div><b>Anomaly Flags:</b> {row.get('anomaly_flags', 'CLEAN')}</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user