add ablation_sensitivity: volume → suppressor/amplifier bilateral test
This commit is contained in:
parent
c990a1ef97
commit
b88cfbdde1
|
|
@ -376,6 +376,68 @@ def shuffle_test(messages: list[dict], n_shuffles: int = 100,
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ablation_sensitivity(messages: list[dict], target: str,
|
||||||
|
n_shuffles: int = 50, use_idf: bool = True) -> dict:
|
||||||
|
"""
|
||||||
|
Ablation test: remove target agent, recompute shuffle test, measure delta.
|
||||||
|
|
||||||
|
Returns dict with:
|
||||||
|
- target: agent removed
|
||||||
|
- target_msg_count: how many messages removed
|
||||||
|
- target_traffic_pct: percentage of corpus removed
|
||||||
|
- deltas: {node: z_ablated - z_full} for all nodes
|
||||||
|
- gainers: nodes whose echo INCREASED (were being suppressed by target)
|
||||||
|
- losers: nodes whose echo DECREASED (were being amplified by target)
|
||||||
|
- verdict: 'suppressor' if mean delta > 0, 'amplifier' if < 0, 'neutral'
|
||||||
|
|
||||||
|
Discovered 2026-07-28: bolt (38% traffic) = systematic suppressor,
|
||||||
|
dispatch (5.6% traffic) = mild amplifier. Crossover threshold TBD.
|
||||||
|
Holocene (Colony) asked the question; ablation answered it.
|
||||||
|
"""
|
||||||
|
# Baseline
|
||||||
|
st_full = shuffle_test(messages, n_shuffles=n_shuffles, use_idf=use_idf)
|
||||||
|
|
||||||
|
# Ablate target
|
||||||
|
ablated = [m for m in messages if m.get("from_id", "") != target]
|
||||||
|
removed = len(messages) - len(ablated)
|
||||||
|
if removed == 0:
|
||||||
|
return {"target": target, "target_msg_count": 0,
|
||||||
|
"error": f"agent '{target}' not found in corpus"}
|
||||||
|
|
||||||
|
st_abl = shuffle_test(ablated, n_shuffles=n_shuffles, use_idf=use_idf)
|
||||||
|
|
||||||
|
z_full = st_full["z_scores"]
|
||||||
|
z_abl = st_abl["z_scores"]
|
||||||
|
|
||||||
|
deltas = {}
|
||||||
|
for node in set(z_full) | set(z_abl):
|
||||||
|
if node == target:
|
||||||
|
continue
|
||||||
|
deltas[node] = round(z_abl.get(node, 0) - z_full.get(node, 0), 4)
|
||||||
|
|
||||||
|
gainers = {k: v for k, v in deltas.items() if v > 2.0}
|
||||||
|
losers = {k: v for k, v in deltas.items() if v < -2.0}
|
||||||
|
|
||||||
|
mean_delta = sum(deltas.values()) / max(len(deltas), 1)
|
||||||
|
if mean_delta > 0.5:
|
||||||
|
verdict = "suppressor"
|
||||||
|
elif mean_delta < -0.5:
|
||||||
|
verdict = "amplifier"
|
||||||
|
else:
|
||||||
|
verdict = "neutral"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"target": target,
|
||||||
|
"target_msg_count": removed,
|
||||||
|
"target_traffic_pct": round(100 * removed / len(messages), 1),
|
||||||
|
"mean_delta": round(mean_delta, 4),
|
||||||
|
"verdict": verdict,
|
||||||
|
"gainers": dict(sorted(gainers.items(), key=lambda x: -x[1])),
|
||||||
|
"losers": dict(sorted(losers.items(), key=lambda x: x[1])),
|
||||||
|
"deltas": dict(sorted(deltas.items(), key=lambda x: -abs(x[1]))),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def detect_shadows(messages: list[dict], now: Optional[float] = None,
|
def detect_shadows(messages: list[dict], now: Optional[float] = None,
|
||||||
silence_days: float = 7.0) -> dict:
|
silence_days: float = 7.0) -> dict:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue