diff --git a/elsim/_docstrings.py b/elsim/_docstrings.py new file mode 100644 index 00000000..f2f277d6 --- /dev/null +++ b/elsim/_docstrings.py @@ -0,0 +1,26 @@ +"""Shared :mod:`docrep` snippets for recurring NumPy-style parameter docs.""" + +from docrep import DocstringProcessor + +docstrings = DocstringProcessor() + + +@docstrings.get_sections(base='utilities_2d', sections=['Parameters']) +@docstrings.dedent +def _utilities_2d_param_doc(): + """ + Shared documentation for the ``utilities`` parameter. + + Parameters + ---------- + utilities : array_like + A 2D collection of utilities. + + Rows represent voters, and columns represent candidate IDs. + Higher utility numbers mean greater approval of that candidate by that + voter. + """ + pass + + +docstrings.keep_params('utilities_2d.parameters', 'utilities') diff --git a/elsim/elections/elections.py b/elsim/elections/elections.py index 623876e6..2fcc8c81 100644 --- a/elsim/elections/elections.py +++ b/elsim/elections/elections.py @@ -3,6 +3,7 @@ import numpy as np from scipy.spatial.distance import cdist as _cdist +from elsim._docstrings import docstrings from elsim.strategies import honest_rankings as _honest_rankings elections_rng = np.random.default_rng() @@ -30,16 +31,11 @@ def _check_random_state(seed): 'numpy.random.Generator instance') -def random_utilities(n_voters, n_cands, random_state=None): +@docstrings.get_sections(base='election_common', sections=['Parameters']) +@docstrings.dedent +def _election_common_param_doc(): """ - Generate utilities using the impartial culture / random society model. - - The random society [1]_ or random uniform utilities [2]_ model selects - independent candidate utilities for each voter from a uniform distribution - in the interval [0, 1). - - This model is unrealistic, but is commonly used because it has some - worst-case properties and is comparable between researchers. [3]_ + Shared parameter documentation for election generators. Parameters ---------- @@ -54,6 +50,29 @@ def random_utilities(n_voters, n_cands, random_state=None): If None (default), an existing Generator is used. If `random_state` is already a Generator instance, then that object is used. + """ + pass + + +docstrings.keep_params('election_common.parameters', 'n_voters', 'n_cands') +docstrings.keep_params('election_common.parameters', 'random_state') + + +@docstrings.dedent +def random_utilities(n_voters, n_cands, random_state=None): + """ + Generate utilities using the impartial culture / random society model. + + The random society [1]_ or random uniform utilities [2]_ model selects + independent candidate utilities for each voter from a uniform distribution + in the interval [0, 1). + + This model is unrealistic, but is commonly used because it has some + worst-case properties and is comparable between researchers. [3]_ + + Parameters + ---------- + %(election_common.parameters)s Returns ------- @@ -92,6 +111,7 @@ def random_utilities(n_voters, n_cands, random_state=None): return rng.random((n_voters, n_cands)) +@docstrings.dedent def impartial_culture(n_voters, n_cands, random_state=None): """ Generate ranked ballots using the impartial culture / random society model. @@ -104,17 +124,7 @@ def impartial_culture(n_voters, n_cands, random_state=None): Parameters ---------- - n_voters : int - Number of voters - n_cands : int - Number of candidates - random_state : {None, int, np.random.Generator}, optional - Initializes the random number generator. If `random_state` is int, a - new Generator instance is used, seeded with its value. (If the same - int is given twice, the function will return the same values.) - If None (default), an existing Generator is used. - If `random_state` is already a Generator instance, then - that object is used. + %(election_common.parameters)s Returns ------- @@ -164,6 +174,7 @@ def impartial_culture(n_voters, n_cands, random_state=None): return rankings +@docstrings.dedent def normal_electorate(n_voters, n_cands, dims=2, corr=0.0, disp=1.0, random_state=None): """ @@ -171,10 +182,7 @@ def normal_electorate(n_voters, n_cands, dims=2, corr=0.0, disp=1.0, Parameters ---------- - n_voters : int - Number of voters - n_cands : int - Number of candidates + %(election_common.parameters.n_voters|n_cands)s dims : int Number of dimensions corr : float @@ -184,13 +192,7 @@ def normal_electorate(n_voters, n_cands, dims=2, corr=0.0, disp=1.0, standard deviations. For example, 1.0 means they are distributed by the same amount, while 0.5 means that candidates are more tightly concentrated than voters. - random_state : {None, int, np.random.Generator}, optional - Initializes the random number generator. If `random_state` is int, a - new Generator instance is used, seeded with its value. (If the same - int is given twice, the function will return the same values.) - If None (default), an existing Generator is used. - If `random_state` is already a Generator instance, then - that object is used. + %(election_common.parameters.random_state)s Returns ------- diff --git a/elsim/methods/utility_winner.py b/elsim/methods/utility_winner.py index aef0619f..35a0dc5b 100644 --- a/elsim/methods/utility_winner.py +++ b/elsim/methods/utility_winner.py @@ -1,13 +1,14 @@ import numpy as np -from elsim.methods._common import (_all_indices, _get_tiebreak, _no_tiebreak, - _order_tiebreak_keep, _random_tiebreak) +from elsim._docstrings import docstrings +from elsim.methods._common import _all_indices, _get_tiebreak, _no_tiebreak, _order_tiebreak_keep, _random_tiebreak _tiebreak_map = {'order': _order_tiebreak_keep, 'random': _random_tiebreak, None: _no_tiebreak} +@docstrings.dedent def utility_winner(utilities, tiebreaker=None): """ Find the utilitarian winner of an election. (Dummy "election method"). @@ -17,12 +18,7 @@ def utility_winner(utilities, tiebreaker=None): Parameters ---------- - utilities : array_like - A 2D collection of utilities. - - Rows represent voters, and columns represent candidate IDs. - Higher utility numbers mean greater approval of that candidate by that - voter. + %(utilities_2d.parameters.utilities)s tiebreaker : {'random', 'order', None}, optional If there is a tie, and `tiebreaker` is ``'random'``, a random finalist diff --git a/elsim/strategies/strategies.py b/elsim/strategies/strategies.py index e69312d6..a986de1c 100644 --- a/elsim/strategies/strategies.py +++ b/elsim/strategies/strategies.py @@ -1,18 +1,16 @@ import numpy as np +from elsim._docstrings import docstrings + +@docstrings.dedent def honest_rankings(utilities): """ Convert utilities into rankings using honest strategy. Parameters ---------- - utilities : array_like - A 2D collection of utilities. - - Rows represent voters, and columns represent candidate IDs. - Higher utility numbers mean greater approval of that candidate by that - voter. + %(utilities_2d.parameters.utilities)s Returns ------- @@ -57,6 +55,7 @@ def honest_rankings(utilities): return np.argsort(utilities)[:, ::-1].astype(np.uint8) +@docstrings.dedent def honest_normed_scores(utilities, max_score=5): """ Convert utilities into scores using honest (but normalized) strategy. @@ -67,12 +66,7 @@ def honest_normed_scores(utilities, max_score=5): Parameters ---------- - utilities : array_like - A 2D collection of utilities. - - Rows represent voters, and columns represent candidate IDs. - Higher utility numbers mean greater approval of that candidate by that - voter. + %(utilities_2d.parameters.utilities)s max_score : int, optional The highest score on the ballot. If `max_score` = 3, the possible @@ -125,6 +119,7 @@ def honest_normed_scores(utilities, max_score=5): return scores +@docstrings.dedent def approval_optimal(utilities): """ Convert utilities to optimal approval voting ballots. @@ -135,12 +130,7 @@ def approval_optimal(utilities): Parameters ---------- - utilities : array_like - A 2D collection of utilities. - - Rows represent voters, and columns represent candidate IDs. - Higher utility numbers mean greater approval of that candidate by that - voter. + %(utilities_2d.parameters.utilities)s Returns ------- @@ -185,6 +175,7 @@ def approval_optimal(utilities): return approvals +@docstrings.dedent def vote_for_k(utilities, k): """ Convert utilities to approval voting ballots, approving top k candidates. @@ -194,12 +185,7 @@ def vote_for_k(utilities, k): Parameters ---------- - utilities : array_like - A 2D collection of utilities. - - Rows represent voters, and columns represent candidate IDs. - Higher utility numbers mean greater approval of that candidate by that - voter. + %(utilities_2d.parameters.utilities)s k : int or 'half' The number of candidates approved of by each voter, or 'half' to make the number dependent on the number of candidates. If a negative int, diff --git a/pyproject.toml b/pyproject.toml index 235e83d6..c3f5f692 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ + "docrep>=0.3.2", "numpy>=1.17", "scipy", ] diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py new file mode 100644 index 00000000..0475ece2 --- /dev/null +++ b/tests/test_docstrings.py @@ -0,0 +1,21 @@ +"""Tests for :mod:`docrep`-backed shared docstring anchors (see ``elsim._docstrings``).""" + + +def test_utilities_2d_doc_anchor_executes(): + from elsim._docstrings import _utilities_2d_param_doc + + assert _utilities_2d_param_doc() is None + + +def test_election_common_doc_anchor_executes(): + from elsim.elections.elections import _election_common_param_doc + + assert _election_common_param_doc() is None + + +def test_docrep_substitutions_present(): + from elsim.elections import random_utilities + from elsim.strategies import honest_rankings + + assert 'n_voters : int' in (random_utilities.__doc__ or '') + assert 'utilities : array_like' in (honest_rankings.__doc__ or '')