git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
48 lines
1.8 KiB
Python
48 lines
1.8 KiB
Python
"""Conservative aggregation of supplier/document observations."""
|
|
|
|
from dataclasses import dataclass
|
|
from collections import defaultdict
|
|
from typing import Any, Iterable
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Observation:
|
|
product_id: str
|
|
attribute: str
|
|
value: str
|
|
supplier_id: str
|
|
document_id: str
|
|
observed_at: str
|
|
provenance: str = "SUPPLIER_OBSERVATION"
|
|
|
|
|
|
def aggregate_observations(observations: Iterable[Observation]) -> dict[tuple[str, str], dict[str, Any]]:
|
|
grouped: dict[tuple[str, str], list[Observation]] = defaultdict(list)
|
|
seen: set[tuple[str, str, str, str]] = set()
|
|
for observation in observations:
|
|
key = (observation.product_id, observation.attribute)
|
|
dedupe = (observation.product_id, observation.attribute, observation.supplier_id, observation.document_id)
|
|
if dedupe not in seen and observation.value not in (None, ""):
|
|
grouped[key].append(observation)
|
|
seen.add(dedupe)
|
|
|
|
output = {}
|
|
for key, rows in grouped.items():
|
|
values = {row.value for row in rows}
|
|
counts = {value: sum(row.value == value for row in rows) for value in values}
|
|
supplier_count = len({row.supplier_id for row in rows})
|
|
agreement_rate = max(counts.values()) / len(rows) if rows else 0.0
|
|
conflict_count = max(0, len(values) - 1)
|
|
strong = len(rows) >= 2 and supplier_count >= 2 and len(values) == 1
|
|
output[key] = {
|
|
"value": next(iter(values)) if strong else None,
|
|
"observation_count": len(rows),
|
|
"supplier_count": supplier_count,
|
|
"agreement_rate": agreement_rate,
|
|
"latest_observation": max(row.observed_at for row in rows),
|
|
"conflict_count": conflict_count,
|
|
"strong_evidence": strong,
|
|
"provenance": sorted({row.provenance for row in rows}),
|
|
}
|
|
return output
|