Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/source/analysis_of_procedures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
Analysis of Procedures
==========

Functions that can be used to compare and contrast voting methods.


## Profiles with Different Winners


```{eval-rst}


.. autofunction:: pref_voting.analysis.find_profiles_with_different_winners


```

## Condorcet Efficiency


```{eval-rst}


.. autofunction:: pref_voting.analysis.condorcet_efficiency_data


```

## Resoluteness


```{eval-rst}


.. autofunction:: pref_voting.analysis.resoluteness_data


```

## Axiom Violations

```{eval-rst}

.. autofunction:: pref_voting.analysis.axiom_violations_data


```

## Binomial Confidence Interval

```{eval-rst}

.. autofunction:: pref_voting.analysis.binomial_confidence_interval

```

## Means with Estimated Standard Error


```{eval-rst}


.. autofunction:: pref_voting.analysis.means_with_estimated_standard_error


```
10 changes: 10 additions & 0 deletions docs/source/analysis_of_profiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Analysis of Profiles
==========

Functions that analyze a preference profile independently of any particular voting procedure, measuring structural properties of the electorate.

:::{toctree}
:maxdepth: 2

single_peakedness
:::
66 changes: 3 additions & 63 deletions docs/source/analysis_overview.md
Original file line number Diff line number Diff line change
@@ -1,68 +1,8 @@
Overview
==========

Functions that can be used to compare and contrast voting methods.


## Profiles with Different Winners


```{eval-rst}


.. autofunction:: pref_voting.analysis.find_profiles_with_different_winners


```

## Condorcet Efficiency


```{eval-rst}


.. autofunction:: pref_voting.analysis.condorcet_efficiency_data


```

## Resoluteness


```{eval-rst}


.. autofunction:: pref_voting.analysis.resoluteness_data


```

## Axiom Violations

```{eval-rst}

.. autofunction:: pref_voting.analysis.axiom_violations_data


```

## Binomial Confidence Interval

```{eval-rst}

.. autofunction:: pref_voting.analysis.binomial_confidence_interval

```

## Means with Estimated Standard Error


```{eval-rst}


.. autofunction:: pref_voting.analysis.means_with_estimated_standard_error


```
``pref_voting`` provides two complementary kinds of analysis tools:

- **Analysis of procedures** — functions that analyze collective decision procedures, for example by finding profiles on which different methods disagree, estimating Condorcet efficiency, measuring resoluteness, or counting axiom violations.

- **Analysis of profiles** — functions that analyze a preference profile independently of any particular procedure, measuring structural properties of the electorate such as how close the profile is to satisfying a domain restriction.
7 changes: 7 additions & 0 deletions docs/source/generate_profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,13 @@ We use the [prefsampling](https://comsoc-community.github.io/prefsampling/index.

```

### Generate a Preference-Approval Profile

```{eval-rst}
.. autofunction:: pref_voting.generate_profiles.generate_pref_approval_profile

```

## Enumerating profiles

### Enumerate anonymous profiles
Expand Down
4 changes: 3 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ Contents
:caption: Analysis

analysis_overview

analysis_of_procedures
analysis_of_profiles


Index
----------------------
Expand Down
36 changes: 36 additions & 0 deletions docs/source/single_peakedness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Single-Peakedness
==========

Functions to analyze single-peakedness of preference profiles.

A preference profile is *single-peaked* with respect to an axis (linear order over the candidates) if every voter has a unique most-preferred candidate (peak) and the voter's preferences decrease monotonically in both directions from the peak along the axis.

A profile is *k-maverick single-peaked* with respect to an axis if all but *k* voters are single-peaked with respect to that axis. The minimum *k* over all axes measures how far the profile is from being single-peaked.

**Reference:** Faliszewski, Hemaspaandra & Hemaspaandra (2014), "The complexity of manipulative attacks in nearly single-peaked electorates", *Artificial Intelligence* 207, 69-99. [DOI](https://doi.org/10.1016/j.artint.2013.11.004)

## Checking Single-Peakedness of Individual Rankings

```{eval-rst}

.. autofunction:: pref_voting.single_peakedness.is_single_peaked

```

## Profile-Level Analysis

### Counting Mavericks

```{eval-rst}

.. autofunction:: pref_voting.single_peakedness.num_mavericks

```

### Finding the Minimum k

```{eval-rst}

.. autofunction:: pref_voting.single_peakedness.min_k_maverick_single_peaked

```
130 changes: 127 additions & 3 deletions pref_voting/generate_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
File: gen_profiles.py
Author: Wes Holliday ([email protected]) and Eric Pacuit ([email protected])
Date: December 7, 2020
Updated: May 25, 2025
Updated: April 11, 2026

Functions to generate profiles

Expand All @@ -18,8 +18,8 @@
from scipy.stats import gamma
from itertools import permutations
from pref_voting.helper import weak_compositions, weak_orders

from pref_voting.profiles_with_ties import ProfileWithTies
from pref_voting.pref_grade_profile import PrefGradeProfile
from ortools.linear_solver import pywraplp
from prefsampling.ordinal import impartial, impartial_anonymous, urn, plackett_luce, didi, stratification, single_peaked_conitzer, single_peaked_walsh, single_peaked_circle, single_crossing, euclidean, mallows

Expand Down Expand Up @@ -507,6 +507,130 @@ def generate_profile_with_groups(

return profs[0] if num_profiles == 1 else profs

def generate_pref_approval_profile(
num_candidates,
num_voters,
num_profiles=1,
**kwargs,
):
"""Generate PrefGradeProfile(s) with approval grades using a given probability model.

Each voter's ranking is generated using a probability model for
preferences, and each voter's approval set is generated using a
probability model for approvals. The two models are specified jointly
via the ``probmodel`` keyword argument, which has the form
``"<pref_model>_<approval_model>"``.

Currently supported approval models:

* ``"uniform"`` -- each voter independently selects a uniformly random
cutoff position *k* in {0, 1, ..., num_candidates}, and approves the
candidates ranked in the top *k* positions. The approval set is
therefore upward closed in the voter's preference relation.

The preference model (the part before the last ``"_"``) can be any
model accepted by :func:`get_rankings` (e.g., ``"IC"``, ``"MALLOWS"``,
``"URN"``, ``"MALLOWS-RELPHI"``, ``"single_peaked_conitzer"``, etc.). In the future, joint models
that do not decompose into separate preference and approval components
can be added as single-token ``probmodel`` values (without an
underscore separator).

Args:
num_candidates (int): The number of candidates.
num_voters (int): The number of voters.
num_profiles (int): The number of profiles to generate (default 1).
kwargs: Parameters for the probability model. The ``probmodel``
keyword (default ``"IC_uniform"``) selects the joint model.
The ``seed`` keyword controls reproducibility. Any remaining
keywords are forwarded to the preference model (e.g., ``phi``
for Mallows).

Returns:
PrefGradeProfile or list[PrefGradeProfile]: A single profile if
``num_profiles`` is 1, otherwise a list of profiles.

Examples:

.. code-block:: python

# IC preferences with uniform approval cutoff (default)
pgp = generate_pref_approval_profile(4, 10)

# Mallows preferences with uniform approval cutoff
pgp = generate_pref_approval_profile(4, 10, probmodel="MALLOWS_uniform", phi=0.5)

# URN preferences with uniform approval cutoff
pgp = generate_pref_approval_profile(4, 10, probmodel="URN_uniform", alpha=10)
"""
# --- parse probmodel ---
if 'probmodel' in kwargs:
probmodel = kwargs.pop('probmodel')
elif 'probmod' in kwargs:
probmodel = kwargs.pop('probmod')
else:
probmodel = "IC_uniform"

# Split into preference model and approval model at the last underscore
if "_" in probmodel:
pref_model, approval_model = probmodel.rsplit("_", 1)
else:
# Future: joint models that don't decompose
raise ValueError(
f"Unrecognized joint probability model: '{probmodel}'. "
f"Expected a compound model of the form '<pref_model>_<approval_model>' "
f"(e.g., 'IC_uniform', 'MALLOWS_uniform')."
)

seed = kwargs.pop('seed', None)
rng = np.random.default_rng(seed)

grades = [0, 1]
gmap = {0: "Not Approved", 1: "Approved"}

profiles = []
for _ in range(num_profiles):
ranking_kwargs = dict(kwargs)
ranking_kwargs['probmodel'] = pref_model
if seed is not None:
ranking_kwargs['seed'] = seed if num_profiles == 1 else int(
rng.integers(0, np.iinfo(np.int64).max)
)

rankings = get_rankings(num_candidates, num_voters, **ranking_kwargs)

ranking_dicts = []
grade_maps = []

for voter_ranking in rankings:
# voter_ranking: list of candidates from most to least preferred
rdict = {int(c): pos + 1 for pos, c in enumerate(voter_ranking)}
ranking_dicts.append(rdict)

if approval_model == "uniform":
# Approve top k candidates, k uniform in {0, ..., num_candidates}
cutoff = int(rng.integers(0, num_candidates + 1))
else:
raise ValueError(
f"Unrecognized approval model: '{approval_model}'. "
f"Currently supported: 'uniform'."
)

gdict = {}
for pos, c in enumerate(voter_ranking):
gdict[int(c)] = 1 if pos < cutoff else 0
grade_maps.append(gdict)

profiles.append(
PrefGradeProfile(
ranking_dicts,
grade_maps,
grades,
gmap=gmap,
)
)

return profiles[0] if num_profiles == 1 else profiles

####
# Enumerating profiles
####
Expand Down Expand Up @@ -798,4 +922,4 @@ def minimal_profile_from_edge_order(cands, edge_order):
print("ERROR: Found non integer, ", v.solution_value())
return None

return Profile(_ranks, rcounts = _rcounts)
return Profile(_ranks, rcounts = _rcounts)
Loading
Loading