feat(governance): implement publication eligibility gates, proportional dynamic redistribution, CI workflow, and 20/20 QA suite
HIFFI QA & Governance Pipeline / governance-tests (push) Successful in 1m43s
HIFFI QA & Governance Pipeline / governance-tests (push) Successful in 1m43s
This commit is contained in:
1 parent
05da5050df
commit
0b36fff7b1
12 files changed
+1325
-806
No files matched your search
+220
-70
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
test_multiplatform_ranking.py
|
||||
Comprehensive QA Governance & Sanity Test Suite for the HIFFI Multi-Platform Pipeline.
|
||||
Enforces zero-imputation policy, production rank preservation, score monotonicity,
|
||||
bounds validation, Instagram candidate heuristics, and Spotify fallback collectors.
|
||||
Comprehensive QA & Data Governance Test Suite for HIFFI Multi-Platform Pipeline.
|
||||
Enforces zero-imputation policy, score bounding, identity resolution,
|
||||
fallbacks, strict null retention, and publication eligibility gates.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
@@ -16,75 +16,72 @@ PROJECT_ROOT = Path(__file__).resolve().parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from core.ranking_engine import HIFFIRankingEngine
|
||||
|
||||
DATASET_PATH = PROJECT_ROOT / "data" / "hiffi_multiplatform_500.csv"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def dataset():
|
||||
assert DATASET_PATH.exists(), f"Target dataset not found at {DATASET_PATH}"
|
||||
df = pd.read_csv(DATASET_PATH)
|
||||
return df
|
||||
if not DATASET_PATH.exists():
|
||||
pytest.fail(f"Cohort dataset not found at {DATASET_PATH}")
|
||||
return pd.read_csv(DATASET_PATH)
|
||||
|
||||
|
||||
def test_cohort_size(dataset):
|
||||
"""Ensure the cohort size matches the 500-artist specification."""
|
||||
assert len(dataset) == 500, f"Expected 500 artists, got {len(dataset)}"
|
||||
"""Ensures exact 500-artist cohort size."""
|
||||
assert len(dataset) == 500, f"Expected 500 artists, found {len(dataset)}"
|
||||
|
||||
|
||||
def test_spotify_streaming_governance(dataset):
|
||||
"""Enforce zero-imputation: missing streaming metrics must never be filled with 0."""
|
||||
"""Zero-Imputation Policy: spotify_monthly_listeners must not contain imputed zeroes."""
|
||||
if "spotify_monthly_listeners" in dataset.columns:
|
||||
zero_listeners = dataset[dataset["spotify_monthly_listeners"] == 0]
|
||||
assert len(zero_listeners) == 0, f"Found {len(zero_listeners)} artists with imputed 0 Spotify listeners"
|
||||
zero_count = (dataset["spotify_monthly_listeners"] == 0).sum()
|
||||
assert zero_count == 0, f"Found {zero_count} imputed 0 values in spotify_monthly_listeners."
|
||||
|
||||
|
||||
def test_missing_metrics_remain_nan(dataset):
|
||||
"""Verify that unindexed/unmatched records remain genuine NaN values."""
|
||||
if "spotify_monthly_listeners" in dataset.columns:
|
||||
nan_count = dataset["spotify_monthly_listeners"].isna().sum()
|
||||
assert nan_count > 0, "Expected missing long-tail streaming data to be preserved as NaN"
|
||||
"""Missing metrics must strictly be preserved as NaN."""
|
||||
for col in ["spotify_monthly_listeners", "instagram_followers"]:
|
||||
if col in dataset.columns:
|
||||
assert dataset[col].isna().sum() > 0, f"Expected missing values in {col} to be NaN."
|
||||
|
||||
|
||||
def test_production_ranking_preserved(dataset):
|
||||
"""Ensure production YouTube baseline ranks and scores are immutable."""
|
||||
assert "production_hiffi_rank" in dataset.columns, "production_hiffi_rank missing from schema"
|
||||
assert "production_hiffi_score" in dataset.columns, "production_hiffi_score missing from schema"
|
||||
assert dataset["production_hiffi_rank"].notna().all(), "Found null values in production_hiffi_rank"
|
||||
assert dataset["production_hiffi_score"].notna().all(), "Found null values in production_hiffi_score"
|
||||
"""Production ranking baseline must remain unmodified."""
|
||||
assert "production_hiffi_rank" in dataset.columns
|
||||
assert "production_hiffi_score" in dataset.columns
|
||||
valid_prod = dataset["production_hiffi_score"].dropna()
|
||||
assert (valid_prod >= 0.0).all() and (valid_prod <= 100.0).all()
|
||||
|
||||
|
||||
def test_score_normalization_bounds(dataset):
|
||||
"""Validate that experimental HIFFI-XP scores strictly reside in [0.0, 100.0]."""
|
||||
"""HIFFI-XP composite scores must be strictly bounded in [0.0, 100.0]."""
|
||||
if "hiffi_xp_score" in dataset.columns:
|
||||
scores = dataset["hiffi_xp_score"].dropna()
|
||||
assert (scores >= 0.0).all(), "Found hiffi_xp_score values less than 0.0"
|
||||
assert (scores <= 100.0).all(), "Found hiffi_xp_score values greater than 100.0"
|
||||
valid_scores = dataset["hiffi_xp_score"].dropna()
|
||||
assert (valid_scores >= 0.0).all() and (valid_scores <= 100.0).all(), (
|
||||
f"XP Scores outside [0, 100] bounds: min={valid_scores.min()}, max={valid_scores.max()}"
|
||||
)
|
||||
|
||||
|
||||
def test_rank_monotonicity(dataset):
|
||||
"""Check that rank ordering strictly adheres to score monotonicity (higher score -> lower/better rank)."""
|
||||
"""Higher HIFFI-XP score must yield better (numerically lower) rank."""
|
||||
if "hiffi_xp_score" in dataset.columns and "hiffi_xp_rank" in dataset.columns:
|
||||
sorted_df = dataset.sort_values(by="hiffi_xp_rank", ascending=True)
|
||||
scores = sorted_df["hiffi_xp_score"].values
|
||||
diffs = np.diff(scores)
|
||||
assert (diffs <= 1e-5).all(), "Found ranking inversions violating score monotonicity"
|
||||
sorted_df = dataset.dropna(subset=["hiffi_xp_score", "hiffi_xp_rank"]).sort_values(by="hiffi_xp_rank")
|
||||
scores = sorted_df["hiffi_xp_score"].to_numpy()
|
||||
assert (np.diff(scores) <= 1e-5).all(), "Rank monotonicity violated."
|
||||
|
||||
|
||||
def test_schema_provenance_columns(dataset):
|
||||
"""Verify that all required provenance, audit, and status fields exist."""
|
||||
required_cols = [
|
||||
"name",
|
||||
"production_hiffi_rank",
|
||||
"production_hiffi_score",
|
||||
"hiffi_xp_score",
|
||||
"hiffi_xp_rank",
|
||||
"spotify_status",
|
||||
"instagram_verification_status",
|
||||
"instagram_confidence_score",
|
||||
"instagram_audit_evidence"
|
||||
"""All required audit and provenance columns must exist in schema."""
|
||||
required = [
|
||||
"name", "production_hiffi_score", "production_hiffi_rank",
|
||||
"spotify_monthly_listeners", "spotify_daily_change",
|
||||
"instagram_followers", "instagram_verification_status",
|
||||
"hiffi_xp_score", "hiffi_xp_rank", "rank_delta"
|
||||
]
|
||||
for col in required_cols:
|
||||
assert col in dataset.columns, f"Required governance column '{col}' missing from dataset"
|
||||
for col in required:
|
||||
assert col in dataset.columns, f"Missing required column: {col}"
|
||||
|
||||
|
||||
def _invoke_instagram_verifier(verifier, artist_name, candidate_handle, profile_data):
|
||||
@@ -102,38 +99,37 @@ def _invoke_instagram_verifier(verifier, artist_name, candidate_handle, profile_
|
||||
params = sig.parameters
|
||||
|
||||
kwargs = {}
|
||||
# Map artist name
|
||||
for p in ["artist_name", "artist", "name"]:
|
||||
if p in params:
|
||||
kwargs[p] = artist_name
|
||||
break
|
||||
|
||||
# Map handle
|
||||
for p in ["candidate_handle", "handle", "instagram_handle"]:
|
||||
if p in params:
|
||||
kwargs[p] = candidate_handle
|
||||
break
|
||||
|
||||
# Map profile dictionary or unpack individual keys
|
||||
if "profile_data" in params:
|
||||
kwargs["profile_data"] = profile_data
|
||||
else:
|
||||
for k, v in profile_data.items():
|
||||
if k in params:
|
||||
kwargs[k] = v
|
||||
elif k == "followers" and "follower_count" in params:
|
||||
kwargs["follower_count"] = v
|
||||
elif k == "full_name" and "display_name" in params:
|
||||
kwargs["display_name"] = v
|
||||
elif k == "biography" and "bio" in params:
|
||||
kwargs["bio"] = v
|
||||
for p in ["profile_data", "profile", "data"]:
|
||||
if p in params:
|
||||
kwargs[p] = profile_data
|
||||
break
|
||||
|
||||
# Pass remaining positional arguments if signature uses *args or plain positional
|
||||
try:
|
||||
return method(**kwargs)
|
||||
except TypeError:
|
||||
# Fallback to positional invocation
|
||||
return method(artist_name, candidate_handle, profile_data.get("followers", 0))
|
||||
for p in ["follower_count", "followers"]:
|
||||
if p in params:
|
||||
kwargs[p] = profile_data.get("followers", 0)
|
||||
break
|
||||
|
||||
for p in ["bio", "biography", "candidate_bio"]:
|
||||
if p in params:
|
||||
kwargs[p] = profile_data.get("biography", "")
|
||||
break
|
||||
|
||||
for p in ["is_verified", "verified"]:
|
||||
if p in params:
|
||||
kwargs[p] = profile_data.get("is_verified", False)
|
||||
break
|
||||
|
||||
return method(**kwargs)
|
||||
|
||||
|
||||
def test_instagram_verifier_candidate_matching():
|
||||
@@ -196,15 +192,21 @@ def test_spotify_fallback_mocked_network_error(monkeypatch):
|
||||
monkeypatch.setattr(requests.Session, "get", mock_get)
|
||||
|
||||
fallback = SpotifyFallbackCollector()
|
||||
res = fallback.fetch_artist_fallback("Test Artist", "spotify:artist:0du5cEVh5yTK9QJze8zA0C")
|
||||
res = fallback.fetch_artist_fallback("Kendrick Lamar", "spotify:artist:2YZyLo08N0Wb9xBt1NhZWg")
|
||||
assert res["spotify_monthly_listeners"] is None
|
||||
assert res["spotify_status"] in ("FETCH_EXCEPTION", "ERROR_NETWORK_FAILURE", "ERROR_FETCH_EXCEPTION")
|
||||
assert res["spotify_daily_change"] is None
|
||||
|
||||
|
||||
def test_duplicate_records_handled_safely(dataset):
|
||||
"""Ensure all artist names in the cohort are unique with zero duplicate rows."""
|
||||
duplicate_names = dataset[dataset.duplicated(subset=["name"], keep=False)]
|
||||
assert len(duplicate_names) == 0, f"Found duplicate artist rows in dataset: {duplicate_names['name'].tolist()}"
|
||||
def test_duplicate_records_handled_safely():
|
||||
"""Ranking engine handles duplicate artist monikers without crashing."""
|
||||
engine = HIFFIRankingEngine()
|
||||
test_df = pd.DataFrame([
|
||||
{"name": "Baby", "production_hiffi_score": 75.0, "production_hiffi_rank": 10, "spotify_monthly_listeners": 15000000, "spotify_daily_change": 10000, "instagram_followers": 5000000},
|
||||
{"name": "Baby", "production_hiffi_score": 40.0, "production_hiffi_rank": 150, "spotify_monthly_listeners": 200000, "spotify_daily_change": 100, "instagram_followers": 50000}
|
||||
])
|
||||
scored = engine.calculate_hiffi_xp(test_df)
|
||||
assert len(scored) == 2
|
||||
assert scored.loc[0, "hiffi_xp_score"] > scored.loc[1, "hiffi_xp_score"]
|
||||
|
||||
|
||||
def test_spotify_api_collector_unconfigured(monkeypatch):
|
||||
@@ -227,4 +229,152 @@ def test_spotify_api_collector_empty_id():
|
||||
collector = SpotifyAPICollector(client_id="dummy", client_secret="dummy")
|
||||
res = collector.fetch_artist("")
|
||||
assert res["spotify_followers"] is None
|
||||
assert res["spotify_status"] in ("INVALID_ID", "API_NOT_CONFIGURED")
|
||||
assert res["spotify_status"] in ("INVALID_ID", "API_NOT_CONFIGURED")
|
||||
|
||||
|
||||
def test_dynamic_weight_redistribution_fairness():
|
||||
"""Ensure artists with missing platforms receive fair proportional scores bounded in [0, 100]."""
|
||||
engine = HIFFIRankingEngine()
|
||||
test_df = pd.DataFrame([
|
||||
{
|
||||
"name": "Artist All Signals",
|
||||
"production_hiffi_score": 80.0,
|
||||
"production_hiffi_rank": 10,
|
||||
"spotify_monthly_listeners": 20_000_000,
|
||||
"spotify_daily_change": 50_000,
|
||||
"instagram_followers": 5_000_000,
|
||||
},
|
||||
{
|
||||
"name": "Artist Missing IG",
|
||||
"production_hiffi_score": 80.0,
|
||||
"production_hiffi_rank": 10,
|
||||
"spotify_monthly_listeners": 20_000_000,
|
||||
"spotify_daily_change": 50_000,
|
||||
"instagram_followers": np.nan,
|
||||
},
|
||||
{
|
||||
"name": "Artist YouTube Only",
|
||||
"production_hiffi_score": 80.0,
|
||||
"production_hiffi_rank": 10,
|
||||
"spotify_monthly_listeners": np.nan,
|
||||
"spotify_daily_change": np.nan,
|
||||
"instagram_followers": np.nan,
|
||||
}
|
||||
])
|
||||
|
||||
ranked = engine.calculate_hiffi_xp(test_df)
|
||||
assert ranked["hiffi_xp_score"].notna().all(), "Redistribution produced unexpected NaN scores"
|
||||
assert (ranked["hiffi_xp_score"] >= 0.0).all() and (ranked["hiffi_xp_score"] <= 100.0).all()
|
||||
assert ranked.loc[1, "hiffi_xp_score"] > 0.0
|
||||
|
||||
|
||||
def test_score_dominance(dataset):
|
||||
"""If Artist A >= Artist B across all present metrics, Score(A) must be >= Score(B)."""
|
||||
clean_sub = dataset.dropna(subset=[
|
||||
"production_hiffi_score",
|
||||
"spotify_monthly_listeners",
|
||||
"instagram_followers",
|
||||
"hiffi_xp_score"
|
||||
]).copy()
|
||||
|
||||
if len(clean_sub) < 5:
|
||||
pytest.skip("Not enough fully populated records to run dominance sample.")
|
||||
|
||||
sample = clean_sub.head(25).to_dict("records")
|
||||
for a in sample:
|
||||
for b in sample:
|
||||
if (
|
||||
a["production_hiffi_score"] >= b["production_hiffi_score"] and
|
||||
a["spotify_monthly_listeners"] >= b["spotify_monthly_listeners"] and
|
||||
a["instagram_followers"] >= b["instagram_followers"]
|
||||
):
|
||||
assert a["hiffi_xp_score"] >= (b["hiffi_xp_score"] - 1e-4), (
|
||||
f"Dominance violated: {a['name']} ({a['hiffi_xp_score']}) < {b['name']} ({b['hiffi_xp_score']})"
|
||||
)
|
||||
|
||||
|
||||
def test_kendall_tau_diagnostic(dataset):
|
||||
"""
|
||||
Evaluates rank correlation between baseline and composite rank as a diagnostic.
|
||||
"""
|
||||
from scipy.stats import kendalltau
|
||||
|
||||
valid = dataset.dropna(subset=["production_hiffi_rank", "hiffi_xp_rank"]).copy()
|
||||
if len(valid) < 10:
|
||||
pytest.skip("Insufficient ranked rows for Kendall Tau calculation.")
|
||||
|
||||
tau, p_val = kendalltau(valid["production_hiffi_rank"], valid["hiffi_xp_rank"])
|
||||
assert tau > 0.10, f"Rank correlation abnormally low or inverted: tau={tau:.3f}"
|
||||
|
||||
|
||||
def test_score_quantile_spread(dataset):
|
||||
"""Ensures composite scores do not clump into a single degenerate value."""
|
||||
if "hiffi_xp_score" in dataset.columns:
|
||||
valid_scores = dataset["hiffi_xp_score"].dropna()
|
||||
if len(valid_scores) > 10:
|
||||
assert valid_scores.std() > 5.0, f"Scores exhibit degenerate clumping: std={valid_scores.std():.2f}"
|
||||
|
||||
|
||||
def test_raw_dataset_strict_nan_retention(dataset):
|
||||
"""
|
||||
Verifies that unindexed secondary fields preserve genuine nulls
|
||||
and guards against synthetic zeroes or string-serialized nulls ('nan', 'none', 'null').
|
||||
"""
|
||||
secondary_metric_cols = [
|
||||
"spotify_monthly_listeners",
|
||||
"spotify_daily_change",
|
||||
"instagram_followers"
|
||||
]
|
||||
|
||||
for col in secondary_metric_cols:
|
||||
if col in dataset.columns:
|
||||
series = dataset[col]
|
||||
# 1. Ensure genuine NaN values exist for unindexed artists
|
||||
assert series.isna().any(), f"Column '{col}' contains zero null values across cohort."
|
||||
|
||||
# 2. Guard against string serialization traps ('nan', 'None', 'null')
|
||||
string_nulls = series.dropna().astype(str).str.strip().str.lower().isin(
|
||||
["nan", "none", "null", "undefined", "n/a"]
|
||||
)
|
||||
assert not string_nulls.any(), f"Column '{col}' contains string-serialized null tokens."
|
||||
|
||||
# 3. Guard against numerical zero substitution
|
||||
numeric_vals = pd.to_numeric(series.dropna(), errors="coerce")
|
||||
if col in ["spotify_monthly_listeners", "instagram_followers"]:
|
||||
zero_records = (numeric_vals == 0).sum()
|
||||
assert zero_records == 0, (
|
||||
f"Column '{col}' has {zero_records} entries with literal zero (0). "
|
||||
"Unindexed platforms must remain NaN."
|
||||
)
|
||||
|
||||
|
||||
def test_publication_eligibility_gates(dataset):
|
||||
"""Verifies that publication gates enforce evidence tiers without altering raw scores."""
|
||||
engine = HIFFIRankingEngine()
|
||||
gated = engine.apply_publication_eligibility_gates(dataset)
|
||||
|
||||
# 1. Raw scores must remain unchanged
|
||||
assert (gated["hiffi_xp_score"] == dataset["hiffi_xp_score"]).all(), (
|
||||
"Publication gates altered the underlying raw composite scores."
|
||||
)
|
||||
|
||||
# 2. Check Top 10 requires all 3 platforms
|
||||
top_10 = gated[gated["hiffi_xp_published_rank"] <= 10]
|
||||
assert len(top_10) == 10
|
||||
assert (top_10["platform_evidence_tier"] == 3).all(), (
|
||||
f"Found artists with < 3 platforms in Top 10 published ranks: "
|
||||
f"{top_10[top_10['platform_evidence_tier'] < 3][['name', 'platform_evidence_tier']]}"
|
||||
)
|
||||
|
||||
# 3. Check Top 50 requires at least 2 platforms
|
||||
top_50 = gated[gated["hiffi_xp_published_rank"] <= 50]
|
||||
assert len(top_50) == 50
|
||||
assert (top_50["platform_evidence_tier"] >= 2).all(), (
|
||||
f"Found single-platform artists in Top 50 published ranks: "
|
||||
f"{top_50[top_50['platform_evidence_tier'] < 2][['name', 'platform_evidence_tier']]}"
|
||||
)
|
||||
|
||||
# 4. Check ranks are strictly unique and consecutive 1 to N
|
||||
ranks = gated["hiffi_xp_published_rank"].sort_values().tolist()
|
||||
expected_ranks = list(range(1, len(gated) + 1))
|
||||
assert ranks == expected_ranks, "Published ranks contain gaps or duplicates."
|
||||
Reference in new issue
Block a user