feat(cron): finalize top 500 cohort weekly ranking pipeline with delta tracking
This commit is contained in:
1 parent
6fc35cd50e
commit
c2962e3bc2
3 files changed
+676
-501
No files matched your search
Binary file not shown.
+501
-501
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,175 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if BASE_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, BASE_DIR)
|
||||||
|
|
||||||
|
from core.ranking_engine import HIFFIRankingEngine
|
||||||
|
|
||||||
|
DB_PATH = os.path.join(BASE_DIR, "data", "hiffi_catalog.db")
|
||||||
|
CSV_PATH = os.path.join(BASE_DIR, "data", "hiffi_multiplatform_500.csv")
|
||||||
|
LOG_PATH = os.path.join(BASE_DIR, "cron_ranking.log")
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
filename=LOG_PATH,
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("HIFFI_CRON")
|
||||||
|
|
||||||
|
def run_weekly_ranking_pipeline():
|
||||||
|
start_time = datetime.now(timezone.utc)
|
||||||
|
print("\n========================================================")
|
||||||
|
print(" HIFFI AUTOMATED WEEKLY RANKING PIPELINE (TOP 500 ONLY)")
|
||||||
|
print(f" Run Timestamp (UTC): {start_time.isoformat()}")
|
||||||
|
print("========================================================\n")
|
||||||
|
logger.info("Starting automated 500-cohort weekly ranking pipeline...")
|
||||||
|
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
err = f"Database not found at {DB_PATH}"
|
||||||
|
logger.error(err)
|
||||||
|
print(f"ERROR: {err}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not os.path.exists(CSV_PATH):
|
||||||
|
err = f"Cohort CSV not found at {CSV_PATH}"
|
||||||
|
logger.error(err)
|
||||||
|
print(f"ERROR: {err}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read the verified 500 cohort IDs directly from the baseline CSV
|
||||||
|
base_cohort_df = pd.read_csv(CSV_PATH)
|
||||||
|
cohort_ids = [str(x) for x in base_cohort_df['artist_id'].dropna().tolist()][:500]
|
||||||
|
placeholders = ",".join([f"'{aid}'" for aid in cohort_ids])
|
||||||
|
|
||||||
|
# 1. Fetch telemetry strictly for the 500 cohort
|
||||||
|
query = f"""
|
||||||
|
WITH latest_telemetry AS (
|
||||||
|
SELECT t.*, ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY t.artist_id
|
||||||
|
ORDER BY t.last_synced_at DESC, t.telemetry_id DESC
|
||||||
|
) as rn
|
||||||
|
FROM telemetry_metrics t
|
||||||
|
WHERE t.artist_id IN ({placeholders})
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
a.artist_id,
|
||||||
|
a.artist_name,
|
||||||
|
a.country_origin,
|
||||||
|
a.image_url,
|
||||||
|
a.spotify_url,
|
||||||
|
a.youtube_url,
|
||||||
|
a.instagram_url,
|
||||||
|
COALESCE(t.spotify_monthly_listeners, 0) as spotify_monthly_listeners,
|
||||||
|
COALESCE(t.subscribers_count, 0) as subscriber_count,
|
||||||
|
COALESCE(t.daily_view_velocity, 0) as daily_view_velocity,
|
||||||
|
COALESCE(t.instagram_followers, 0) as instagram_followers,
|
||||||
|
r.catalog_rank as previous_rank,
|
||||||
|
r.catalog_intelligence_tier as current_tier
|
||||||
|
FROM artists a
|
||||||
|
LEFT JOIN latest_telemetry t ON a.artist_id = t.artist_id AND t.rn = 1
|
||||||
|
LEFT JOIN rankings r ON a.artist_id = r.artist_id
|
||||||
|
WHERE a.artist_id IN ({placeholders});
|
||||||
|
"""
|
||||||
|
df = pd.read_sql_query(query, conn)
|
||||||
|
total_artists = len(df)
|
||||||
|
print(f"Loaded verified cohort: {total_artists} artists.")
|
||||||
|
logger.info(f"Loaded {total_artists} cohort artists for recalculation.")
|
||||||
|
|
||||||
|
# 2. Run Core Scoring Engine (45% SP / 35% YT / 20% IG)
|
||||||
|
engine = HIFFIRankingEngine()
|
||||||
|
ranked_df = engine.calculate_hiffi_xp(df)
|
||||||
|
|
||||||
|
if 'hiffi_xp_score' in ranked_df.columns:
|
||||||
|
score_col = 'hiffi_xp_score'
|
||||||
|
elif 'score' in ranked_df.columns:
|
||||||
|
score_col = 'score'
|
||||||
|
elif 'production_hiffi_score' in ranked_df.columns:
|
||||||
|
score_col = 'production_hiffi_score'
|
||||||
|
else:
|
||||||
|
numeric_cols = ranked_df.select_dtypes(include=['float64', 'float32']).columns
|
||||||
|
score_col = numeric_cols[0]
|
||||||
|
|
||||||
|
# Sort descending by score
|
||||||
|
ranked_df = ranked_df.sort_values(by=score_col, ascending=False).reset_index(drop=True)
|
||||||
|
ranked_df['new_rank'] = ranked_df.index + 1
|
||||||
|
|
||||||
|
# Track rank deltas
|
||||||
|
ranked_df['rank_delta'] = ranked_df.apply(
|
||||||
|
lambda row: int(row['previous_rank'] - row['new_rank'])
|
||||||
|
if pd.notna(row['previous_rank']) and row['previous_rank'] > 0
|
||||||
|
else 0,
|
||||||
|
axis=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def compute_tier(rank_val):
|
||||||
|
if rank_val <= 50:
|
||||||
|
return "TIER_1_ICON"
|
||||||
|
elif rank_val <= 200:
|
||||||
|
return "TIER_2_MAINSTREAM"
|
||||||
|
else:
|
||||||
|
return "TIER_3_RISING"
|
||||||
|
|
||||||
|
ranked_df['tier_rank'] = ranked_df['new_rank']
|
||||||
|
ranked_df['catalog_intelligence_tier'] = ranked_df['new_rank'].apply(compute_tier)
|
||||||
|
|
||||||
|
# 3. Update SQLite database rankings table for the 500 cohort
|
||||||
|
print("Committing 500 cohort ranks to SQLite database...")
|
||||||
|
update_data = []
|
||||||
|
for _, row in ranked_df.iterrows():
|
||||||
|
aid = str(row['artist_id'])
|
||||||
|
c_rank = int(row['new_rank'])
|
||||||
|
t_rank = int(row['tier_rank'])
|
||||||
|
tier_str = str(row['catalog_intelligence_tier'])
|
||||||
|
update_data.append((c_rank, t_rank, tier_str, aid))
|
||||||
|
|
||||||
|
cur.executemany("""
|
||||||
|
UPDATE rankings
|
||||||
|
SET catalog_rank = ?,
|
||||||
|
tier_rank = ?,
|
||||||
|
catalog_intelligence_tier = ?
|
||||||
|
WHERE artist_id = ?;
|
||||||
|
""", update_data)
|
||||||
|
conn.commit()
|
||||||
|
print(f"Updated database ranking records for {len(update_data)} artists.")
|
||||||
|
|
||||||
|
# 4. Save updated 500 cohort CSV
|
||||||
|
ranked_df['catalog_rank'] = ranked_df['new_rank']
|
||||||
|
if 'name' not in ranked_df.columns and 'artist_name' in ranked_df.columns:
|
||||||
|
ranked_df['name'] = ranked_df['artist_name']
|
||||||
|
|
||||||
|
ranked_df.to_csv(CSV_PATH, index=False)
|
||||||
|
print(f"Exported Top 500 cohort CSV to: {CSV_PATH}")
|
||||||
|
|
||||||
|
# Telemetry summary
|
||||||
|
print("\n--- TOP 5 RANKING SNAPSHOT ---")
|
||||||
|
for _, r in ranked_df.head(5).iterrows():
|
||||||
|
delta_str = f"+{r['rank_delta']}" if r['rank_delta'] > 0 else str(r['rank_delta'])
|
||||||
|
print(f"#{r['new_rank']} {r.get('name', r.get('artist_name'))} | Score: {r[score_col]:.2f} | Tier: {r['catalog_intelligence_tier']} | Delta: {delta_str}")
|
||||||
|
|
||||||
|
elapsed = (datetime.now(timezone.utc) - start_time).total_seconds()
|
||||||
|
print(f"\nWeekly ranking pipeline completed successfully in {elapsed:.2f}s.")
|
||||||
|
logger.info(f"Weekly ranking pipeline finished successfully in {elapsed:.2f}s.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
logger.error(f"Weekly ranking run failed: {str(e)}", exc_info=True)
|
||||||
|
print(f"\nERROR running ranking pipeline: {e}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_weekly_ranking_pipeline()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
Reference in new issue
Block a user