Rumor vs Counterwave
Competing rumor and fact-check messages racing across a social network.
Level:Intermediate
FAQ
- When is the fact-check counterwave seeded?
- At the configured counter_delay time the simulation introduces counter_seed nodes spreading the correction.
- How does rumor propagation stop?
- Each day rumor holders may forget due to rumor_decay or switch to the correction when contacted.
- Which metrics summarize spread?
- The result object reports peak_rumor, correction_coverage and basic reproduction numbers for both waves.
simulation.py
Viral misinformation vs fact-check counterwave
Two memes race across a social graph. A rumor starts first, hopping from account to account. After a delay, a fact-check counterwave begins spreading through the same network. Each has its own forwarding probability and a chance to fade from memory each day. When the correction reaches a user they drop the rumor if they were spreading it.
Probes track how widely each message propagates. At the end we report:
- Peak rumor reach
- Correction coverage percentage
- Time gap between rumor peak and when half the network holds the correction
- The basic reproduction number R0 for both rumor and correction
from tys import probe, progress
def simulate(cfg: dict):
"""Simulate competing contagions on a random social graph."""
import random
import simpy
env = simpy.Environment()
rng = random.Random(cfg.get("seed", 12345))
Parameters
n = cfg["num_nodes"] # number of users in the network
degree = cfg["avg_degree"] # average connections per user
rumor_p = cfg["rumor_p"] # chance a rumor holder forwards to each neighbour
rumor_decay = cfg["rumor_decay"] # daily chance a rumor fades
correction_p = cfg["correction_p"] # chance a correction holder forwards
correction_decay = cfg["correction_decay"]
counter_delay = cfg["counter_delay"] # days before fact-check seeding
rumor_seed = cfg["rumor_seed"] # initial rumor carriers
counter_seed = cfg["counter_seed"] # initial correction carriers
sim_time = cfg["sim_time"]
Build an undirected random graph with roughly the desired degree.
neighbors = {i: set() for i in range(n)}
for i in range(n):
while len(neighbors[i]) < degree:
j = rng.randrange(n)
if j != i and j not in neighbors[i]:
neighbors[i].add(j)
neighbors[j].add(i)
rumor = set(rng.sample(range(n), rumor_seed))
correction = set()
initial_rumor = len(rumor)
initial_correction = counter_seed
rumor_spread = 0
correction_spread = 0
rumor_peak = len(rumor)
rumor_peak_time = 0
correction_half_time = None
done = env.event()
def dynamics():
nonlocal rumor, correction, rumor_peak, rumor_peak_time, correction_half_time
nonlocal rumor_spread, correction_spread
for t in range(sim_time):
new_rumor = set()
new_correction = set()
Launch the counterwave after the configured delay
if t == counter_delay:
seeds = rng.sample([x for x in range(n) if x not in correction], counter_seed)
new_correction.update(seeds)
Spread rumor
for node in list(rumor):
for neigh in neighbors[node]:
if neigh not in rumor and neigh not in correction:
if rng.random() < rumor_p:
new_rumor.add(neigh)
rumor_spread += 1
if rng.random() < rumor_decay:
rumor.remove(node)
Spread correction
for node in list(correction):
for neigh in neighbors[node]:
if neigh not in correction:
if neigh in rumor:
rumor.discard(neigh)
if rng.random() < correction_p:
new_correction.add(neigh)
correction_spread += 1
if rng.random() < correction_decay:
correction.remove(node)
rumor.update(new_rumor - correction)
correction.update(new_correction)
probe("rumor", env.now, len(rumor))
probe("correction", env.now, len(correction))
if len(rumor) > rumor_peak:
rumor_peak = len(rumor)
rumor_peak_time = t
if correction_half_time is None and len(correction) >= n / 2:
correction_half_time = t
progress(int(100 * t / sim_time))
yield env.timeout(1)
progress(100)
final_coverage = len(correction) / n * 100
time_gap = None
if correction_half_time is not None:
time_gap = correction_half_time - rumor_peak_time
done.succeed({
"peak_rumor": rumor_peak,
"correction_coverage": final_coverage,
"time_gap": time_gap,
"R0_rumor": rumor_spread / initial_rumor if initial_rumor else 0,
"R0_correction": correction_spread / initial_correction if initial_correction else 0,
})
env.process(dynamics())
env.run(until=done)
return done.value
def requirements():
return {
"builtin": ["micropip", "pyyaml"],
"external": ["simpy==4.1.1"],
}
Default.yaml
num_nodes: 200
avg_degree: 6
rumor_seed: 5
counter_seed: 5
rumor_p: 0.3
rumor_decay: 0.05
correction_p: 0.25
correction_decay: 0.05
counter_delay: 5
sim_time: 50
seed: 12345
Charts (Default)
Final Results (Default)
Metric | Value |
---|---|
peak_rumor | 196.00 |
correction_coverage | 98.00 |
time_gap | 4.00 |
R0_rumor | 121.00 |
R0_correction | 182.80 |