Skip to content

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

The hegselmann_krause example

Example

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