2026-10-23 15:48:38 +05:30
|
|
|
"""
|
|
|
|
|
pipeline/batch_500_runner.py
|
2026-10-24 15:57:20 +05:30
|
|
|
Batch Multi-Platform Signal Ingestion & Scoring Engine for 500 Hip-Hop Artists.
|
|
|
|
|
Integrates YouTube baseline production data, Kworb/Spotify telemetry, and verified Instagram metadata.
|
|
|
|
|
Strictly adheres to zero-imputation policy and outputs fully audited provenance datasets.
|
2026-10-23 15:48:38 +05:30
|
|
|
"""
|
|
|
|
|
|
2026-10-24 12:42:22 +05:30
|
|
|
from datetime import datetime, timezone
|
2026-10-24 11:20:10 +05:30
|
|
|
import json
|
2026-10-23 15:48:38 +05:30
|
|
|
from pathlib import Path
|
2026-10-24 15:57:20 +05:30
|
|
|
import sys
|
|
|
|
|
from typing import Dict, Optional
|
2026-10-24 11:20:10 +05:30
|
|
|
|
|
|
|
|
import pandas as pd
|
2026-10-23 15:48:38 +05:30
|
|
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
|
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
|
|
2026-10-24 12:42:22 +05:30
|
|
|
from collectors.instagram_verifier import InstagramCandidateVerifier
|
2026-10-24 15:57:20 +05:30
|
|
|
from collectors.kworb_spotify_collector import KworbStreamingCollector
|
|
|
|
|
from collectors.spotify_api_collector import SpotifyAPICollector
|
|
|
|
|
from collectors.spotify_fallback_collector import SpotifyFallbackCollector
|
2026-10-27 10:23:02 +05:30
|
|
|
from core.ranking_engine import HIFFIRankingEngine
|
2026-10-23 15:48:38 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
DATA_DIR = PROJECT_ROOT / "data"
|
|
|
|
|
BASE_DATASET = DATA_DIR / "hiffi_multiplatform_500.csv"
|
|
|
|
|
OVERRIDES_FILE = DATA_DIR / "handle_overrides.json"
|
2026-10-23 15:48:38 +05:30
|
|
|
|
|
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
def load_handle_overrides() -> Dict[str, object]:
|
|
|
|
|
"""Loads manual curator overrides for handles and verification statuses."""
|
|
|
|
|
if OVERRIDES_FILE.exists():
|
|
|
|
|
try:
|
|
|
|
|
with open(OVERRIDES_FILE, "r", encoding="utf-8") as f:
|
|
|
|
|
return json.load(f)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"[Loader] Warning: Could not parse handle overrides: {e}")
|
|
|
|
|
return {}
|
2026-10-24 11:20:10 +05:30
|
|
|
|
|
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
def calculate_hiffi_xp_scores(df: pd.DataFrame) -> pd.DataFrame:
|
|
|
|
|
"""
|
2026-10-27 10:23:02 +05:30
|
|
|
Delegates to the canonical HIFFIRankingEngine to enforce zero-imputation,
|
|
|
|
|
proportional dynamic redistribution, anomaly detection, and publication eligibility gates.
|
2026-10-24 15:57:20 +05:30
|
|
|
"""
|
2026-10-27 10:23:02 +05:30
|
|
|
engine = HIFFIRankingEngine()
|
|
|
|
|
scored = engine.calculate_hiffi_xp(df)
|
|
|
|
|
flagged = engine.detect_metric_anomalies(scored)
|
|
|
|
|
gated = engine.apply_publication_eligibility_gates(flagged)
|
|
|
|
|
return gated
|
2026-10-24 15:57:20 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_multiplatform_pipeline(
|
|
|
|
|
input_path: Optional[Path] = None,
|
|
|
|
|
output_path: Optional[Path] = None
|
|
|
|
|
):
|
|
|
|
|
source_file = input_path or BASE_DATASET
|
|
|
|
|
dest_file = output_path or BASE_DATASET
|
|
|
|
|
|
2026-10-24 11:20:10 +05:30
|
|
|
print("=" * 90)
|
2026-10-24 12:42:22 +05:30
|
|
|
print("HIFFI MULTI-PLATFORM RUNNER (VERIFIED SIGNALS & STRICT GOVERNANCE)")
|
2026-10-24 11:20:10 +05:30
|
|
|
print("Total Base Cohort: 500 artists")
|
|
|
|
|
print("=" * 90)
|
2026-10-23 15:48:38 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
if not source_file.exists():
|
|
|
|
|
raise FileNotFoundError(f"Source cohort dataset not found at: {source_file}")
|
2026-10-23 15:48:38 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
print(f"[Loader] Loading base cohort from: {source_file.name}")
|
|
|
|
|
df = pd.read_csv(source_file)
|
|
|
|
|
initial_ig_verified = df["instagram_followers"].notna().sum() if "instagram_followers" in df.columns else 0
|
|
|
|
|
print(f"[Loader] Loaded {len(df)} checkpoints ({initial_ig_verified} verified follower records).")
|
2026-10-23 15:48:38 +05:30
|
|
|
|
2026-10-27 10:23:02 +05:30
|
|
|
now_utc = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
|
|
|
|
# Ensure baseline YouTube timestamps exist for freshness validation
|
|
|
|
|
if "youtube_updated_at" not in df.columns or df["youtube_updated_at"].isna().all():
|
|
|
|
|
df["youtube_updated_at"] = df.get("updated_at", now_utc)
|
|
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
# 1. Instagram Candidate Verification & Overrides Ingestion
|
2026-10-24 12:42:22 +05:30
|
|
|
print("\n[Phase 1] Executing Automated Instagram Verification & Provenance...")
|
2026-10-24 15:57:20 +05:30
|
|
|
overrides = load_handle_overrides()
|
|
|
|
|
verifier = InstagramCandidateVerifier()
|
2026-10-23 15:48:38 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
ig_handles = []
|
|
|
|
|
ig_followers = []
|
|
|
|
|
ig_conf = []
|
|
|
|
|
ig_status = []
|
|
|
|
|
ig_evidence = []
|
|
|
|
|
ig_dates = []
|
|
|
|
|
|
2026-10-24 12:42:22 +05:30
|
|
|
for _, row in df.iterrows():
|
2026-10-24 15:57:20 +05:30
|
|
|
name = str(row["name"]).strip()
|
|
|
|
|
override = overrides.get(name)
|
2026-10-24 12:42:22 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
if override is not None:
|
|
|
|
|
if isinstance(override, dict):
|
|
|
|
|
handle_val = override.get("instagram_handle") or override.get("handle")
|
|
|
|
|
followers_val = override.get("instagram_followers") or row.get("instagram_followers")
|
|
|
|
|
evidence_val = override.get("auditor_notes", "Human curator approved in handle_overrides.json")
|
|
|
|
|
date_val = override.get("curated_at", now_utc)
|
|
|
|
|
else:
|
|
|
|
|
handle_val = str(override).strip()
|
|
|
|
|
followers_val = row.get("instagram_followers")
|
|
|
|
|
evidence_val = "Curator override from handle_overrides.json"
|
|
|
|
|
date_val = now_utc
|
2026-10-24 12:42:22 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
ig_handles.append(handle_val)
|
|
|
|
|
ig_followers.append(followers_val)
|
|
|
|
|
ig_conf.append(1.0)
|
|
|
|
|
ig_status.append("MANUAL_VERIFIED")
|
|
|
|
|
ig_evidence.append(evidence_val)
|
|
|
|
|
ig_dates.append(date_val)
|
|
|
|
|
else:
|
|
|
|
|
cand_handle = row.get("instagram_handle")
|
|
|
|
|
cand_followers = row.get("instagram_followers")
|
2026-10-24 12:42:22 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
if hasattr(verifier, "evaluate_candidate"):
|
|
|
|
|
ver_res = verifier.evaluate_candidate(
|
|
|
|
|
artist_name=name,
|
|
|
|
|
candidate_handle=cand_handle,
|
2026-10-27 10:23:02 +05:30
|
|
|
follower_count=cand_followers if pd.notna(cand_followers) else None
|
2026-10-24 15:57:20 +05:30
|
|
|
)
|
|
|
|
|
elif hasattr(verifier, "verify_candidate"):
|
|
|
|
|
ver_res = verifier.verify_candidate(
|
|
|
|
|
artist_name=name,
|
|
|
|
|
candidate_handle=cand_handle,
|
|
|
|
|
profile_data={"followers": cand_followers}
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
ver_res = {
|
|
|
|
|
"status": "UNMATCHED_NO_HANDLE" if pd.isna(cand_handle) else "AUTOMATED_VERIFIED",
|
|
|
|
|
"confidence_score": 0.85 if pd.notna(cand_handle) else 0.0,
|
|
|
|
|
"evidence": "Existing catalog checkpoint",
|
|
|
|
|
"followers": cand_followers
|
|
|
|
|
}
|
2026-10-24 12:42:22 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
status = ver_res.get("status") or ver_res.get("verification_status") or "UNCERTAIN_NEEDS_REVIEW"
|
|
|
|
|
ig_handles.append(cand_handle if status != "UNMATCHED_NO_HANDLE" else None)
|
|
|
|
|
ig_followers.append(ver_res.get("followers", cand_followers))
|
|
|
|
|
ig_conf.append(ver_res.get("confidence_score", 0.0))
|
|
|
|
|
ig_status.append(status)
|
|
|
|
|
ig_evidence.append(ver_res.get("evidence", "Candidate verification heuristics"))
|
|
|
|
|
ig_dates.append(now_utc)
|
2026-10-24 12:42:22 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
df["instagram_handle"] = ig_handles
|
|
|
|
|
df["instagram_followers"] = ig_followers
|
|
|
|
|
df["instagram_confidence_score"] = ig_conf
|
|
|
|
|
df["instagram_verification_status"] = ig_status
|
|
|
|
|
df["instagram_audit_evidence"] = ig_evidence
|
|
|
|
|
df["instagram_collected_at"] = ig_dates
|
2026-10-27 10:23:02 +05:30
|
|
|
df["instagram_updated_at"] = ig_dates
|
2026-10-24 12:42:22 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
# 2. Spotify Multi-Tier Telemetry Ingestion (Tier 1: Kworb -> Tier 2: Official API -> Tier 3: Embed Fallback)
|
|
|
|
|
print("\n[Phase 2] Ingesting Spotify Telemetry (Tier 1: Kworb -> Tier 2: Official API -> Tier 3: Embed Fallback)...")
|
2026-10-24 12:42:22 +05:30
|
|
|
kworb = KworbStreamingCollector()
|
|
|
|
|
kworb.load_aggregator_tables(max_pages=2)
|
2026-10-24 15:57:20 +05:30
|
|
|
api_collector = SpotifyAPICollector()
|
2026-10-24 12:42:22 +05:30
|
|
|
fallback_collector = SpotifyFallbackCollector()
|
|
|
|
|
|
|
|
|
|
sp_listeners = []
|
|
|
|
|
sp_daily = []
|
|
|
|
|
sp_source = []
|
|
|
|
|
sp_status = []
|
|
|
|
|
sp_dates = []
|
|
|
|
|
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
|
name = str(row["name"]).strip()
|
|
|
|
|
listeners, daily = kworb.get_artist_metrics(name)
|
|
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
# Tier 1: Kworb Public Aggregator (Monthly Listeners + Daily Change)
|
2026-10-24 12:42:22 +05:30
|
|
|
if listeners is not None:
|
|
|
|
|
sp_listeners.append(listeners)
|
|
|
|
|
sp_daily.append(daily)
|
|
|
|
|
sp_source.append("kworb_public_aggregator")
|
|
|
|
|
sp_status.append("VERIFIED_PUBLIC_AGGREGATOR")
|
|
|
|
|
sp_dates.append(now_utc)
|
2026-10-24 15:57:20 +05:30
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
sp_id = row.get("spotify_id") or row.get("spotify_uri")
|
|
|
|
|
has_valid_id = pd.notna(sp_id) and str(sp_id).strip().lower() not in ("nan", "none", "")
|
|
|
|
|
|
|
|
|
|
# Tier 2: Official Spotify Web API (if enabled & accessible)
|
|
|
|
|
api_success = False
|
|
|
|
|
if has_valid_id and api_collector.is_configured and not api_collector.is_access_forbidden:
|
|
|
|
|
api_res = api_collector.fetch_artist(str(sp_id))
|
|
|
|
|
if api_res.get("spotify_status") == "VERIFIED_SPOTIFY_API":
|
2026-10-24 12:42:22 +05:30
|
|
|
sp_listeners.append(None)
|
|
|
|
|
sp_daily.append(None)
|
2026-10-24 15:57:20 +05:30
|
|
|
sp_source.append(api_res["spotify_data_source"])
|
|
|
|
|
sp_status.append(api_res["spotify_status"])
|
|
|
|
|
sp_dates.append(api_res["collection_date"])
|
|
|
|
|
api_success = True
|
2026-10-24 12:42:22 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
if api_success:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Tier 3: Public Embed Fallback
|
|
|
|
|
if has_valid_id:
|
|
|
|
|
fb_res = fallback_collector.fetch_artist_fallback(name, str(sp_id))
|
|
|
|
|
sp_listeners.append(fb_res.get("spotify_monthly_listeners"))
|
|
|
|
|
sp_daily.append(fb_res.get("spotify_daily_change"))
|
|
|
|
|
sp_source.append(fb_res.get("spotify_data_source"))
|
|
|
|
|
sp_status.append(fb_res.get("spotify_status"))
|
|
|
|
|
sp_dates.append(fb_res.get("collection_date", now_utc))
|
|
|
|
|
else:
|
|
|
|
|
sp_listeners.append(None)
|
|
|
|
|
sp_daily.append(None)
|
|
|
|
|
sp_source.append(None)
|
|
|
|
|
sp_status.append("UNAVAILABLE_IN_TOP_5000")
|
|
|
|
|
sp_dates.append(now_utc)
|
|
|
|
|
|
|
|
|
|
df["spotify_monthly_listeners"] = sp_listeners
|
|
|
|
|
df["spotify_daily_change"] = sp_daily
|
2026-10-24 12:42:22 +05:30
|
|
|
df["spotify_data_source"] = sp_source
|
|
|
|
|
df["spotify_status"] = sp_status
|
2026-10-24 15:57:20 +05:30
|
|
|
df["spotify_collected_at"] = sp_dates
|
2026-10-27 10:23:02 +05:30
|
|
|
df["spotify_updated_at"] = sp_dates
|
2026-10-24 12:42:22 +05:30
|
|
|
|
2026-10-27 10:23:02 +05:30
|
|
|
# 3. Canonical Scoring & Governance
|
2026-10-24 15:57:20 +05:30
|
|
|
scored_df = calculate_hiffi_xp_scores(df)
|
2026-10-23 15:48:38 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
# Save finalized cohort
|
|
|
|
|
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
scored_df.to_csv(dest_file, index=False)
|
|
|
|
|
print(f"\n[Exporter] Final Audited Dataset Exported: {dest_file.resolve()}")
|
2026-10-23 15:48:38 +05:30
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
# Output Summary
|
|
|
|
|
sp_verified = scored_df["spotify_monthly_listeners"].notna().sum()
|
|
|
|
|
ig_verified = scored_df["instagram_verification_status"].isin(["MANUAL_VERIFIED", "AUTOMATED_VERIFIED"]).sum()
|
|
|
|
|
complete_3p = (
|
|
|
|
|
scored_df["production_hiffi_score"].notna()
|
|
|
|
|
& scored_df["spotify_monthly_listeners"].notna()
|
|
|
|
|
& scored_df["instagram_followers"].notna()
|
|
|
|
|
).sum()
|
2026-10-23 15:48:38 +05:30
|
|
|
|
2026-10-24 11:20:10 +05:30
|
|
|
print("\n" + "=" * 90)
|
2026-10-24 12:42:22 +05:30
|
|
|
print("AUDIT SUMMARY (ZERO-IMPUTATION & VERIFIED COVERAGE)")
|
|
|
|
|
print("=" * 90)
|
2026-10-24 15:57:20 +05:30
|
|
|
print(f"Total Cohort Size: {len(scored_df)}")
|
|
|
|
|
print(f"Verified Spotify Telemetry: {sp_verified} / {len(scored_df)}")
|
|
|
|
|
print(f"Verified Instagram Profiles: {ig_verified} / {len(scored_df)}")
|
|
|
|
|
print(f"Complete 3-Platform Coverage: {complete_3p} / {len(scored_df)}")
|
2026-10-24 11:20:10 +05:30
|
|
|
print("=" * 90)
|
2026-10-23 15:48:38 +05:30
|
|
|
|
|
|
|
|
|
2026-10-24 15:57:20 +05:30
|
|
|
def main():
|
|
|
|
|
run_multiplatform_pipeline()
|
|
|
|
|
|
|
|
|
|
|
2026-10-23 15:48:38 +05:30
|
|
|
if __name__ == "__main__":
|
2026-10-24 11:20:10 +05:30
|
|
|
main()
|