Build holdout v4 from the public AA set, correct the two v3 dates, and sweep R1–R5 plus pairs offline. No production scoring defaults change. No implementation brief: delivered width stays the full window on every radius.
347 lines
12 KiB
Python
347 lines
12 KiB
Python
"""Pure helpers for the minute-resolution scoring sweep. No production defaults."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections.abc import Iterable, Sequence
|
|
from typing import Any
|
|
|
|
from scripts.active_rectification_event_engine import DOMAIN_CONFIG
|
|
from scripts.rectification.candidate_contrast import (
|
|
SIGNATURE_LAYERS,
|
|
cluster_contexts_by_signature,
|
|
context_time,
|
|
layer_value,
|
|
select_signature_representatives,
|
|
)
|
|
from scripts.rectification.scoring_service import PRECISION_WEIGHTS, _event_kind_factor
|
|
|
|
VARGA_RULE_WEIGHT = {
|
|
"vim_md_domain_varga": 2.0,
|
|
"vim_ad_domain_varga": 1.5,
|
|
"vim_pd_domain_varga": 0.75,
|
|
}
|
|
|
|
KP_MATCH = {
|
|
"md": (0.50, 0.25),
|
|
"ad": (0.35, 0.15),
|
|
"pd": (0.20, 0.10),
|
|
}
|
|
|
|
DOMAIN_SIGNATURE_LAYERS = {
|
|
"education": ("d24", "d5"),
|
|
"relocation": ("d4",),
|
|
"relationship": ("d9",),
|
|
"career": ("d10",),
|
|
"occupation": ("d10",),
|
|
"finance": ("d2", "d11"),
|
|
"health_pressure": ("d30",),
|
|
"family": ("d12", "d7", "d3"),
|
|
}
|
|
|
|
EXTRA_LAYER_VARGA = {
|
|
"d2": "D2",
|
|
"d3": "D3",
|
|
"d5": "D5",
|
|
"d7": "D7",
|
|
"d11": "D11",
|
|
"d30": "D30",
|
|
}
|
|
|
|
NARROW_WIDTH = 5
|
|
|
|
|
|
def clock(value: str) -> int:
|
|
stamp = str(value)[:5]
|
|
return int(stamp[:2]) * 60 + int(stamp[3:5])
|
|
|
|
|
|
def hhmm_from_minutes(value: int) -> str:
|
|
wrapped = value % 1440
|
|
return f"{wrapped // 60:02d}:{wrapped % 60:02d}"
|
|
|
|
|
|
def shift_clock(value: str, delta: int) -> str:
|
|
return hhmm_from_minutes(clock(value) + delta)
|
|
|
|
|
|
def range_width(times: Sequence[str]) -> int | None:
|
|
clocks = sorted(clock(item) for item in times if str(item)[:5])
|
|
if not clocks:
|
|
return None
|
|
return clocks[-1] - clocks[0] + 1
|
|
|
|
|
|
def varga_divisor(count: int, mode: str) -> float:
|
|
n = max(int(count), 1)
|
|
if mode == "len":
|
|
return float(n)
|
|
if mode == "sqrt":
|
|
return math.sqrt(n)
|
|
if mode == "fixed2":
|
|
return 2.0
|
|
return 2.0 * n
|
|
|
|
|
|
def rescale_varga_points(
|
|
points: float,
|
|
rule_ids: Sequence[str],
|
|
domain: str,
|
|
event_kind: str,
|
|
precision: str,
|
|
mode: str,
|
|
) -> float:
|
|
if mode == "2len":
|
|
return float(points)
|
|
prefixes, _houses = DOMAIN_CONFIG[domain]
|
|
n = max(len(prefixes), 1)
|
|
old_div = varga_divisor(n, "2len")
|
|
new_div = varga_divisor(n, mode)
|
|
if abs(old_div - new_div) < 1e-12:
|
|
return float(points)
|
|
delta_inner = 0.0
|
|
for rule in rule_ids:
|
|
weight = VARGA_RULE_WEIGHT.get(str(rule))
|
|
if weight is None:
|
|
continue
|
|
delta_inner += weight * (1.0 / new_div - 1.0 / old_div)
|
|
precision_w = PRECISION_WEIGHTS.get(precision, 1.0)
|
|
kind = _event_kind_factor(event_kind, rule_ids)
|
|
return round(float(points) + delta_inner * kind * (precision_w ** 2), 4)
|
|
|
|
|
|
def aggregate_samples(samples: Sequence[float], mode: str, temperature: float = 1.0) -> float:
|
|
values = [float(item) for item in samples]
|
|
if not values:
|
|
return 0.0
|
|
if mode == "max":
|
|
return round(max(values), 4)
|
|
if mode == "lse":
|
|
temp = max(float(temperature), 1e-6)
|
|
peak = max(values)
|
|
mean_exp = sum(math.exp((item - peak) / temp) for item in values) / len(values)
|
|
return round(temp * math.log(mean_exp) + peak, 4)
|
|
return round(sum(values) / len(values), 4)
|
|
|
|
|
|
def subtract_event_floor(by_time: dict[str, float]) -> dict[str, float]:
|
|
if not by_time:
|
|
return {}
|
|
floor = min(by_time.values())
|
|
return {time: round(score - floor, 4) for time, score in by_time.items()}
|
|
|
|
|
|
def shannon_entropy(weights: Iterable[float]) -> float:
|
|
values = [max(float(item), 0.0) for item in weights]
|
|
total = sum(values)
|
|
if total <= 0:
|
|
return 0.0
|
|
entropy = 0.0
|
|
for value in values:
|
|
if value <= 0:
|
|
continue
|
|
share = value / total
|
|
entropy -= share * math.log(share, 2)
|
|
return round(entropy, 6)
|
|
|
|
|
|
def kp_event_points(
|
|
context: dict[str, Any],
|
|
domain: str,
|
|
vim_lords: tuple[str, str, str],
|
|
weight: float,
|
|
) -> float:
|
|
feature = context.get("feature") if isinstance(context.get("feature"), dict) else {}
|
|
snapshot = feature.get("kp_cusps") if isinstance(feature.get("kp_cusps"), dict) else {}
|
|
if snapshot.get("status") != "executed":
|
|
return 0.0
|
|
houses = snapshot.get("houses") if isinstance(snapshot.get("houses"), dict) else {}
|
|
_prefixes, target_houses = DOMAIN_CONFIG[domain]
|
|
points = 0.0
|
|
md, ad, pd = vim_lords
|
|
for house in target_houses:
|
|
row = houses.get(str(house))
|
|
if not isinstance(row, dict):
|
|
continue
|
|
sub = str(row.get("sub_lord") or "")
|
|
sub_sub = str(row.get("sub_sub_lord") or "")
|
|
for lord, (sub_w, sub_sub_w) in (
|
|
(md, KP_MATCH["md"]),
|
|
(ad, KP_MATCH["ad"]),
|
|
(pd, KP_MATCH["pd"]),
|
|
):
|
|
if lord and lord == sub:
|
|
points += sub_w
|
|
if lord and lord == sub_sub:
|
|
points += sub_sub_w
|
|
return round(points * float(weight), 4)
|
|
|
|
|
|
def kp_sub_lord_key(context: dict[str, Any], houses: Sequence[int]) -> tuple[str, ...]:
|
|
feature = context.get("feature") if isinstance(context.get("feature"), dict) else {}
|
|
snapshot = feature.get("kp_cusps") if isinstance(feature.get("kp_cusps"), dict) else {}
|
|
table = snapshot.get("houses") if isinstance(snapshot.get("houses"), dict) else {}
|
|
keys = []
|
|
for house in houses:
|
|
row = table.get(str(house)) if isinstance(table.get(str(house)), dict) else {}
|
|
keys.append(str(row.get("sub_lord") or ""))
|
|
return tuple(keys)
|
|
|
|
|
|
def kp_changes_in_window(contexts: Sequence[dict[str, Any]], domain: str) -> int:
|
|
_prefixes, houses = DOMAIN_CONFIG[domain]
|
|
seen: list[tuple[str, ...]] = []
|
|
for context in contexts:
|
|
key = kp_sub_lord_key(context, houses)
|
|
if not seen or seen[-1] != key:
|
|
seen.append(key)
|
|
return max(len(seen) - 1, 0)
|
|
|
|
|
|
def dynamic_signature_layers(domains: Sequence[str]) -> tuple[str, ...]:
|
|
layers = ["d1"]
|
|
for domain in domains:
|
|
for layer in DOMAIN_SIGNATURE_LAYERS.get(domain, ()):
|
|
if layer not in layers:
|
|
layers.append(layer)
|
|
return tuple(layers)
|
|
|
|
|
|
def layer_value_extended(context: dict[str, Any], layer: str) -> int | None:
|
|
if layer in SIGNATURE_LAYERS or layer == "md" or layer == "d1":
|
|
return layer_value(context, layer)
|
|
name = EXTRA_LAYER_VARGA.get(layer)
|
|
if not name:
|
|
return None
|
|
feature = context.get("feature") if isinstance(context.get("feature"), dict) else {}
|
|
vargas = feature.get("varga_ascendants") if isinstance(feature.get("varga_ascendants"), dict) else {}
|
|
raw = vargas.get(name)
|
|
return raw if isinstance(raw, int) else None
|
|
|
|
|
|
def cluster_contexts(
|
|
contexts: Sequence[dict[str, Any]],
|
|
layers: Sequence[str] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
if not layers or tuple(layers) == SIGNATURE_LAYERS:
|
|
return cluster_contexts_by_signature(contexts)
|
|
buckets: dict[tuple[int | None, ...], list[dict[str, Any]]] = {}
|
|
for context in contexts:
|
|
if not isinstance(context, dict):
|
|
continue
|
|
time = context_time(context)
|
|
if not time:
|
|
continue
|
|
signature = tuple(layer_value_extended(context, layer) for layer in layers)
|
|
buckets.setdefault(signature, []).append(context)
|
|
clusters: list[dict[str, Any]] = []
|
|
for signature, members in buckets.items():
|
|
ordered = sorted(members, key=lambda item: clock(str(context_time(item))))
|
|
times = [str(context_time(item)) for item in ordered]
|
|
clusters.append({
|
|
"signature": signature,
|
|
"signature_key": ",".join("x" if value is None else str(value) for value in signature),
|
|
"contexts": ordered,
|
|
"times": times,
|
|
"representative_time": times[len(times) // 2],
|
|
"representative": ordered[len(ordered) // 2],
|
|
})
|
|
clusters.sort(key=lambda item: clock(item["representative_time"]))
|
|
return clusters
|
|
|
|
|
|
def public_rows(
|
|
rows: Sequence[dict[str, Any]],
|
|
contexts: Sequence[dict[str, Any]],
|
|
layers: Sequence[str] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
if not layers or tuple(layers) == SIGNATURE_LAYERS:
|
|
return select_signature_representatives(rows, contexts)
|
|
by_time = {str(row.get("time"))[:5]: row for row in rows if str(row.get("time"))}
|
|
clusters = cluster_contexts(contexts, layers)
|
|
from scripts.rectification.candidate_contrast import cap_clusters_by_adjacent_merge
|
|
clusters = cap_clusters_by_adjacent_merge(clusters, by_time)
|
|
representatives: list[dict[str, Any]] = []
|
|
for cluster in clusters:
|
|
members = [by_time[time] for time in cluster["times"] if time in by_time]
|
|
if not members:
|
|
continue
|
|
best = max(members, key=lambda row: (float(row.get("score") or 0), str(row.get("time"))))
|
|
representatives.append({
|
|
**best,
|
|
"cluster_times": [time for time in cluster["times"] if time in by_time],
|
|
})
|
|
representatives.sort(key=lambda row: (-float(row.get("score") or 0), str(row.get("time"))))
|
|
return representatives or list(rows)[:1]
|
|
|
|
|
|
def cluster_of(true_time: str, clusters: Sequence[dict[str, Any]]) -> dict[str, Any] | None:
|
|
stamp = str(true_time)[:5]
|
|
for cluster in clusters:
|
|
times = [str(item)[:5] for item in cluster.get("times") or []]
|
|
if stamp in times:
|
|
return cluster
|
|
return None
|
|
|
|
|
|
def metrics_from_public(
|
|
public: Sequence[dict[str, Any]],
|
|
true_time: str,
|
|
window_times: Sequence[str],
|
|
) -> dict[str, Any]:
|
|
if not public:
|
|
return {
|
|
"top1_hit": False,
|
|
"coverage": False,
|
|
"width": None,
|
|
"tie": False,
|
|
"entropy": 0.0,
|
|
"truth_squeezed": True,
|
|
"too_narrow": False,
|
|
"leader_count": 0,
|
|
"public_count": 0,
|
|
"true_cluster_rank": None,
|
|
"true_cluster_size": 0,
|
|
}
|
|
scores = [float(row.get("score") or 0) for row in public]
|
|
best = max(scores)
|
|
leaders = [row for row in public if abs(float(row.get("score") or 0) - best) <= 1e-9]
|
|
delivered = []
|
|
seen: set[str] = set()
|
|
for row in public:
|
|
for time in row.get("cluster_times") or [str(row.get("time"))[:5]]:
|
|
stamp = str(time)[:5]
|
|
if stamp not in seen:
|
|
seen.add(stamp)
|
|
delivered.append(stamp)
|
|
true = str(true_time)[:5]
|
|
true_in_leaders = any(true in (row.get("cluster_times") or [str(row.get("time"))[:5]]) for row in leaders)
|
|
if not true_in_leaders:
|
|
true_in_leaders = any(str(row.get("time"))[:5] == true for row in leaders)
|
|
rank = None
|
|
size = 0
|
|
ordered = sorted(public, key=lambda row: (-float(row.get("score") or 0), str(row.get("time"))))
|
|
for index, row in enumerate(ordered, start=1):
|
|
members = [str(item)[:5] for item in (row.get("cluster_times") or [str(row.get("time"))[:5]])]
|
|
if true in members:
|
|
rank = index
|
|
size = len(members)
|
|
break
|
|
width = range_width(delivered)
|
|
second = sorted({round(float(row.get("score") or 0), 4) for row in public}, reverse=True)
|
|
tied = len(leaders) >= 2 or (len(second) >= 2 and abs(second[0] - second[1]) <= 1e-9)
|
|
return {
|
|
"top1_hit": true_in_leaders,
|
|
"coverage": true in seen,
|
|
"width": width,
|
|
"tie": tied,
|
|
"entropy": shannon_entropy(max(score, 0.0) for score in scores),
|
|
"truth_squeezed": true not in seen,
|
|
"too_narrow": width is not None and width <= NARROW_WIDTH and true in seen,
|
|
"leader_count": len(leaders),
|
|
"public_count": len(public),
|
|
"true_cluster_rank": rank,
|
|
"true_cluster_size": size,
|
|
"window_width": range_width(list(window_times)),
|
|
}
|