UAP-Data-Analysis-Tool / api /services /scu_service.py
Ashoka74's picture
Deploy current work to HF Space (slim)
a1aef88
Raw
History Blame Contribute Delete
3.51 kB
"""SCU normalization service — wraps ``scu_normalizer`` to canonicalise parsed
UAP responses (country/US-state codes, witness roles, craft shape/size bands)
and derive the SCU five-criterion eligibility gate, plus an eligibility filter
with the same presets as the Streamlit page.
"""
from __future__ import annotations
from typing import Any
# Preset → ordered list of criterion keys, mirroring render_scu_filter() in
# parsing.py. Built lazily so importing this module doesn't import the
# normalizer (and its data tables) until a request actually needs it.
def _presets(criteria_keys: list[str]) -> dict[str, list[str] | None]:
post_1975_keys = ["post_1975_window"] + [k for k in criteria_keys if k != "in_scu_window"]
return {
"Full five-criterion gate (1945-1975, recommended)": criteria_keys,
"Post-1975 five-criterion gate (1975 onwards)": post_1975_keys,
"Phase-3 analog — no window / anomaly / credibility gates": [
"has_core_fields", "has_investigation_channel",
"has_engagement_signal", "day_night_resolved", "military_public_known",
],
"SCU window only (1945-1975)": ["in_scu_window"],
"Post-1975 window only (1975 onwards)": ["post_1975_window"],
"Data quality only — Criterion 2": ["has_core_fields"],
}
def criteria_info() -> dict:
"""Criterion keys/labels and named presets for the filter UI."""
import scu_normalizer
criteria = [
{"key": k, "column": c, "label": lbl} for k, c, lbl in scu_normalizer.SCU_CRITERIA
]
extra = [
{"key": k, "column": c, "label": lbl} for k, c, lbl in scu_normalizer.SCU_EXTRA_CRITERIA
]
keys = [k for k, _c, _l in scu_normalizer.SCU_CRITERIA]
return {"criteria": criteria, "extra_criteria": extra, "presets": _presets(keys)}
def _raw_df_from_parsed(parsed_responses: dict):
import pandas as pd
return pd.json_normalize(list(parsed_responses.values()))
def normalize(parsed_responses: dict) -> dict:
"""Run scu_normalizer.normalize() on parsed responses.
Returns the normalized DataFrame plus the audit dict, its markdown render,
and headline eligibility metrics.
"""
import scu_normalizer
if not parsed_responses:
raise ValueError("No parsed responses to normalize.")
raw_df = _raw_df_from_parsed(parsed_responses)
if raw_df.empty:
raise ValueError("Parsed responses produced an empty DataFrame.")
norm_df, audit = scu_normalizer.normalize(raw_df)
audit_md = scu_normalizer.audit_to_markdown(audit)
metrics = {
"rows": int(audit.get("output_rows", len(norm_df))),
"scu_eligible": int(audit.get("scu_eligible_count", 0)),
"in_scu_window": int(audit.get("in_scu_window_count", 0)),
"has_credible_witness": int(audit.get("has_credible_witness_count", 0)),
}
return {"df": norm_df, "audit": audit, "audit_markdown": audit_md, "metrics": metrics}
def filter_eligibility(norm_df, criterion_keys: list[str]) -> dict:
"""AND the selected SCU criterion columns and return the surviving rows plus
a cumulative funnel (one stage per criterion)."""
import scu_normalizer
stages, values, mask = scu_normalizer.incremental_funnel(norm_df, criterion_keys)
filtered = norm_df[mask.values] if len(mask) == len(norm_df) else norm_df[mask]
funnel = [{"stage": s, "count": v} for s, v in zip(stages, values)]
return {"df": filtered, "funnel": funnel, "n_passed": int(mask.sum())}