Skip to content

Mandelbrot least-effort word frequencies

The word frequencies that minimise the mean cost of a language over its entropy, a power law of rank.

  • Name: mandelbrot_least_effort
  • Field: social/language
  • Formalism: static solution
  • References: Giordano 2024, arXiv 2407.02181; Mandelbrot 1953

Factory

@register_model(
    field='social/language',
    tags=('power law', 'least effort', 'entropy', 'static'),
    references=('Giordano 2024, arXiv 2407.02181', 'Mandelbrot 1953'),
)
def mandelbrot_least_effort(n_words: int = 200) -> System:
    """Return the word frequencies that minimise the mean cost of a language over its entropy, a power law of rank.

    A word of rank `i` costs `log(i + 1)`, the length of its code, so the probabilities are `(i + 1)^-lambda`, where
    `lambda` makes them sum to 1 (Giordano 2024 §6, after Mandelbrot 1953).

    Args:
        n_words:
            The number of words.

    Returns:
        system:
            The model, with the states `language.exponent`, `words.cost`, and `words.probability`.

    Examples:
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.social.language import mandelbrot_least_effort
        >>> trajectory = Simulation(mandelbrot_least_effort(50)).check().prepare().run(1.0).trajectory_
        >>> round(float(trajectory.values('words.probability')[0].sum()), 6)
        1.0
    """
    costs = np.log(np.arange(1, n_words + 1) + 1.0)
    visual = VisualSpec('histograms', {'color_by': 'probability'})
    words = GroupElement('words', element=WordElement, size=n_words, values={'cost': costs}, visual=visual)
    effort = LeastEffortInteraction('effort', target='language', words='words')
    return System(elements=[words, LanguageElement('language')], interactions=[effort], time=StaticTime())

Components

class WordElement(Element):
    """Word with a quenched cost and the probability of its use.

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

        cost:
            The cost of the word.

        probability:
            The initial guess of the probability, which the equation replaces.

    Examples:
        >>> from skcomplex.models.social.language import WordElement
        >>> WordElement('word', cost=1.0).cost
        1.0
    """

    def __init__(
        self,
        label: str,
        cost: Annotated[float, StateSpec()] = 1.0,
        probability: Annotated[float, StateSpec()] = 0.0,
    ) -> None:
        super().__init__(label)
        self.cost = cost
        self.probability = probability

class LanguageElement(Element):
    """Language with the exponent `lambda` of its word frequencies.

    Args:
        label:
            The label of the language.

        exponent:
            The initial guess of the exponent, from which it is solved.

    Examples:
        >>> from skcomplex.models.social.language import LanguageElement
        >>> LanguageElement('language').exponent
        1.0
    """

    def __init__(self, label: str, exponent: Annotated[float, StateSpec()] = 1.0) -> None:
        super().__init__(label)
        self.exponent = exponent

class LeastEffortInteraction(Interaction):
    """Probabilities of words that minimise the ratio of the mean cost `C` to the entropy `H`.

    The probabilities that are stationary for `C / H` on the simplex are `p_i = exp(-lambda c_i) / Z`, for which
    `H = lambda C + ln Z`. The ratio is stationary in `lambda` where `H = lambda C`, that is where `ln Z = 0`, which
    solves the exponent. The sum reduces in log space.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the element that holds `exponent`.

        words:
            The path of the group of words, which hold `cost` and `probability`.

    Examples:
        >>> from skcomplex.models.social.language import LeastEffortInteraction
        >>> LeastEffortInteraction('effort', target='language', words='words').words
        'words'
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        words: Annotated[str, RoleSpec()],
    ) -> None:
        super().__init__(label)
        self.target = target
        self.words = words

    def _get_laws(self, s: SimpleNamespace) -> Sequence[BasePart]:
        """Return the equations of the exponent and of the probabilities.

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

        Returns:
            laws:
                The stationarity of the ratio and the probability of a word.
        """
        weight = -s.target.exponent * s.words.cost
        log_partition = log_sum_exp(weight, over=s.words)
        return [
            EquationLaw(log_partition == 0, target=s.target.exponent),
            EquationLaw(s.words.probability == exp(weight - log_partition), target=s.words.probability),
        ]