feat: shuffle test for causal echo (Anagnostopoulos 2008, via ColonistOne)
This commit is contained in:
parent
edc7f28dfc
commit
22f9ce405c
|
|
@ -284,6 +284,71 @@ def score_echo(messages: list[dict], concept_window_seconds: float = 604800,
|
||||||
return node_echo
|
return node_echo
|
||||||
|
|
||||||
|
|
||||||
|
def shuffle_test(messages: list[dict], n_shuffles: int = 100,
|
||||||
|
use_idf: bool = True) -> dict:
|
||||||
|
"""
|
||||||
|
Shuffle test for causal echo (Anagnostopoulos, Kumar & Mahdian 2008).
|
||||||
|
Permutes timestamps while holding network fixed.
|
||||||
|
If echo scores survive shuffling, they measured correlation (shared
|
||||||
|
environment) not diffusion (causal influence).
|
||||||
|
|
||||||
|
Returns dict with:
|
||||||
|
- observed: {node: echo_coeff} from actual data
|
||||||
|
- mean_shuffled: {node: mean echo across shuffles}
|
||||||
|
- z_scores: {node: (observed - mean_shuffled) / std_shuffled}
|
||||||
|
- significant: {node: bool} where |z| > 2.0
|
||||||
|
|
||||||
|
Cited: ColonistOne (Colony, 2026-07-28) pointed to this method.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Observed echo
|
||||||
|
observed = score_echo(messages, use_idf=use_idf)
|
||||||
|
|
||||||
|
# Run shuffles
|
||||||
|
shuffled_scores = defaultdict(list)
|
||||||
|
timestamps = [m.get("timestamp", 0) for m in messages]
|
||||||
|
|
||||||
|
for _ in range(n_shuffles):
|
||||||
|
# Permute timestamps, keep everything else
|
||||||
|
perm = timestamps.copy()
|
||||||
|
random.shuffle(perm)
|
||||||
|
shuffled_msgs = []
|
||||||
|
for i, m in enumerate(messages):
|
||||||
|
sm = m.copy()
|
||||||
|
sm["timestamp"] = perm[i]
|
||||||
|
shuffled_msgs.append(sm)
|
||||||
|
|
||||||
|
shuffled_echo = score_echo(shuffled_msgs, use_idf=use_idf)
|
||||||
|
all_nodes = set(observed) | set(shuffled_echo)
|
||||||
|
for node in all_nodes:
|
||||||
|
shuffled_scores[node].append(shuffled_echo.get(node, 0.0))
|
||||||
|
|
||||||
|
# Compute z-scores
|
||||||
|
result = {
|
||||||
|
"observed": observed,
|
||||||
|
"mean_shuffled": {},
|
||||||
|
"z_scores": {},
|
||||||
|
"significant": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for node in set(observed) | set(shuffled_scores):
|
||||||
|
scores = shuffled_scores.get(node, [0.0])
|
||||||
|
mean_s = sum(scores) / len(scores)
|
||||||
|
std_s = (sum((s - mean_s)**2 for s in scores) / len(scores)) ** 0.5
|
||||||
|
obs = observed.get(node, 0.0)
|
||||||
|
|
||||||
|
result["mean_shuffled"][node] = round(mean_s, 4)
|
||||||
|
if std_s > 0:
|
||||||
|
z = (obs - mean_s) / std_s
|
||||||
|
else:
|
||||||
|
z = 0.0 if obs == mean_s else float('inf')
|
||||||
|
result["z_scores"][node] = round(z, 4)
|
||||||
|
result["significant"][node] = abs(z) > 2.0
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
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