Skip to content

Majority rule with willingness and zealots

The majority rule with willingness, where zealots of willingness 0 never change their opinion.

  • Name: willing_majority
  • Field: social/opinion_dynamics
  • Formalism: agent-based system
  • References: Jensen 2023

Select this system

from skcomplex.systems import create_simulation, create_system, describe

system = create_system('willing_majority')
entry = describe('willing_majority')
trajectory = create_simulation('willing_majority').prepare().run().trajectory_

Factory

@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'majority', 'zealots', 'consensus'),
    references=('Jensen 2023',),
    defaults={'until': 50},
)
def willing_majority(
    n_agents: int = 200,
    *,
    group_size: int = 3,
    zealots: float = 0.0,
    zealot_opinion: float | None = None,
    willingness: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the majority rule with willingness, where zealots of willingness 0 never change their opinion.

    Every agent holds opinion `A` or `B`, drawn with probability 1/2, and a willingness `w_i`. An update chooses an
    agent and an odd number `n` of other agents at random, and the agent adopts the majority opinion of the group with
    the probability `w_i` (Jensen 2023 section 11.5, Exercise 4). Every agent has `w_i = willingness`, except a
    fraction `zealots` of them with `w_i = 0`. With every agent willing, the population reaches consensus. Zealots of
    both opinions prevent it, while a small group of zealots of one opinion, `zealot_opinion`, wins over everyone.

    Args:
        n_agents:
            The number of agents.

        group_size:
            The odd number `n` of other agents an agent consults.

        zealots:
            The fraction `f` of agents with willingness 0.

        zealot_opinion:
            The opinion, 1 for `A` or 0 for `B`, every zealot starts with, or `None` for random opinions.

        willingness:
            The willingness of the other agents.

        random_state:
            The seed or generator of the opinions and of the zealots.

    Returns:
        system:
            The system, with the group `voters`.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import willing_majority
        >>> willing_majority(30).elements[0].prepare().size_
        30
    """
    rng = np.random.default_rng(random_state)
    opinions = rng.integers(2, size=n_agents).astype(float)
    stubborn = rng.random(n_agents) < zealots
    if zealot_opinion is not None:
        opinions[stubborn] = zealot_opinion
    voters = GroupElement(
        'voters',
        element=WillingVoterElement,
        size=n_agents,
        values={'opinion': opinions, 'willingness': np.where(stubborn, 0.0, willingness)},
    )
    majority = WillingMajorityInteraction('majority', target='voters', group_size=group_size)
    return System(elements=[voters], interactions=[majority])

Components

Elements:

  • WillingVoterElement, from Element

Interactions:

  • WillingMajorityInteraction, from Interaction
class WillingVoterElement(Element):
    """Voter with a binary opinion and a willingness to follow the majority, a zealot at willingness 0.

    Args:
        label:
            The label of the voter or the group.

        visual:
            How the agent is drawn, or `None` for `default_visual_`.

        opinion:
            The opinion, 0 for `B` or 1 for `A`.

        willingness:
            The probability that the voter adopts the majority opinion of the group it consults.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import WillingVoterElement
        >>> WillingVoterElement('voter').willingness
        1.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}),
        *,
        opinion: Annotated[float, StateSpec(domain=NN)] = 0.0,
        willingness: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 1.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.opinion = opinion
        self.willingness = willingness

    @classmethod
    def _get_view(cls) -> type[WillingVoterView]:
        """Return the class of the views of the agents, whose attributes are the states."""
        return WillingVoterView

class WillingMajorityInteraction(Interaction):
    """Majority rule with willingness, where a random agent adopts the majority opinion of a random group.

    Every step has one update per agent. An update chooses an agent uniformly, then an odd number `group_size` of
    other agents uniformly, and the agent adopts the majority opinion of the group with the probability of its
    willingness.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the group of voters.

        group_size:
            The odd number of other agents an agent consults.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import WillingMajorityInteraction
        >>> WillingMajorityInteraction('majority', target='voters').group_size
        3
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        group_size: Annotated[int, ParameterSpec(domain=NN)] = 3,
    ) -> None:
        super().__init__(label)
        self.target = target
        self.group_size = group_size

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the majority rule.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return [RuleLaw(self.follow)]

    def follow(self, system: SystemView) -> None:
        """Update as many random agents as there are agents, each by the majority of a random group.

        Args:
            system:
                The system.
        """
        voters = system.agents(self.target, WillingVoterView)
        size = len(voters)
        group_size = int(self.group_size)
        if size <= group_size:
            return
        opinions = np.array([voter.opinion for voter in voters])
        willingness = np.array([voter.willingness for voter in voters])
        rng = system.rng
        for chosen in rng.integers(size, size=size).tolist():
            others = rng.choice(size - 1, group_size, replace=False)
            others = others + (others >= chosen)
            if rng.random() < willingness[chosen]:
                opinions[chosen] = float(2 * opinions[others].sum() > group_size)
        for voter, opinion in zip(voters, opinions.tolist(), strict=True):
            voter.opinion = opinion