Hegselmann-Krause
The Hegselmann-Krause model, whose opinions freeze within a few steps into clusters about 2 ε apart.
- Name:
hegselmann_krause - Field:
social/opinion_dynamics - Formalism: discrete map
- References: Hegselmann and Krause 2002; Castellano, Fortunato, and Loreto 2009
Factory
@register_model(
field='social/opinion_dynamics',
tags=('opinion', 'bounded confidence', 'clusters', 'network', 'synchronous'),
references=('Hegselmann and Krause 2002', 'Castellano, Fortunato, and Loreto 2009'),
defaults={'until': 30},
)
def hegselmann_krause(
n_agents: int = 200,
*,
confidence: float = 0.15,
graph: nx.Graph | None = None,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return the Hegselmann-Krause model, whose opinions freeze within a few steps into clusters about `2 ε` apart.
Every step every agent moves at once to the mean opinion of its neighbours, itself included, whose opinions lie
within the confidence `ε` of its own. On the complete graph the opinions settle into about `1 / (2 ε)` clusters.
Args:
n_agents:
The number of agents, the nodes of the complete graph when no graph is given.
confidence:
The confidence `ε`.
graph:
The network of the agents, or `None` for the complete graph.
random_state:
The seed or generator of the uniform initial opinions.
Returns:
system:
The model, with the group `people` on the nodes of the network.
Examples:
>>> import numpy as np
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.social.opinion_dynamics import hegselmann_krause
>>> trajectory = Simulation(
... hegselmann_krause(50, confidence=0.3), random_state=0,
... ).check().prepare().run(until=20).trajectory_
>>> len(np.unique(np.round(trajectory.values('people.opinion')[-1], 6))) <= 3
True
"""
space = NetworkSpace(graph if graph is not None else nx.complete_graph(n_agents)).check().prepare()
opinions = np.random.default_rng(random_state).random(space.size_)
people = LatticeElement(
'people',
element=ConfidentElement,
space=space,
values={'opinion': opinions, 'confidence': confidence},
)
coupling = ConfidenceCouplingInteraction('compromise', target='people')
return System(elements=[people], interactions=[coupling], time=DiscreteTime())
Components
class ConfidentElement(Element):
"""Agent of the Hegselmann-Krause model, with an opinion and the confidence within which it trusts neighbours.
`ConfidenceCouplingInteraction` moves every agent each step to the mean opinion of the neighbours, itself
included, whose opinions differ from its own by less than `confidence`.
Args:
label:
The label of the agent or group.
visual:
How the agent is drawn, or `None` for `default_visual_`.
opinion:
The opinion, between 0 and 1.
confidence:
The largest opinion difference of a neighbour an agent still listens to.
Examples:
>>> from skcomplex.models.social.opinion_dynamics import ConfidentElement
>>> ConfidentElement('people', confidence=0.1).confidence
0.1
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = VisualSpec('histograms', {'color_range': (0.0, 1.0), 'histogram_first': True}),
*,
opinion: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
confidence: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
) -> None:
super().__init__(label, visual=visual)
self.opinion = opinion
self.confidence = confidence
class ConfidenceCouplingInteraction(CouplingInteraction):
"""Hegselmann-Krause coupling, the move of every agent to the mean opinion of the neighbours within its confidence.
Every step every agent takes at once the mean opinion of its neighbours, itself included, whose opinions differ
from its own by less than its parameter `confidence`. It is the map form of coupling with the normalized mean over
the neighbours of the network weighted by the indicator of the confidence.
Args:
label:
The label of the interaction, unique among the parts of its system.
target:
The path of the group of agents, on the nodes of a network.
state:
The name of the state that holds the opinions.
Examples:
>>> import networkx as nx
>>> import numpy as np
>>> from skcomplex.core.elements import LatticeElement
>>> from skcomplex.core.spaces import DiscreteTime, NetworkSpace
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.social.opinion_dynamics import ConfidenceCouplingInteraction, ConfidentElement
>>> people = LatticeElement(
... 'people',
... element=ConfidentElement,
... space=NetworkSpace(nx.complete_graph(3)),
... values={'opinion': [0.1, 0.2, 0.9], 'confidence': 0.2},
... )
>>> coupling = ConfidenceCouplingInteraction('compromise', target='people')
>>> system = System(elements=[people], interactions=[coupling], time=DiscreteTime())
>>> opinions = Simulation(system).check().prepare().run(until=1).trajectory_.values('people.opinion')
>>> np.round(opinions[-1], 6).tolist()
[0.15, 0.15, 0.9]
"""
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], state: str = 'opinion') -> None:
super().__init__(
label,
target=target,
read=state,
write=state,
function=self._get_opinion,
reduce='normalized-mean',
weights=self._get_trust,
form='map',
neighborhood=AdjacencyNeighborhood(center=True),
)
self.state = state
def _get_opinion(self, namespace: SimpleNamespace) -> Expression:
"""Return the opinion of the neighbour."""
return getattr(namespace.other, self.state)
def _get_trust(self, namespace: SimpleNamespace) -> Expression:
"""Return 1 for a neighbour whose opinion lies within the confidence of the agent, and 0 otherwise."""
difference = getattr(namespace.other, self.state) - getattr(namespace.member, self.state)
return where(abs(difference) < namespace.member.confidence, 1, 0)
Equations
people.opinion[i]: opinion_people(t + 1) == _neighbours_0
