vera-api / backend /app /ml /features.py
prathvi9607's picture
Deploy VERA API (round 3 models)
31e8a9f verified
Raw History Blame Contribute Delete
4.37 kB
"""Fusion feature contract (PRD §10.1).
This module is the SINGLE source of truth for the 40-dimensional feature vector.
Notebook 05 (training), notebook 06 (calibration), and the production pipeline
all call :func:`build_feature_vector`, so train/serve skew is impossible by
construction. Changing the order or meaning of any feature requires retraining
the ensemble and re-fitting the calibrator.
Imputation of missing metadata happens upstream in Agent 5
(:class:`~app.ml.agents.quality.QualityAssessor`); the PRD's
``build_feature_vector(imputation=...)`` parameter was never actually used.
"""
from __future__ import annotations
from typing import Any, Final
import numpy as np
from app.ml.constants import EARBUD_ASPECTS, SENTIMENT_TO_NUMERIC
FEATURE_NAMES: Final[tuple[str, ...]] = (
# Base signals (3)
"star_rating",
"experience_score",
"domain_score",
# Consistency signals (4)
"star_text_gap",
"absolute_gap",
"star_inflated",
"consistency_score",
# Quality signals (4)
"quality_score",
"helpfulness_score",
"freshness_score",
"verified_purchase",
# Text quality signals (4)
"word_count_norm",
"vocab_richness",
"avg_sentence_length_norm",
"experience_model_uncertainty",
# Aspect detection (10) + aspect sentiment (10)
*(f"aspect_detected__{a}" for a in EARBUD_ASPECTS),
*(f"aspect_sentiment__{a}" for a in EARBUD_ASPECTS),
# Interactions (5)
"star_x_experience",
"abs_star_minus_domain",
"experience_x_quality",
"consistency_x_trust",
"domain_confidence_x_domain_score",
)
N_FEATURES: Final = len(FEATURE_NAMES)
#: Feature-name groups used by the ablation study (PRD §13.2).
FEATURE_GROUPS: Final[dict[str, tuple[str, ...]]] = {
"star": ("star_rating",),
"experience": ("experience_score", "experience_model_uncertainty", "star_x_experience"),
"domain": ("domain_score", "abs_star_minus_domain", "domain_confidence_x_domain_score"),
"consistency": ("star_text_gap", "absolute_gap", "star_inflated", "consistency_score"),
"quality": (
"quality_score",
"helpfulness_score",
"freshness_score",
"verified_purchase",
"word_count_norm",
"vocab_richness",
"avg_sentence_length_norm",
"experience_x_quality",
"consistency_x_trust",
),
"absa": tuple(n for n in FEATURE_NAMES if n.startswith("aspect_")),
}
def build_feature_vector(agent_outputs: dict[str, Any]) -> np.ndarray:
"""Assemble the fusion features from Agents 1–6.
Args:
agent_outputs: Mapping with keys ``preprocessor``, ``experience_extractor``,
``absa``, ``consistency``, ``quality``, ``domain_rating`` (each the
agent's ``process``/``predict`` output) and ``star_rating``.
Returns:
``float32`` array of shape ``(N_FEATURES,)``.
"""
pre = agent_outputs["preprocessor"]
exp = agent_outputs["experience_extractor"]
absa = agent_outputs["absa"]["aspects"]
cons = agent_outputs["consistency"]
qual = agent_outputs["quality"]
dom = agent_outputs["domain_rating"]
star = float(agent_outputs["star_rating"])
feats: list[float] = [
star,
exp["predicted_score"],
dom["domain_score"],
cons["star_text_gap"],
cons["absolute_gap"],
float(cons["star_inflated"]),
cons["consistency_score"],
qual["quality_score"],
qual["helpfulness_score"],
qual["freshness_score"],
float(qual["verified_purchase"]),
pre["word_count"] / 500,
pre["vocab_richness"],
pre["avg_sentence_length"] / 20,
exp["model_uncertainty"],
]
feats += [float(absa[a]["detected"]) for a in EARBUD_ASPECTS]
feats += [float(SENTIMENT_TO_NUMERIC[absa[a]["sentiment"]]) for a in EARBUD_ASPECTS]
feats += [
star * exp["predicted_score"],
abs(star - dom["domain_score"]),
exp["predicted_score"] * qual["quality_score"],
cons["consistency_score"] * qual["trust_weight"],
dom["domain_confidence"] * dom["domain_score"],
]
vector = np.asarray(feats, dtype=np.float32)
if vector.shape != (N_FEATURES,): # guards against silent contract drift
raise AssertionError(f"feature vector has {vector.shape[0]} dims, expected {N_FEATURES}")
return vector