Autonomous AgentsPrediction715 lines
Social Dynamics Modeling
Quick Summary14 lines
Social dynamics modeling simulates how opinions, behaviors, and information spread through populations. By modeling individuals as agents embedded in social networks, we can predict cascades, polarization, consensus formation, and tipping points. This is essential for forecasting elections, market sentiment, technology adoption, viral content spread, and social movements. ## Key Points 1. Even mild individual preferences (Schelling's 30% threshold) can produce extreme macro-level segregation 2. Bounded confidence models explain polarization: people stop listening to those who disagree, forming isolated clusters 3. Information cascades occur when individuals rationally follow the crowd, potentially leading to collectively wrong outcomes 4. Network topology dramatically affects dynamics: scale-free networks are vulnerable to targeted influence, while small-world networks spread information quickly 5. Complex contagion (requiring social reinforcement) spreads differently than simple contagion and explains why some behaviors are harder to spread than diseases 6. Echo chamber detection requires both structural (network clustering) and opinion (belief homogeneity) analysis 7. Influence maximization can identify the most impactful seed nodes for marketing, public health campaigns, or information operations 8. Social dynamics models bridge the gap between individual behavior and population-level outcomes for forecasting
skilldb get prediction-skills/social-dynamics-modelingFull skill: 715 linesPaste into your CLAUDE.md or agent config
Social Dynamics Modeling
Overview
Social dynamics modeling simulates how opinions, behaviors, and information spread through populations. By modeling individuals as agents embedded in social networks, we can predict cascades, polarization, consensus formation, and tipping points. This is essential for forecasting elections, market sentiment, technology adoption, viral content spread, and social movements.
Opinion Dynamics Models
The DeGroot Model (Consensus)
The simplest opinion dynamics model: agents repeatedly average their opinion with those of their neighbors.
import numpy as np
import networkx as nx
class DeGrootModel:
"""
Agents converge to consensus by averaging neighbor opinions.
Models: committee deliberation, peer influence, social learning.
"""
def __init__(self, n_agents: int, adjacency_matrix: np.ndarray):
self.n = n_agents
# Row-stochastic weight matrix (rows sum to 1)
row_sums = adjacency_matrix.sum(axis=1, keepdims=True)
self.W = adjacency_matrix / np.where(row_sums > 0, row_sums, 1)
self.opinions = np.random.uniform(0, 1, n_agents)
self.history = [self.opinions.copy()]
def step(self):
"""One round of opinion averaging."""
self.opinions = self.W @ self.opinions
self.history.append(self.opinions.copy())
def run(self, steps: int = 100):
"""Run until convergence or max steps."""
for _ in range(steps):
old = self.opinions.copy()
self.step()
if np.max(np.abs(self.opinions - old)) < 1e-8:
break
return self.opinions
def consensus_value(self) -> float:
"""The final consensus value depends on eigenvector centrality."""
return np.mean(self.opinions)
def convergence_speed(self) -> float:
"""Second-largest eigenvalue determines convergence speed."""
eigenvalues = np.sort(np.abs(np.linalg.eigvals(self.W)))[::-1]
if len(eigenvalues) > 1:
return eigenvalues[1] # Closer to 0 = faster convergence
return 0
Bounded Confidence Models
Agents only listen to neighbors whose opinions are "close enough" to their own, modeling ideological filtering:
class DeffuantModel:
"""
Bounded confidence model: agents only interact if their
opinion difference is below a threshold.
Models: echo chambers, filter bubbles, selective exposure.
"""
def __init__(self, n_agents: int, confidence_threshold: float = 0.3,
convergence_rate: float = 0.5):
self.n = n_agents
self.threshold = confidence_threshold
self.mu = convergence_rate
self.opinions = np.random.uniform(0, 1, n_agents)
self.history = [self.opinions.copy()]
def step(self):
"""Randomly pair agents and update if within confidence bound."""
i, j = np.random.choice(self.n, 2, replace=False)
diff = abs(self.opinions[i] - self.opinions[j])
if diff < self.threshold:
# Move opinions closer together
self.opinions[i] += self.mu * (self.opinions[j] - self.opinions[i])
self.opinions[j] += self.mu * (self.opinions[i] - self.opinions[j])
self.history.append(self.opinions.copy())
def run(self, steps: int = 10000):
for _ in range(steps):
self.step()
return self.opinions
def count_clusters(self, cluster_threshold: float = 0.05) -> int:
"""Count the number of distinct opinion clusters."""
sorted_opinions = np.sort(self.opinions)
clusters = 1
for i in range(1, len(sorted_opinions)):
if sorted_opinions[i] - sorted_opinions[i-1] > cluster_threshold:
clusters += 1
return clusters
def phase_diagram(self) -> str:
"""
Predict outcome based on confidence threshold:
- High threshold (>0.5): Consensus (one cluster)
- Medium (0.25-0.5): Moderate pluralism (2-3 clusters)
- Low (<0.25): Extreme pluralism (many clusters)
"""
n_clusters = self.count_clusters()
if n_clusters == 1:
return 'consensus'
elif n_clusters <= 3:
return 'moderate_pluralism'
else:
return 'fragmentation'
The Voter Model
class VoterModel:
"""
Each agent randomly copies the opinion of a random neighbor.
Models: social pressure, conformity, imitation.
Simple but produces interesting dynamics on networks.
"""
def __init__(self, graph: nx.Graph, initial_opinions: dict = None):
self.graph = graph
self.nodes = list(graph.nodes())
if initial_opinions:
self.opinions = initial_opinions
else:
self.opinions = {node: np.random.choice([0, 1]) for node in self.nodes}
self.history = [self.opinions.copy()]
def step(self):
"""One agent copies a random neighbor."""
agent = np.random.choice(self.nodes)
neighbors = list(self.graph.neighbors(agent))
if neighbors:
neighbor = np.random.choice(neighbors)
self.opinions[agent] = self.opinions[neighbor]
self.history.append(self.opinions.copy())
def run(self, steps: int = 10000) -> dict:
for _ in range(steps):
self.step()
# Check for consensus
values = list(self.opinions.values())
if len(set(values)) == 1:
return {'outcome': 'consensus', 'value': values[0], 'steps': _ + 1}
return {
'outcome': 'incomplete',
'distribution': {
0: sum(1 for v in self.opinions.values() if v == 0),
1: sum(1 for v in self.opinions.values() if v == 1)
}
}
def consensus_probability(self, opinion: int) -> float:
"""Probability that the system reaches consensus on given opinion.
On a complete graph, it equals the initial fraction holding that opinion."""
total = len(self.opinions)
count = sum(1 for v in self.opinions.values() if v == opinion)
return count / total
Social Contagion
SIR-Style Information Spread
class InformationSIR:
"""
SIR model adapted for information/rumor spreading.
S = Susceptible (unaware)
I = Infected (spreading)
R = Recovered (aware but no longer spreading)
"""
def __init__(self, graph: nx.Graph, seed_nodes: list,
spread_prob: float = 0.1, recovery_prob: float = 0.05):
self.graph = graph
self.spread_prob = spread_prob
self.recovery_prob = recovery_prob
self.state = {node: 'S' for node in graph.nodes()}
for seed in seed_nodes:
self.state[seed] = 'I'
self.history = [self._snapshot()]
def _snapshot(self) -> dict:
counts = {'S': 0, 'I': 0, 'R': 0}
for state in self.state.values():
counts[state] += 1
return counts
def step(self):
"""One time step of SIR dynamics."""
new_state = self.state.copy()
for node in self.graph.nodes():
if self.state[node] == 'I':
# Try to infect neighbors
for neighbor in self.graph.neighbors(node):
if self.state[neighbor] == 'S':
if np.random.random() < self.spread_prob:
new_state[neighbor] = 'I'
# Try to recover
if np.random.random() < self.recovery_prob:
new_state[node] = 'R'
self.state = new_state
self.history.append(self._snapshot())
def run(self, max_steps: int = 500) -> dict:
for step in range(max_steps):
self.step()
if self.history[-1]['I'] == 0:
break
return {
'total_reached': self.history[-1]['R'],
'peak_infected': max(h['I'] for h in self.history),
'duration': len(self.history),
'reach_fraction': self.history[-1]['R'] / len(self.graph.nodes()),
'timeline': self.history
}
def estimate_R0(self) -> float:
"""Estimate basic reproduction number."""
avg_degree = sum(dict(self.graph.degree()).values()) / len(self.graph)
return self.spread_prob * avg_degree / self.recovery_prob
Complex Contagion
class ComplexContagion:
"""
Unlike simple contagion (one contact sufficient),
complex contagion requires social reinforcement:
multiple neighbors must adopt before you do.
Models: social movements, risky behaviors, costly adoption.
"""
def __init__(self, graph: nx.Graph, threshold_type: str = 'fractional'):
self.graph = graph
self.threshold_type = threshold_type
self.adopted = set()
# Each agent has a personal adoption threshold
self.thresholds = {}
for node in graph.nodes():
if threshold_type == 'fractional':
self.thresholds[node] = np.random.beta(2, 5) # Most need ~20-30% of neighbors
else:
self.thresholds[node] = np.random.geometric(0.3) # Absolute count
self.history = []
def seed(self, initial_adopters: list):
self.adopted = set(initial_adopters)
def step(self) -> int:
"""One cascade step. Returns number of new adoptions."""
new_adoptions = set()
for node in self.graph.nodes():
if node in self.adopted:
continue
neighbors = list(self.graph.neighbors(node))
if not neighbors:
continue
adopted_neighbors = sum(1 for n in neighbors if n in self.adopted)
if self.threshold_type == 'fractional':
fraction = adopted_neighbors / len(neighbors)
if fraction >= self.thresholds[node]:
new_adoptions.add(node)
else:
if adopted_neighbors >= self.thresholds[node]:
new_adoptions.add(node)
self.adopted.update(new_adoptions)
self.history.append(len(self.adopted))
return len(new_adoptions)
def run(self, max_steps: int = 100) -> dict:
for _ in range(max_steps):
new = self.step()
if new == 0:
break
total = len(self.graph.nodes())
return {
'total_adopted': len(self.adopted),
'adoption_rate': len(self.adopted) / total,
'cascade_size': self.history,
'reached_tipping_point': len(self.adopted) / total > 0.5
}
def find_critical_mass(self, n_trials: int = 50) -> float:
"""
Find the minimum seed fraction needed for a cascade
to reach >50% of the population.
"""
fractions = np.linspace(0.01, 0.30, 20)
cascade_rates = []
for frac in fractions:
successes = 0
for _ in range(n_trials):
self.adopted = set()
seed_size = int(frac * len(self.graph.nodes()))
seeds = np.random.choice(list(self.graph.nodes()), seed_size, replace=False)
self.seed(seeds.tolist())
result = self.run()
if result['reached_tipping_point']:
successes += 1
cascade_rates.append(successes / n_trials)
# Find the fraction where cascade probability exceeds 50%
for i, rate in enumerate(cascade_rates):
if rate > 0.5:
return fractions[i]
return fractions[-1]
Cascade Effects and Influence Propagation
Information Cascade Model
class InformationCascade:
"""
Model where rational agents may ignore their private signal
and follow the crowd, leading to herding behavior.
Classic model from Bikhchandani, Hirshleifer, and Welch (1992).
"""
def __init__(self, n_agents: int, signal_quality: float = 0.6):
self.n_agents = n_agents
self.signal_quality = signal_quality
self.true_state = np.random.choice([0, 1]) # True state of the world
self.decisions = []
self.cascade_started = False
self.cascade_start_agent = None
def run(self) -> dict:
for i in range(self.n_agents):
# Private signal (noisy observation of true state)
if np.random.random() < self.signal_quality:
private_signal = self.true_state
else:
private_signal = 1 - self.true_state
# Count previous public decisions
count_0 = sum(1 for d in self.decisions if d == 0)
count_1 = sum(1 for d in self.decisions if d == 1)
# Bayesian rational decision
if count_1 > count_0 + 1:
# Strong evidence for 1: ignore private signal
decision = 1
if not self.cascade_started:
self.cascade_started = True
self.cascade_start_agent = i
elif count_0 > count_1 + 1:
decision = 0
if not self.cascade_started:
self.cascade_started = True
self.cascade_start_agent = i
else:
# Evidence inconclusive: follow private signal
decision = private_signal
self.decisions.append(decision)
final_majority = 1 if sum(self.decisions) > len(self.decisions) / 2 else 0
return {
'true_state': self.true_state,
'majority_decision': final_majority,
'correct': final_majority == self.true_state,
'cascade_started': self.cascade_started,
'cascade_start_agent': self.cascade_start_agent,
'decisions': self.decisions,
'conformity_rate': max(
sum(1 for d in self.decisions if d == 1),
sum(1 for d in self.decisions if d == 0)
) / len(self.decisions)
}
Network Influence Maximization
class InfluenceMaximization:
"""
Find the most influential seed nodes for maximum cascade spread.
Uses greedy algorithm with Monte Carlo simulation.
"""
def __init__(self, graph: nx.Graph, spread_prob: float = 0.1):
self.graph = graph
self.spread_prob = spread_prob
def _simulate_spread(self, seeds: set, n_simulations: int = 100) -> float:
"""Estimate average spread from a seed set."""
total_spread = 0
for _ in range(n_simulations):
active = set(seeds)
newly_active = set(seeds)
while newly_active:
next_active = set()
for node in newly_active:
for neighbor in self.graph.neighbors(node):
if neighbor not in active:
if np.random.random() < self.spread_prob:
next_active.add(neighbor)
active.update(next_active)
newly_active = next_active
total_spread += len(active)
return total_spread / n_simulations
def greedy_selection(self, k: int, n_simulations: int = 100) -> list:
"""Select k seed nodes to maximize influence spread."""
selected = set()
candidates = set(self.graph.nodes())
for _ in range(k):
best_node = None
best_marginal_gain = -1
for candidate in candidates - selected:
trial_seeds = selected | {candidate}
spread = self._simulate_spread(trial_seeds, n_simulations)
current_spread = self._simulate_spread(selected, n_simulations) if selected else 0
marginal_gain = spread - current_spread
if marginal_gain > best_marginal_gain:
best_marginal_gain = marginal_gain
best_node = candidate
if best_node is not None:
selected.add(best_node)
return list(selected)
Schelling Segregation Model
class SchellingModel:
"""
Agents on a grid move if too few neighbors share their type.
Even mild preferences for similarity produce strong segregation.
Key insight: micro-motives != macro-behavior.
"""
def __init__(self, grid_size: int = 50, empty_fraction: float = 0.1,
similarity_threshold: float = 0.3):
self.size = grid_size
self.threshold = similarity_threshold
self.grid = np.zeros((grid_size, grid_size), dtype=int)
# Populate grid: 0=empty, 1=type A, 2=type B
cells = grid_size * grid_size
n_empty = int(cells * empty_fraction)
n_each = (cells - n_empty) // 2
types = [0] * n_empty + [1] * n_each + [2] * (cells - n_empty - n_each)
np.random.shuffle(types)
self.grid = np.array(types).reshape((grid_size, grid_size))
def _get_neighbors(self, r: int, c: int) -> list:
neighbors = []
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue
nr, nc = r + dr, c + dc
if 0 <= nr < self.size and 0 <= nc < self.size:
neighbors.append(self.grid[nr, nc])
return neighbors
def _is_satisfied(self, r: int, c: int) -> bool:
if self.grid[r, c] == 0:
return True
neighbors = [n for n in self._get_neighbors(r, c) if n > 0]
if not neighbors:
return True
same = sum(1 for n in neighbors if n == self.grid[r, c])
return same / len(neighbors) >= self.threshold
def step(self) -> int:
"""Move unsatisfied agents to random empty cells."""
unsatisfied = []
empty = []
for r in range(self.size):
for c in range(self.size):
if self.grid[r, c] == 0:
empty.append((r, c))
elif not self._is_satisfied(r, c):
unsatisfied.append((r, c))
np.random.shuffle(unsatisfied)
np.random.shuffle(empty)
moves = min(len(unsatisfied), len(empty))
for i in range(moves):
ur, uc = unsatisfied[i]
er, ec = empty[i]
self.grid[er, ec] = self.grid[ur, uc]
self.grid[ur, uc] = 0
return moves
def run(self, max_steps: int = 100) -> dict:
segregation_history = []
for step in range(max_steps):
moves = self.step()
seg = self.segregation_index()
segregation_history.append(seg)
if moves == 0:
break
return {
'final_segregation': self.segregation_index(),
'steps_to_equilibrium': step + 1,
'segregation_history': segregation_history
}
def segregation_index(self) -> float:
"""Compute average fraction of same-type neighbors."""
total_same_fraction = 0
count = 0
for r in range(self.size):
for c in range(self.size):
if self.grid[r, c] == 0:
continue
neighbors = [n for n in self._get_neighbors(r, c) if n > 0]
if neighbors:
same = sum(1 for n in neighbors if n == self.grid[r, c])
total_same_fraction += same / len(neighbors)
count += 1
return total_same_fraction / count if count > 0 else 0
Polarization Modeling
Echo Chamber Detection
class EchoChamberDetector:
"""Detect echo chambers in social networks based on opinion and connection patterns."""
def __init__(self, graph: nx.Graph, opinions: dict):
self.graph = graph
self.opinions = opinions
def opinion_homophily(self) -> float:
"""Measure how much more similar connected agents are vs random pairs."""
connected_diffs = []
for u, v in self.graph.edges():
if u in self.opinions and v in self.opinions:
connected_diffs.append(abs(self.opinions[u] - self.opinions[v]))
random_diffs = []
nodes = list(self.opinions.keys())
for _ in range(len(connected_diffs)):
i, j = np.random.choice(len(nodes), 2, replace=False)
random_diffs.append(abs(self.opinions[nodes[i]] - self.opinions[nodes[j]]))
connected_avg = np.mean(connected_diffs) if connected_diffs else 0
random_avg = np.mean(random_diffs) if random_diffs else 0
# Homophily ratio: 0 = no homophily, 1 = complete homophily
return 1 - (connected_avg / random_avg) if random_avg > 0 else 0
def find_echo_chambers(self, min_size: int = 5,
opinion_threshold: float = 0.15) -> list:
"""Identify tightly-connected groups with similar opinions."""
# Find communities
communities = list(nx.community.greedy_modularity_communities(self.graph))
echo_chambers = []
for community in communities:
if len(community) < min_size:
continue
opinions_in_community = [
self.opinions[n] for n in community if n in self.opinions
]
if not opinions_in_community:
continue
opinion_std = np.std(opinions_in_community)
opinion_mean = np.mean(opinions_in_community)
# Echo chamber: tight community with homogeneous opinions
if opinion_std < opinion_threshold:
# Measure insularity (how many edges go outside)
internal_edges = sum(
1 for u, v in self.graph.edges()
if u in community and v in community
)
total_edges = sum(
self.graph.degree(n) for n in community
) / 2
insularity = internal_edges / max(total_edges, 1)
echo_chambers.append({
'size': len(community),
'mean_opinion': opinion_mean,
'opinion_std': opinion_std,
'insularity': insularity,
'is_echo_chamber': opinion_std < opinion_threshold and insularity > 0.7
})
return echo_chambers
def polarization_index(self) -> float:
"""Measure overall network polarization (bimodality of opinions)."""
values = list(self.opinions.values())
# Distance from center
deviations = [abs(v - 0.5) for v in values]
return np.mean(deviations) * 2 # 0 = no polarization, 1 = complete
Prediction Applications
Using Social Dynamics Models for Forecasting
class SocialForecastEngine:
"""Use social dynamics to predict real-world outcomes."""
def __init__(self, network_data: dict, opinion_data: dict):
self.network = self._build_network(network_data)
self.opinions = opinion_data
def predict_election(self, candidate_support: dict,
n_simulations: int = 100) -> dict:
"""Predict election outcome using opinion dynamics simulation."""
results = []
for _ in range(n_simulations):
# Initialize with polling data + noise
initial = {}
for node in self.network.nodes():
base = candidate_support.get('A', 0.5)
noise = np.random.normal(0, 0.1)
initial[node] = np.clip(base + noise, 0, 1)
# Run bounded confidence dynamics
model = DeffuantModel(len(self.network.nodes()), confidence_threshold=0.3)
model.opinions = np.array([initial.get(n, 0.5) for n in self.network.nodes()])
model.run(steps=5000)
# Election result: majority wins
vote_A = np.mean(model.opinions > 0.5)
results.append(vote_A)
return {
'candidate_A_win_probability': np.mean(np.array(results) > 0.5),
'expected_vote_share': np.mean(results),
'uncertainty': np.std(results),
'scenarios': {
'landslide_A': np.mean(np.array(results) > 0.6),
'close_race': np.mean((np.array(results) > 0.45) & (np.array(results) < 0.55)),
'landslide_B': np.mean(np.array(results) < 0.4)
}
}
def _build_network(self, data: dict) -> nx.Graph:
G = nx.Graph()
G.add_edges_from(data.get('edges', []))
return G
Key Takeaways
- Even mild individual preferences (Schelling's 30% threshold) can produce extreme macro-level segregation
- Bounded confidence models explain polarization: people stop listening to those who disagree, forming isolated clusters
- Information cascades occur when individuals rationally follow the crowd, potentially leading to collectively wrong outcomes
- Network topology dramatically affects dynamics: scale-free networks are vulnerable to targeted influence, while small-world networks spread information quickly
- Complex contagion (requiring social reinforcement) spreads differently than simple contagion and explains why some behaviors are harder to spread than diseases
- Echo chamber detection requires both structural (network clustering) and opinion (belief homogeneity) analysis
- Influence maximization can identify the most impactful seed nodes for marketing, public health campaigns, or information operations
- Social dynamics models bridge the gap between individual behavior and population-level outcomes for forecasting
Install this skill directly: skilldb add prediction-skills