| from __future__ import annotations |
|
|
| import csv |
| import os |
| import re |
| import statistics |
| import sys |
| import time |
| from pathlib import Path |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| sys.path.insert(0, str(ROOT)) |
|
|
| from src.aspect_sentiment import AspectSentimentEngine |
|
|
| GOLD_CSV = ROOT / "data" / "raw" / "features.csv" |
| WORD_RX = re.compile(r"[a-z0-9]+") |
|
|
|
|
| def load_local_env() -> None: |
| for env_path in (ROOT / ".env.local", ROOT / ".env"): |
| if not env_path.exists(): |
| continue |
| for raw_line in env_path.read_text(encoding="utf-8").splitlines(): |
| line = raw_line.strip() |
| if not line or line.startswith("#") or "=" not in line: |
| continue |
| key, value = line.split("=", 1) |
| os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) |
|
|
|
|
| def norm(value: object) -> str: |
| return " ".join(WORD_RX.findall(str(value or "").lower())) |
|
|
|
|
| def split_terms(value: object) -> list[str]: |
| return [term for term in (norm(part) for part in str(value or "").split(",")) if term] |
|
|
|
|
| def expected_terms(row: dict[str, str]) -> set[str]: |
| terms: set[str] = set() |
| for field in ("product", "brands", "use_case", "product_suggested"): |
| terms.update(split_terms(row.get(field, ""))) |
| budget = norm(row.get("budget", "")) |
| if budget and budget != "none": |
| terms.add(budget) |
| return {term for term in terms if term not in {"none", "na", "n a"}} |
|
|
|
|
| def term_matches(expected: str, predicted_terms: set[str]) -> bool: |
| expected_tokens = set(expected.split()) |
| for predicted in predicted_terms: |
| if expected == predicted or expected in predicted or predicted in expected: |
| return True |
| predicted_tokens = set(predicted.split()) |
| if expected_tokens and expected_tokens <= predicted_tokens: |
| return True |
| return False |
|
|
|
|
| def main() -> None: |
| load_local_env() |
|
|
| engine = AspectSentimentEngine() |
| latencies: list[float] = [] |
| total_expected = 0 |
| total_matched = 0 |
| total_predicted = 0 |
| rows_with_any_match = 0 |
| errors: list[str] = [] |
|
|
| with GOLD_CSV.open(newline="", encoding="utf-8-sig") as handle: |
| rows = list(csv.DictReader(handle)) |
|
|
| print(f"Provider: llama:{engine.llama_model}") |
| print(f"Rows: {len(rows)}") |
| print() |
|
|
| for index, row in enumerate(rows, start=1): |
| text = row.get("text", "") |
| gold = expected_terms(row) |
| started = time.perf_counter() |
| try: |
| result = engine.extract_mentions_with_provider(text) |
| elapsed = time.perf_counter() - started |
| latencies.append(elapsed) |
| predicted = {norm(mention.name) for mention in result.mentions if norm(mention.name)} |
| matched = {term for term in gold if term_matches(term, predicted)} |
| rows_with_any_match += int(bool(matched)) |
| total_expected += len(gold) |
| total_matched += len(matched) |
| total_predicted += len(predicted) |
| print( |
| f"{index:02d} {row.get('file','')}: " |
| f"{elapsed:.2f}s expected={len(gold)} predicted={len(predicted)} matched={len(matched)}" |
| ) |
| if gold - matched: |
| print(f" missed: {', '.join(sorted(gold - matched))}") |
| if predicted: |
| print(f" predicted: {', '.join(sorted(predicted))}") |
| except Exception as exc: |
| elapsed = time.perf_counter() - started |
| errors.append(f"{row.get('file', index)}: {exc}") |
| print(f"{index:02d} {row.get('file','')}: ERROR after {elapsed:.2f}s - {exc}") |
|
|
| print() |
| print("Summary") |
| recall = total_matched / total_expected if total_expected else 0.0 |
| approx_precision = total_matched / total_predicted if total_predicted else 0.0 |
| print(f"Expected term recall: {recall:.3f} ({total_matched}/{total_expected})") |
| print(f"Approx term precision: {approx_precision:.3f} ({total_matched}/{total_predicted})") |
| print(f"Rows with any expected match: {rows_with_any_match}/{len(rows)}") |
| if latencies: |
| print(f"Latency avg: {statistics.mean(latencies):.2f}s") |
| print(f"Latency median: {statistics.median(latencies):.2f}s") |
| print(f"Latency min/max: {min(latencies):.2f}s / {max(latencies):.2f}s") |
| if errors: |
| print("Errors:") |
| for error in errors: |
| print(f"- {error}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|