docs(research): measure minute-resolution scoring; no variant clears all radii
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.
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
"""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)),
|
||||
}
|
||||
Reference in New Issue
Block a user