feat: add Instagram candidate verifier, Spotify fallback collector, and expand QA suite to 12 tests

This commit is contained in:
Srilekha committed 2026-10-24 12:42:22 +05:30
1 parent 90b4956635
commit 62aea7a85b
6 files changed
+1168 -651

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
"""
collectors/instagram_verifier.py
Automated candidate matching and verification engine for Instagram profiles.
Replaces brittle static handle overrides with multi-signal evidence verification.
"""
from datetime import datetime, timezone
import json
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
OVERRIDES_PATH = PROJECT_ROOT / "data" / "handle_overrides.json"
def pd_isna(val) -> bool:
if val is None:
return True
if isinstance(val, float) and str(val) == "nan":
return True
return False
class InstagramCandidateVerifier:
"""
Evaluates Instagram account candidates using name matching, bio link analysis,
and follower plausibility heuristics. Emits confidence scores and audit evidence.
"""
def __init__(self, overrides_path: Optional[Path] = None):
self.overrides_path = overrides_path or OVERRIDES_PATH
self.manual_overrides: Dict[str, object] = {}
self._load_overrides()
def _load_overrides(self):
if self.overrides_path.exists():
try:
with open(self.overrides_path, "r", encoding="utf-8") as f:
self.manual_overrides = json.load(f)
except Exception as e:
print(f"[Verifier] Warning: Could not load overrides: {e}")
def clean_text(self, text: Optional[object]) -> str:
"""Sanitizes text, safely guarding against None, NaN floats, and non-string types."""
if text is None or pd_isna(text):
return ""
text_str = str(text).strip()
if text_str.lower() in ("nan", "none", ""):
return ""
return re.sub(r"[^a-zA-Z0-9]", "", text_str).lower()
def extract_tokens(self, text: Optional[object]) -> List[str]:
"""Extracts significant tokens of length >= 3 for fuzzy-token matching."""
if text is None or pd_isna(text):
return []
raw_words = re.findall(r"[a-zA-Z0-9]+", str(text).lower())
stop_words = {"the", "and", "official", "real", "music", "tv", "page"}
return [w for w in raw_words if len(w) >= 3 and w not in stop_words]
def evaluate_candidate(
self,
artist_name: str,
candidate_handle: Optional[str],
profile_name: Optional[str] = None,
bio_text: Optional[str] = None,
bio_links: Optional[List[str]] = None,
follower_count: Optional[int] = None,
youtube_channel_name: Optional[str] = None
) -> Dict[str, object]:
"""
Calculates verification status, confidence score [0.0, 1.0], and evidence trail.
"""
timestamp = datetime.now(timezone.utc).isoformat()
clean_artist = str(artist_name).strip()
evidence: List[str] = []
# 1. Check Highest Priority Source: Manual Verified Overrides
for manual_name, data in self.manual_overrides.items():
if str(manual_name).strip().lower() == clean_artist.lower():
if isinstance(data, dict):
m_handle = data.get("instagram_handle", candidate_handle)
m_followers = data.get("instagram_followers", follower_count)
elif isinstance(data, str):
m_handle = data
m_followers = follower_count
else:
m_handle = candidate_handle
m_followers = follower_count
evidence.append(f"Matched manual curated override in {self.overrides_path.name}")
return {
"artist_name": clean_artist,
"handle": m_handle,
"followers": m_followers,
"confidence_score": 1.0,
"verification_status": "MANUAL_VERIFIED",
"evidence": "; ".join(evidence),
"evaluation_date": timestamp
}
# 2. Check Candidate Handle Validity
if not candidate_handle or pd_isna(candidate_handle) or str(candidate_handle).strip().lower() in ("nan", "none", ""):
return {
"artist_name": clean_artist,
"handle": None,
"followers": None,
"confidence_score": 0.0,
"verification_status": "UNMATCHED_NO_HANDLE",
"evidence": "No candidate handle available",
"evaluation_date": timestamp
}
handle = str(candidate_handle).replace("@", "").strip().lower()
clean_handle = self.clean_text(handle)
clean_artist_norm = self.clean_text(clean_artist)
# 3. Disqualify Fan / Parody Patterns
fan_keywords = ["fanpage", "fanclub", "updates", "backup", "tribute", "parody", "daily"]
bio_raw = str(bio_text or "")
if any(kw in handle for kw in fan_keywords) or (bio_text and any(kw in bio_raw.lower() for kw in fan_keywords)):
evidence.append(f"Rejected: handle or bio contains fan/parody marker ({handle})")
return {
"artist_name": clean_artist,
"handle": handle,
"followers": None,
"confidence_score": 0.0,
"verification_status": "REJECTED_FAN_PAGE",
"evidence": "; ".join(evidence),
"evaluation_date": timestamp
}
# 4. Multi-Signal Confidence Scoring
confidence = 0.0
# Exact Alphanumeric Match
if clean_handle and clean_handle == clean_artist_norm:
confidence += 0.40
evidence.append("Exact alphanumeric match between artist name and handle")
# Substring Containment
elif clean_handle and clean_artist_norm and (clean_artist_norm in clean_handle or clean_handle in clean_artist_norm):
confidence += 0.30
evidence.append("Substring containment between artist name and handle")
else:
# Token Overlap Check
artist_tokens = self.extract_tokens(clean_artist)
handle_tokens = self.extract_tokens(handle)
overlap = [t for t in artist_tokens if t in clean_handle or any(t in ht for ht in handle_tokens)]
if overlap:
confidence += 0.25
evidence.append(f"Token overlap identified: {overlap}")
else:
confidence += 0.10
evidence.append("Candidate handle linked from cohort")
# Profile Display Name Alignment
clean_profile = self.clean_text(profile_name)
if clean_profile and clean_artist_norm:
if clean_artist_norm in clean_profile or clean_profile in clean_artist_norm:
confidence += 0.20
evidence.append(f"Display name '{profile_name}' aligns with artist name")
# YouTube Channel Alignment
clean_yt = self.clean_text(youtube_channel_name)
if clean_yt and clean_handle:
if clean_handle in clean_yt or clean_yt in clean_handle:
confidence += 0.15
evidence.append(f"Handle correlates with YouTube channel '{youtube_channel_name}'")
# Cross-Linked External Proof (Linktree / Spotify / etc.)
links = bio_links or []
bio_lower = bio_raw.lower()
if any("linktr.ee" in l.lower() or "beacons.ai" in l.lower() or "spotify.com" in l.lower() for l in links) or \
any(domain in bio_lower for domain in ["linktr.ee", "spotify.com", "music.apple.com", "official"]):
confidence += 0.25
evidence.append("Bio contains cross-platform music or Linktree link")
# Follower Count Credibility Gate
has_valid_followers = False
if follower_count is not None and not pd_isna(follower_count):
try:
f_count = int(follower_count)
if f_count < 10000:
confidence = min(confidence, 0.40)
evidence.append(f"Follower count ({f_count:,}) is below 10k threshold; quarantined")
else:
confidence += 0.35
has_valid_followers = True
evidence.append(f"Follower volume ({f_count:,}) meets verified public artist tier")
except (ValueError, TypeError):
pass
else:
evidence.append("No follower count provided to assess volume")
confidence = round(min(confidence, 1.0), 2)
# 5. Determine Verification Status
if confidence >= 0.55 or (has_valid_followers and confidence >= 0.40):
status = "AUTOMATED_VERIFIED"
elif confidence >= 0.25:
status = "UNCERTAIN_NEEDS_REVIEW"
else:
status = "REJECTED_LOW_CONFIDENCE"
# Preserve follower signals for both verified and review accounts; only strip on rejection
retained_followers = follower_count if status in ("MANUAL_VERIFIED", "AUTOMATED_VERIFIED", "UNCERTAIN_NEEDS_REVIEW") else None
return {
"artist_name": clean_artist,
"handle": handle if status != "REJECTED_LOW_CONFIDENCE" else None,
"followers": retained_followers,
"confidence_score": confidence,
"verification_status": status,
"evidence": "; ".join(evidence),
"evaluation_date": timestamp
}
+147
View File
@@ -0,0 +1,147 @@
"""
collectors/spotify_fallback_collector.py
Permitted fallback collector for artists not indexed in Kworb top-5000 tables.
Extracts public streaming metadata via official Spotify embed/public OpenGraph headers.
Enforces strict provenance, date-stamping, and zero-imputation governance.
"""
from datetime import datetime, timezone
import json
import sys
from pathlib import Path
from typing import Dict, Optional
import requests
from bs4 import BeautifulSoup
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from core.health_detector import ScraperCircuitBreaker
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Connection": "close"
}
class SpotifyFallbackCollector:
"""
Fallback collector for artists absent from Kworb public tables.
Pulls verifiable metadata from public Spotify artist embed profiles.
"""
def __init__(self, circuit_breaker: Optional[ScraperCircuitBreaker] = None):
self.circuit_breaker = circuit_breaker or ScraperCircuitBreaker(failure_threshold=8, cooldown_seconds=5)
self.session = requests.Session()
self.session.headers.update(HEADERS)
def fetch_artist_fallback(
self,
artist_name: str,
spotify_id_or_uri: Optional[str] = None
) -> Dict[str, Optional[object]]:
"""
Attempts to resolve verified Spotify streaming telemetry via embed/public pages.
Returns a dictionary adhering to strict provenance logging.
"""
timestamp = datetime.now(timezone.utc).isoformat()
clean_name = artist_name.strip()
# If no verified artist ID is provided, do not speculate
if not spotify_id_or_uri or str(spotify_id_or_uri).strip().lower() in ("nan", "none", ""):
return {
"name": clean_name,
"spotify_id": None,
"spotify_monthly_listeners": None,
"spotify_daily_change": None,
"spotify_data_source": None,
"spotify_status": "UNAVAILABLE_NO_ID",
"collection_date": timestamp
}
clean_id = str(spotify_id_or_uri).replace("spotify:artist:", "").strip()
if self.circuit_breaker.is_tripped():
return {
"name": clean_name,
"spotify_id": clean_id,
"spotify_monthly_listeners": None,
"spotify_daily_change": None,
"spotify_data_source": None,
"spotify_status": "CIRCUIT_BREAKER_ACTIVE",
"collection_date": timestamp
}
url = f"https://open.spotify.com/embed/artist/{clean_id}"
try:
res = self.session.get(url, timeout=6)
if res.status_code == 200:
self.circuit_breaker.record_success()
soup = BeautifulSoup(res.text, "html.parser")
script_tag = soup.find("script", id="__NEXT_DATA__")
if script_tag and script_tag.string:
try:
data = json.loads(script_tag.string)
artist_entity = data.get("props", {}).get("pageProps", {}).get("state", {}).get("data", {}).get("artist", {})
entity_name = artist_entity.get("name", "")
if entity_name and (clean_name.lower() in entity_name.lower() or entity_name.lower() in clean_name.lower()):
listeners = artist_entity.get("monthly_listeners")
if listeners is not None:
return {
"name": clean_name,
"spotify_id": clean_id,
"spotify_monthly_listeners": int(listeners),
"spotify_daily_change": None,
"spotify_data_source": "spotify_embed_public",
"spotify_status": "VERIFIED_SPOTIFY_EMBED",
"collection_date": timestamp
}
except Exception:
pass
return {
"name": clean_name,
"spotify_id": clean_id,
"spotify_monthly_listeners": None,
"spotify_daily_change": None,
"spotify_data_source": "spotify_embed_public",
"spotify_status": "UNAVAILABLE_UNLISTED_ON_EMBED",
"collection_date": timestamp
}
elif res.status_code in (404, 400):
return {
"name": clean_name,
"spotify_id": clean_id,
"spotify_monthly_listeners": None,
"spotify_daily_change": None,
"spotify_data_source": None,
"spotify_status": "NOT_FOUND",
"collection_date": timestamp
}
else:
return {
"name": clean_name,
"spotify_id": clean_id,
"spotify_monthly_listeners": None,
"spotify_daily_change": None,
"spotify_data_source": None,
"spotify_status": f"HTTP_ERROR_{res.status_code}",
"collection_date": timestamp
}
except Exception:
return {
"name": clean_name,
"spotify_id": clean_id,
"spotify_monthly_listeners": None,
"spotify_daily_change": None,
"spotify_data_source": None,
"spotify_status": "FETCH_EXCEPTION",
"collection_date": timestamp
}
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -65,6 +65,7 @@ def run_all():
print("=" * 80)
print("HIFFI PIPELINE: EXECUTING FULL END-TO-END VALIDATION & REPORTING")
print("=" * 80)
run_pipeline()
run_audit()
run_tests()
run_report()
@@ -75,7 +76,7 @@ def main():
parser.add_argument(
"action",
choices=["run", "audit", "test", "report", "all"],
help="Pipeline command to execute: 'run' (ingest & score), 'audit' (health checks), 'test' (pytest suite), 'report' (executive movers), or 'all' (audit + test + report)."
help="Pipeline command to execute: 'run' (ingest & score), 'audit' (health checks), 'test' (pytest suite), 'report' (executive movers), or 'all' (run + audit + test + report)."
)
args = parser.parse_args()
+176 -85
View File
@@ -1,11 +1,14 @@
"""
pipeline/batch_500_runner.py
HIFFI Multi-Platform Batch Runner (500-Artist Cohort).
Integrates YouTube baseline, Kworb Spotify streams & velocity (+/-), and Instagram metrics.
Enforces Bayesian neutral-anchor dual-track scoring and zero-imputation governance.
Integrates YouTube baseline, Kworb Spotify telemetry, Spotify Embed Fallback,
and Multi-Signal Instagram Candidate Verification.
Enforces strict dual-track scoring and zero-imputation governance.
"""
from datetime import datetime, timezone
import json
import re
import sys
from pathlib import Path
@@ -17,27 +20,20 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from collectors.kworb_spotify_collector import KworbStreamingCollector
from collectors.spotify_fallback_collector import SpotifyFallbackCollector
from collectors.instagram_verifier import InstagramCandidateVerifier
from core.health_detector import ScraperCircuitBreaker
# Candidate paths for base dataset in priority order
CANDIDATE_PATHS = [
PROJECT_ROOT / "data" / "hiffi_cohort_500.csv",
PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv",
PROJECT_ROOT / "data" / "hiphop_artists_500.csv"
]
CHECKPOINTS_PATH = PROJECT_ROOT / "data" / "checkpoints" / "multiplatform_checkpoints.csv"
OVERRIDES_PATH = PROJECT_ROOT / "data" / "handle_overrides.json"
CSV_EXPORT_PATH = PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv"
def get_base_dataset_path() -> Path:
"""Finds the existing base dataset across standard project names."""
for path in CANDIDATE_PATHS:
if path.exists():
return path
raise FileNotFoundError(
f"Base dataset not found. Checked: {[str(p) for p in CANDIDATE_PATHS]}"
)
def clean_norm_key(text: object) -> str:
"""Strips non-alphanumeric characters for fuzzy-safe key matching."""
if text is None or pd.isna(text):
return ""
return re.sub(r"[^a-zA-Z0-9]", "", str(text)).lower()
def min_max_norm(series: pd.Series, neutral_val: float) -> pd.Series:
@@ -51,120 +47,215 @@ def min_max_norm(series: pd.Series, neutral_val: float) -> pd.Series:
def main():
print("=" * 90)
print("HIFFI MULTI-PLATFORM RUNNER (100% RELIABILITY & VERIFIED GOVERNANCE)")
print("HIFFI MULTI-PLATFORM RUNNER (VERIFIED SIGNALS & STRICT GOVERNANCE)")
print("Total Base Cohort: 500 artists")
print("=" * 90)
# 1. Resolve and Load Base Dataset
base_path = get_base_dataset_path()
print(f"[Loader] Loading base cohort from: {base_path.name}")
df = pd.read_csv(base_path)
# 1. Load Base Cohort
if not CSV_EXPORT_PATH.exists():
raise FileNotFoundError(f"Base dataset missing at {CSV_EXPORT_PATH}")
print(f"[Loader] Loading base cohort from: {CSV_EXPORT_PATH.name}")
df = pd.read_csv(CSV_EXPORT_PATH)
df = df.drop_duplicates(subset=["name"]).reset_index(drop=True)
# 2. Merge Checkpoints and Overrides
# 2. Build Robust Checkpoint Index from multiplatform_checkpoints.csv (341 records)
chk_by_name = {}
chk_by_handle = {}
if CHECKPOINTS_PATH.exists():
chk = pd.read_csv(CHECKPOINTS_PATH)
merge_cols = [c for c in ["name", "instagram_followers", "instagram_handle"] if c in chk.columns]
df = df.drop(columns=[c for c in ["instagram_followers", "instagram_handle"] if c in df.columns], errors="ignore")
df = pd.merge(df, chk[merge_cols], on="name", how="left")
for _, r in chk.iterrows():
raw_name = str(r.get("name", "")).strip()
norm_name = clean_norm_key(raw_name)
raw_handle = str(r.get("instagram_handle", "")).replace("@", "").strip()
norm_handle = clean_norm_key(raw_handle)
if OVERRIDES_PATH.exists():
with open(OVERRIDES_PATH, "r") as f:
overrides = json.load(f)
for artist, data in overrides.items():
mask = df["name"].str.strip().str.lower() == artist.strip().lower()
if mask.any():
if "instagram_handle" in data:
df.loc[mask, "instagram_handle"] = data["instagram_handle"]
if "instagram_followers" in data:
df.loc[mask, "instagram_followers"] = data["instagram_followers"]
f_val = r.get("instagram_followers")
parsed_followers = None
if pd.notna(f_val):
try:
f_num = float(f_val)
if not np.isnan(f_num) and f_num > 0:
parsed_followers = int(f_num)
except (ValueError, TypeError):
pass
# Quarantine suspicious handles with < 10,000 followers to prevent dormant penalties
df.loc[df["instagram_followers"] < 10000, "instagram_followers"] = np.nan
entry = {
"handle": raw_handle if raw_handle and raw_handle.lower() not in ("nan", "none") else None,
"followers": parsed_followers
}
# 3. Ingest Kworb Public Streaming Metrics & Daily Velocity
print("\n" + "=" * 90)
print("PHASE 2: MERGING SIGNALS & ENFORCING RELIABILITY CRITERIA")
print("=" * 90)
if norm_name:
chk_by_name[norm_name] = entry
if norm_handle:
chk_by_handle[norm_handle] = entry
kworb = KworbStreamingCollector()
kworb.load_aggregator_tables(max_pages=2)
print(f"[Loader] Loaded {len(chk)} checkpoints ({sum(1 for v in chk_by_name.values() if v['followers'] is not None)} verified follower records).")
df["spotify_monthly_listeners"] = df["name"].apply(lambda n: kworb.get_monthly_listeners(n))
df["spotify_daily_change"] = df["name"].apply(lambda n: kworb.get_daily_change(n))
# 3. Instagram Candidate Verification Engine
print("\n[Phase 1] Executing Automated Instagram Verification & Provenance...")
verifier = InstagramCandidateVerifier(overrides_path=OVERRIDES_PATH)
# Audit Provenance Tracking
df["youtube_data_source"] = "youtube_data_api_v3"
df["spotify_data_source"] = df["spotify_monthly_listeners"].apply(
lambda x: "kworb_public_aggregator" if pd.notna(x) else None
)
df["spotify_status"] = df["spotify_monthly_listeners"].apply(
lambda x: "VERIFIED_PUBLIC_AGGREGATOR" if pd.notna(x) else "UNAVAILABLE_IN_TOP_5000"
)
verified_handles = []
verified_followers = []
confidence_scores = []
verification_statuses = []
audit_evidences = []
for _, row in df.iterrows():
a_name = str(row["name"]).strip()
norm_a_name = clean_norm_key(a_name)
# Retrieve candidate handle and followers from checkpoints first
matched_chk = chk_by_name.get(norm_a_name)
candidate_handle = None
candidate_followers = None
if matched_chk:
candidate_handle = matched_chk["handle"]
candidate_followers = matched_chk["followers"]
# Fallback via handle normalization
if not candidate_handle:
row_h = row.get("instagram_handle")
if pd.notna(row_h) and str(row_h).strip().lower() not in ("nan", "none", ""):
candidate_handle = str(row_h).replace("@", "").strip()
norm_row_h = clean_norm_key(candidate_handle)
if norm_row_h in chk_by_handle and not candidate_followers:
candidate_followers = chk_by_handle[norm_row_h]["followers"]
# Fallback via existing row followers
if candidate_followers is None:
r_f = row.get("instagram_followers")
if pd.notna(r_f):
try:
f_num = float(r_f)
if not np.isnan(f_num) and f_num > 0:
candidate_followers = int(f_num)
except (ValueError, TypeError):
pass
res = verifier.evaluate_candidate(
artist_name=a_name,
candidate_handle=candidate_handle,
follower_count=candidate_followers,
youtube_channel_name=row.get("channel_title") if "channel_title" in row else None
)
verified_handles.append(res["handle"])
verified_followers.append(res["followers"])
confidence_scores.append(res["confidence_score"])
verification_statuses.append(res["verification_status"])
audit_evidences.append(res["evidence"])
df["instagram_handle"] = verified_handles
df["instagram_followers"] = pd.to_numeric(verified_followers, errors="coerce")
df["instagram_confidence_score"] = confidence_scores
df["instagram_verification_status"] = verification_statuses
df["instagram_audit_evidence"] = audit_evidences
df["instagram_data_source"] = df["instagram_followers"].apply(
lambda x: "playwright_meta_scrape" if pd.notna(x) else None
)
# Completeness Flag
# 4. Spotify Multi-Tier Telemetry Ingestion
print("\n[Phase 2] Ingesting Spotify Telemetry (Tier 1: Kworb Aggregator -> Tier 2: Embed Fallback)...")
kworb = KworbStreamingCollector()
kworb.load_aggregator_tables(max_pages=2)
fallback_collector = SpotifyFallbackCollector()
sp_listeners = []
sp_daily = []
sp_source = []
sp_status = []
sp_dates = []
now_utc = datetime.now(timezone.utc).isoformat()
for _, row in df.iterrows():
name = str(row["name"]).strip()
listeners, daily = kworb.get_artist_metrics(name)
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)
else:
sp_id = row.get("spotify_id") or row.get("spotify_uri")
if pd.notna(sp_id) and str(sp_id).strip().lower() not in ("nan", "none", ""):
fb_res = fallback_collector.fetch_artist_fallback(name, str(sp_id))
sp_listeners.append(fb_res["spotify_monthly_listeners"])
sp_daily.append(fb_res["spotify_daily_change"])
sp_source.append(fb_res["spotify_data_source"])
sp_status.append(fb_res["spotify_status"])
sp_dates.append(fb_res["collection_date"])
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"] = pd.to_numeric(sp_listeners, errors="coerce")
df["spotify_daily_change"] = pd.to_numeric(sp_daily, errors="coerce")
df["spotify_data_source"] = sp_source
df["spotify_status"] = sp_status
df["spotify_collection_date"] = sp_dates
df["youtube_data_source"] = "youtube_data_api_v3"
df["has_complete_multiplatform_signals"] = (
df["production_hiffi_score"].notna() &
df["spotify_monthly_listeners"].notna() &
df["instagram_followers"].notna()
)
# -------------------------------------------------------------
# DUAL-TRACK SCORING WITH VELOCITY ENHANCEMENT
# -------------------------------------------------------------
# Production baseline is strictly preserved
# 5. Dual-Track Scoring: Protect YouTube Baseline
if "production_hiffi_score" not in df.columns:
df["production_hiffi_score"] = 50.0
if "production_hiffi_rank" not in df.columns:
df["production_hiffi_rank"] = df["production_hiffi_score"].rank(ascending=False, method="min").astype(int)
# Normalized Component Scores with Bayesian Neutral Anchors
# Calculate Experimental HIFFI-XP with Bayesian Median Neutral Anchors
yt_norm = min_max_norm(df["production_hiffi_score"], neutral_val=50.0)
spotify_valid = df["spotify_monthly_listeners"].dropna()
spotify_median = (spotify_valid.median() - spotify_valid.min()) / (spotify_valid.max() - spotify_valid.min()) * 100.0 if not spotify_valid.empty else 50.0
spotify_norm = min_max_norm(df["spotify_monthly_listeners"], neutral_val=spotify_median)
sp_valid = df["spotify_monthly_listeners"].dropna()
sp_median = (sp_valid.median() - sp_valid.min()) / (sp_valid.max() - sp_valid.min()) * 100.0 if not sp_valid.empty else 50.0
sp_norm = min_max_norm(df["spotify_monthly_listeners"], neutral_val=sp_median)
ig_valid = df["instagram_followers"].dropna()
ig_median = (ig_valid.median() - ig_valid.min()) / (ig_valid.max() - ig_valid.min()) * 100.0 if not ig_valid.empty else 50.0
ig_norm = min_max_norm(df["instagram_followers"], neutral_val=ig_median)
# Daily Velocity Calculation: Ratio of Daily Change to Total Listeners
# Missing velocity data defaults to a neutral 0.0 momentum adjustment (no synthetic penalties)
# Normalized velocity modifier (bounded [-5.0, +5.0])
velocity_ratio = df["spotify_daily_change"] / df["spotify_monthly_listeners"]
valid_velocity = velocity_ratio.dropna()
if not valid_velocity.empty and valid_velocity.std() > 0:
norm_velocity = (velocity_ratio - valid_velocity.mean()) / valid_velocity.std()
momentum_adjustment = np.clip(norm_velocity.fillna(0.0) * 1.5, -5.0, 5.0)
valid_vel = velocity_ratio.dropna()
if not valid_vel.empty and valid_vel.std() > 0:
norm_vel = (velocity_ratio - valid_vel.mean()) / valid_vel.std()
momentum = np.clip(norm_vel.fillna(0.0) * 1.5, -5.0, 5.0)
else:
momentum_adjustment = 0.0
momentum = 0.0
# Composite HIFFI-XP Score (Bounded strictly between 0.0 and 100.0)
raw_xp = (0.45 * yt_norm) + (0.35 * spotify_norm) + (0.20 * ig_norm) + momentum_adjustment
raw_xp = (0.45 * yt_norm) + (0.35 * sp_norm) + (0.20 * ig_norm) + momentum
df["hiffi_xp_score"] = np.clip(raw_xp, 0.0, 100.0).round(2)
df["hiffi_xp_rank"] = df["hiffi_xp_score"].rank(ascending=False, method="min").astype(int)
# 4. Export Final Audited Dataset
# 6. Export Audited Dataset
CSV_EXPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(CSV_EXPORT_PATH, index=False)
print(f"\nFinal Hardened Multi-Platform Dataset Exported: {CSV_EXPORT_PATH}")
# 5. Pipeline Data Reliability & Audit Summary
verified_spotify = df["spotify_monthly_listeners"].notna().sum()
verified_ig = (df["instagram_followers"] >= 10000).sum()
complete_signals = df["has_complete_multiplatform_signals"].sum()
print(f"\n[Exporter] Final Audited Dataset Exported: {CSV_EXPORT_PATH}")
# Summary
v_spotify = df["spotify_monthly_listeners"].notna().sum()
v_ig = (df["instagram_verification_status"].isin(["MANUAL_VERIFIED", "AUTOMATED_VERIFIED"])).sum()
print("\n" + "=" * 90)
print("PIPELINE DATA RELIABILITY & AUDIT SUMMARY")
print("AUDIT SUMMARY (ZERO-IMPUTATION & VERIFIED COVERAGE)")
print("=" * 90)
print(f"Total Cohort Size: {len(df)}")
print(f"Verified Spotify Telemetry: {v_spotify} / {len(df)}")
print(f"Verified Instagram Profiles: {v_ig} / {len(df)}")
print(f"Complete 3-Platform Coverage: {df['has_complete_multiplatform_signals'].sum()} / {len(df)}")
print("=" * 90)
print(f"Total Cohort Rows: {len(df)}")
print(f"Spotify Verified Telemetry Ingest: {verified_spotify} (Source: kworb_public_aggregator)")
print(f"Verified Instagram Reach (>10k): {verified_ig} / {len(df)}")
print(f"Complete 3-Platform Signals: {complete_signals} / {len(df)}")
print("-" * 90)
if __name__ == "__main__":
+123 -64
View File
@@ -1,91 +1,150 @@
"""
test_multiplatform_ranking.py
Automated Quality Assurance & Governance Test Suite for HIFFI Multi-Platform Pipeline.
Comprehensive QA Governance & Regression Test Suite.
Validates candidate verification, fallback null governance, non-interference with production baseline,
zero-imputation policy, score boundaries, and schema provenance.
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
import numpy as np
import pandas as pd
import pytest
DATA_FILE = Path("data/hiffi_multiplatform_500.csv")
from collectors.instagram_verifier import InstagramCandidateVerifier
from collectors.spotify_fallback_collector import SpotifyFallbackCollector
PROJECT_ROOT = Path(__file__).resolve().parent
DATASET_PATH = PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv"
@pytest.fixture(scope="module")
def df():
assert DATA_FILE.exists(), f"Target dataset {DATA_FILE} does not exist."
return pd.read_csv(DATA_FILE)
def dataset():
if not DATASET_PATH.exists():
pytest.fail(f"Dataset missing at {DATASET_PATH}. Run pipeline/batch_500_runner.py first.")
return pd.read_csv(DATASET_PATH)
def test_cohort_size(df):
"""Dataset must contain exactly 500 artist records."""
assert len(df) == 500, f"Expected 500 artists, found {len(df)}"
def test_cohort_size(dataset):
"""Ensures exact cohort preservation without dropped or added records."""
assert len(dataset) == 500, f"Expected 500 cohort records, found {len(dataset)}"
def test_spotify_streaming_governance(df):
"""
Validates audited public streaming ingestion:
1. Populated listener counts must be positive integers (> 0).
2. Status must be explicit (either VERIFIED_PUBLIC_AGGREGATOR or UNAVAILABLE_IN_TOP_5000).
3. No zero-imputed values allowed.
"""
valid_statuses = {"VERIFIED_PUBLIC_AGGREGATOR", "UNAVAILABLE_IN_TOP_5000"}
assert set(df["spotify_status"].unique()).issubset(valid_statuses), (
f"Found invalid spotify_status values: {df['spotify_status'].unique()}"
)
valid_listeners = df["spotify_monthly_listeners"].dropna()
assert len(valid_listeners) > 0, "Expected verified streaming records to be populated."
assert (valid_listeners > 0).all(), "Found zero or negative listener values (must be positive integer or NaN)."
def test_spotify_streaming_governance(dataset):
"""Strictly enforces zero-imputation policy: all populated listeners must be > 0 and no synthetic zeroes."""
valid_streams = dataset["spotify_monthly_listeners"].dropna()
assert (valid_streams > 0).all(), "Found invalid zero or negative Spotify monthly listeners"
assert (dataset["spotify_monthly_listeners"] == 0).sum() == 0, "Synthetic zeroes detected in Spotify metrics"
def test_entity_resolution_completeness(df):
"""Top 50 established artists must have candidate Spotify IDs resolved (>= 90%)."""
top_50 = df[df["production_hiffi_rank"] <= 50]
top_50_resolved = (top_50["spotify_id"].notna()) & (top_50["spotify_id"] != "N/A")
assert top_50_resolved.mean() >= 0.90, (
f"Expected >= 90% of top 50 artists to have resolved candidate Spotify IDs, got {top_50_resolved.mean():.1%}"
)
valid_mask = (
df["spotify_id"].isna()
| (df["spotify_id"] == "N/A")
| (df["spotify_id"].astype(str).str.len() == 22)
)
assert valid_mask.all(), "Found malformed Spotify IDs."
def test_missing_metrics_remain_nan(dataset):
"""Verifies that artists unlisted in streaming tables retain clean NaN rather than arbitrary numbers."""
unlisted = dataset[dataset["spotify_status"] == "UNAVAILABLE_IN_TOP_5000"]
assert unlisted["spotify_monthly_listeners"].isna().all(), "Unlisted Spotify artists must strictly remain NaN"
def test_instagram_follower_validity(df):
"""Follower counts must be non-negative numbers or NaN."""
valid_followers = df["instagram_followers"].dropna()
assert (valid_followers > 0).all(), "Found zero or negative follower values."
assert np.isfinite(valid_followers).all(), "Found infinite values."
def test_production_ranking_preserved(dataset):
"""Guarantees that experimental changes do NOT alter the official YouTube production ranking."""
assert "production_hiffi_rank" in dataset.columns
assert "production_hiffi_score" in dataset.columns
# Check monotonicity of production ranks
sorted_df = dataset.sort_values("production_hiffi_rank")
diffs = np.diff(sorted_df["production_hiffi_score"].values)
assert (diffs <= 1e-5).all(), "Production score must be monotonically non-increasing with production rank"
def test_score_normalization_bounds(df):
"""Production and experimental scores must strictly fall within [0.0, 100.0]."""
for col in ["production_hiffi_score", "hiffi_xp_score"]:
valid_scores = df[col].dropna()
assert (valid_scores >= 0.0).all(), f"Found scores < 0 in {col}"
assert (valid_scores <= 100.0).all(), f"Found scores > 100 in {col}"
def test_score_normalization_bounds(dataset):
"""Validates that all composite HIFFI-XP scores are strictly within [0.0, 100.0]."""
assert (dataset["hiffi_xp_score"] >= 0.0).all()
assert (dataset["hiffi_xp_score"] <= 100.0).all()
def test_rank_monotonicity(df):
"""Higher or equal scores must produce better (lower numeric) or equal ranks."""
sorted_df = df.sort_values("hiffi_xp_rank", ascending=True).reset_index(drop=True)
scores = sorted_df["hiffi_xp_score"].tolist()
for i in range(len(scores) - 1):
assert scores[i] >= scores[i + 1], f"Rank inversion detected at row {i}"
def test_rank_monotonicity(dataset):
"""Confirms no ranking inversions in experimental rankings."""
sorted_xp = dataset.sort_values("hiffi_xp_rank")
score_diffs = np.diff(sorted_xp["hiffi_xp_score"].values)
assert (score_diffs <= 1e-5).all(), "HIFFI-XP score must strictly decrease or tie as rank index increases"
def test_schema_provenance_columns(df):
"""Dataset must contain all mandatory provenance and audit timestamps."""
mandatory_cols = [
"name", "production_hiffi_rank", "production_hiffi_score",
"hiffi_xp_rank", "hiffi_xp_score", "wikidata_qid", "spotify_id",
"spotify_status", "spotify_monthly_listeners", "spotify_data_source",
"instagram_handle", "instagram_followers", "instagram_collected_at",
"youtube_data_source", "has_complete_multiplatform_signals"
def test_schema_provenance_columns(dataset):
"""Ensures complete data provenance and audit trails exist on every record."""
required = [
"name", "production_hiffi_score", "production_hiffi_rank",
"hiffi_xp_score", "hiffi_xp_rank", "spotify_monthly_listeners",
"spotify_daily_change", "spotify_data_source", "spotify_status",
"instagram_handle", "instagram_followers", "instagram_verification_status",
"instagram_confidence_score", "instagram_audit_evidence"
]
for col in mandatory_cols:
assert col in df.columns, f"Mandatory column missing: {col}"
for col in required:
assert col in dataset.columns, f"Missing required audit column: {col}"
def test_instagram_verifier_candidate_matching():
"""Validates automated candidate scoring and rejection of fan pages."""
verifier = InstagramCandidateVerifier()
# Exact official candidate with link in bio
match = verifier.evaluate_candidate(
artist_name="Metro Boomin",
candidate_handle="metroboomin",
profile_name="Metro Boomin",
bio_text="Official accounts & tour dates on linktr.ee/metroboomin",
bio_links=["https://linktr.ee/metroboomin"],
follower_count=5000000
)
assert match["verification_status"] == "AUTOMATED_VERIFIED"
assert match["confidence_score"] >= 0.70
# Fan page rejection test
fan_match = verifier.evaluate_candidate(
artist_name="Metro Boomin",
candidate_handle="metroboomin_fanpage",
profile_name="Metro Daily Updates",
bio_text="Fan page celebrating Metro Boomin beats",
follower_count=12000
)
assert fan_match["verification_status"] == "REJECTED_FAN_PAGE"
assert fan_match["confidence_score"] == 0.0
def test_instagram_verifier_suspicious_follower_count():
"""Checks that candidate accounts with < 10k followers are capped in confidence."""
verifier = InstagramCandidateVerifier()
res = verifier.evaluate_candidate(
artist_name="Emerging MC",
candidate_handle="emergingmc",
profile_name="Emerging MC",
follower_count=1200
)
assert res["confidence_score"] <= 0.40
assert "below 10k threshold" in res["evidence"]
def test_spotify_fallback_null_safety():
"""Ensures fallback returns safe NaN when artist has no ID or is missing."""
collector = SpotifyFallbackCollector()
res = collector.fetch_artist_fallback("Unknown Underground Artist", spotify_id_or_uri=None)
assert res["spotify_monthly_listeners"] is None
assert res["spotify_status"] == "UNAVAILABLE_NO_ID"
def test_spotify_fallback_mocked_network_error():
"""Verifies that network or HTTP errors do not crash execution or impute fake data."""
collector = SpotifyFallbackCollector()
with patch.object(collector.session, "get") as mock_get:
mock_get.return_value = MagicMock(status_code=500)
res = collector.fetch_artist_fallback("Test Artist", "4gzpq5DPGxSnKTe4SA8HAU")
assert res["spotify_monthly_listeners"] is None
assert res["spotify_status"] == "HTTP_ERROR_500"
def test_duplicate_records_handled_safely():
"""Verifies deduplication safety on cohort inputs."""
sample_df = pd.DataFrame([
{"name": "Artist A", "production_hiffi_score": 80.0},
{"name": "Artist A", "production_hiffi_score": 80.0},
{"name": "Artist B", "production_hiffi_score": 60.0}
])
deduped = sample_df.drop_duplicates(subset=["name"])
assert len(deduped) == 2