More Classes of Profiles

[1]:
from fractions import Fraction
from matplotlib import pyplot as plt
import poisson_approval as pa

Up to now, we have always used the class of profiles ProfileNoisyDiscrete. The package provides other kinds of profiles.

ProfileHistogram

ProfileHistogram is probably the closest class to ProfileNoisyDiscrete, because both are:

  • Cardinal: the voters have explicit utilities (unlike ProfileOrdinal, cf. below).
  • Continuous: there is no concentration of voters on a specific utility (unlike ProfileDiscrete, cf. below).

A ProfileHistogram can be defined manually (cf. Reference section), but it is more common to use a random factory:

[2]:
rand_profile = pa.RandProfileHistogramGridUniform(
    denominator=100,
    denominator_bins=100,
    n_bins=10
)
profile = rand_profile()
profile
[2]:
<abc: 11/100 [Fraction(9, 100) Fraction(3, 50) Fraction(3, 100) Fraction(1, 50)
 Fraction(21, 100) Fraction(1, 5) Fraction(3, 20) Fraction(1, 10)
 Fraction(1, 25) Fraction(1, 10)], acb: 13/100 [Fraction(3, 100) Fraction(13, 100) Fraction(1, 25) Fraction(2, 25)
 Fraction(21, 100) Fraction(3, 20) Fraction(1, 5) Fraction(3, 100)
 Fraction(13, 100) 0], bac: 13/25 [0 Fraction(2, 25) Fraction(3, 100) Fraction(1, 10) Fraction(3, 50)
 Fraction(2, 25) Fraction(7, 20) Fraction(3, 100) Fraction(1, 25)
 Fraction(23, 100)], bca: 1/100 [Fraction(37, 100) Fraction(1, 50) Fraction(1, 25) Fraction(3, 50)
 Fraction(9, 50) Fraction(19, 100) Fraction(3, 100) Fraction(2, 25)
 Fraction(3, 100) 0], cab: 2/25 [Fraction(4, 25) Fraction(1, 5) Fraction(1, 100) Fraction(13, 100)
 Fraction(1, 50) Fraction(13, 100) Fraction(3, 20) Fraction(9, 100)
 Fraction(1, 100) Fraction(1, 10)], cba: 3/20 [Fraction(1, 100) Fraction(29, 50) Fraction(1, 25) Fraction(7, 50)
 Fraction(1, 100) 0 Fraction(1, 100) Fraction(2, 25) Fraction(7, 100)
 Fraction(3, 50)]> (Condorcet winner: b)

In this example, the share of voters \(abc\) is:

[3]:
profile.abc
[3]:
Fraction(11, 100)

The distribution of their utility for their middle candidate \(b\) is given by a histogram:

[4]:
profile.d_ranking_histogram['abc']
[4]:
array([Fraction(9, 100), Fraction(3, 50), Fraction(3, 100),
       Fraction(1, 50), Fraction(21, 100), Fraction(1, 5),
       Fraction(3, 20), Fraction(1, 10), Fraction(1, 25), Fraction(1, 10)],
      dtype=object)

The above notation means for example that for a voter \(abc\), the probability that her utility for her second candidate \(b\) is in the first of the ten bins (i.e. between 0 and 0.1) is:

[5]:
profile.d_ranking_histogram['abc'][0]
[5]:
Fraction(9, 100)

Voters in a bin are distributed uniformly. This histogram and the CDF of this distribution can be visualized by:

[6]:
ranking = 'abc'
profile.plot_histogram(ranking, label='Histogram')
profile.plot_cdf(ranking, label='CDF')
plt.ylabel('Proportion of the voters %s' % ranking)
plt.legend()
[6]:
<matplotlib.legend.Legend at 0x11bdf97b160>
../_images/tutorials_tutorial_more_classes_of_profiles_14_1.png

All other features are defined as usual. For example:

[7]:
rand_strategy = pa.RandStrategyThresholdGridUniform(denominator_threshold=100, profile=profile)
strategy = rand_strategy()
strategy
[7]:
<abc: utility-dependent (1/4), acb: utility-dependent (16/25), bac: utility-dependent (19/20), bca: utility-dependent (2/5), cab: utility-dependent (1/10), cba: utility-dependent (9/20)> ==> b
[8]:
strategy.is_equilibrium
[8]:
EquilibriumStatus.NOT_EQUILIBRIUM

ProfileTwelve

In a ProfileTwelve, the voters behave as if their utility for their middle candidate was either infinitely close to 0 or to 1. For example, voters with the ranking \(abc\) are split in two categories: voters a_bc (whose utility for \(b\) is very low) and voters ab_c (whose utility for \(b\) is very high). This can be interpreted in two ways:

  • As a model of their preferences: voters a_bc have a utility for \(b\) that is very close to 1,
  • As a behavioral model: when their best response is utility-dependent, voters a_bc always choose to vote \(a\) only, no matter the utility threshold of the best response.

Create a profile:

[9]:
profile = pa.ProfileTwelve({'ab_c': Fraction(1, 10), 'b_ac': Fraction(6, 10),
                            'c_ab': Fraction(2, 10), 'ca_b': Fraction(1, 10)})
profile
[9]:
<ab_c: 1/10, b_ac: 3/5, c_ab: 1/5, ca_b: 1/10> (Condorcet winner: b)

Share of voters ab_c:

[10]:
profile.ab_c
[10]:
Fraction(1, 10)

Which types are in the profile?

[11]:
profile.support_in_types
[11]:
{ab_c, b_ac, c_ab, ca_b}

Are all possible types in the profile?

[12]:
profile.is_generic_in_types
[12]:
False

Is one type shared by a majority of voters?

[13]:
profile.has_majority_type
[13]:
True

In addition to the usual StrategyOrdinal and StrategyThreshold, this kind of profile also accepts a StrategyTwelve:

[14]:
strategy = pa.StrategyTwelve({'abc': 'ab', 'bac': 'b', 'cab': pa.UTILITY_DEPENDENT}, profile=profile)
strategy
[14]:
<abc: ab, bac: b, cab: utility-dependent> ==> b

In the example above, the strategy for \(cab\) is utility-dependent, meaning that voters c_ba vote for \(c\) and voters cb_a vote for \(c\) and \(b\). One can envision it as a threshold strategy, but it is useless to specify the value of the threshold because we’re dealing with a ProfileTwelve.

[15]:
profile.is_equilibrium(strategy)
[15]:
EquilibriumStatus.EQUILIBRIUM

Since there is a finite number of pure strategies for a ProfileTwelve, it is possible to analyze them all:

[16]:
profile.analyzed_strategies_pure
[16]:
Equilibria:
<abc: a, bac: b, cab: ac> ==> b (FF)
<abc: a, bac: ab, cab: c> ==> a (D)
<abc: ab, bac: b, cab: utility-dependent> ==> b (FF)

Non-equilibria:
<abc: a, bac: b, cab: c> ==> b (FF)
<abc: a, bac: b, cab: utility-dependent> ==> b (FF)
<abc: a, bac: ab, cab: ac> ==> a (D)
<abc: a, bac: ab, cab: utility-dependent> ==> a (D)
<abc: ab, bac: b, cab: c> ==> b (FF)
<abc: ab, bac: b, cab: ac> ==> b (FF)
<abc: ab, bac: ab, cab: c> ==> a, b (FF)
<abc: ab, bac: ab, cab: ac> ==> a (D)
<abc: ab, bac: ab, cab: utility-dependent> ==> a (D)

All other features are defined as usual.

ProfileDiscrete

A ProfileDiscrete is similar to a ProfileNoisyDiscrete, but of course without noise:

[17]:
profile = pa.ProfileDiscrete({
    ('abc', 0.9): Fraction(1, 10),
    ('bac', 0.2): Fraction(6, 10),
    ('cab', 0.25): Fraction(2, 10),
    ('cab', 0.75): Fraction(1, 10)
})
profile
[17]:
<abc 0.9: 1/10, bac 0.2: 3/5, cab 0.25: 1/5, cab 0.75: 1/10> (Condorcet winner: b)

In the above example, the share of voters with ranking \(abc\) and a utility 0.9 for their middle candidate \(b\) is 1/10, the share of voters with ranking \(bac\) and a utility 0.2 for their middle candidate \(a\) is 3/5, etc.

The difficulty is that there is now a concentration of voters, for example, on \((abc, 0.9)\). This opens the door to mixed strategies where voters of this type split their ballots between \(a\) and \(ab\). To account for this fact, a StrategyThreshold can include ratios of optimistic voters:

[18]:
strategy = pa.StrategyThreshold({
    'abc': (0.9, 0.7),
    'bac': 0.5,
    'cab': 0.5
}, profile=profile)
strategy
[18]:
<abc: utility-dependent (0.9, 0.7), bac: utility-dependent (0.5), cab: utility-dependent (0.5)> ==> b

The above notation means that voters \(abc\) have:

  • A utility threshold of 0.9: they vote for \(a\) (resp. \(ab\)) if their utility for their second candidate \(b\) is lower (resp. greater) than 0.9;
  • An ratio of optimistic voters of 0.7: voters whose utility for \(b\) is equal to the utility threshold 0.9 are split, with 0.7 of them voting for \(a\) and the rest voting for \(ab\).

Like for ProfileTwelve, there is a finite number of pure strategies, so it is possible to analyze them all:

[19]:
profile.analyzed_strategies_pure
[19]:
Equilibria:
<abc: ab, bac: b, cab: utility-dependent (0.5)> ==> b (FF)
<abc: a, bac: ab, cab: c> ==> a (D)
<abc: a, bac: b, cab: ac> ==> b (FF)

Non-equilibria:
<abc: ab, bac: ab, cab: ac> ==> a (D)
<abc: ab, bac: ab, cab: utility-dependent (0.5)> ==> a (D)
<abc: ab, bac: ab, cab: c> ==> a, b (FF)
<abc: ab, bac: b, cab: ac> ==> b (FF)
<abc: ab, bac: b, cab: c> ==> b (FF)
<abc: a, bac: ab, cab: ac> ==> a (D)
<abc: a, bac: ab, cab: utility-dependent (0.5)> ==> a (D)
<abc: a, bac: b, cab: utility-dependent (0.5)> ==> b (FF)
<abc: a, bac: b, cab: c> ==> b (FF)

All other features are defined as usual.

ProfileOrdinal

A ProfileOrdinal is a bit special, in the sense that it does not represent a profile, strictly speaking, but rather a family of profiles that have the same ordinal representation.

Definition and Strategic Analysis

[20]:
profile = pa.ProfileOrdinal({
    'abc': Fraction(1, 10),
    'bac': Fraction(6, 10),
    'cab': Fraction(3, 10)
})
profile
[20]:
<abc: 1/10, bac: 3/5, cab: 3/10> (Condorcet winner: b)

Let us analyze its ordinal strategies:

[21]:
profile.analyzed_strategies_ordinal
[21]:
Equilibria:
<abc: a, bac: b, cab: ac> ==> b (FF)
<abc: a, bac: ab, cab: c> ==> a (D)

Utility-dependent equilibrium:
<abc: ab, bac: b, cab: c> ==> b (FF)

Non-equilibria:
<abc: a, bac: b, cab: c> ==> b (FF)
<abc: a, bac: ab, cab: ac> ==> a (D)
<abc: ab, bac: b, cab: ac> ==> b (FF)
<abc: ab, bac: ab, cab: c> ==> a, b (FF)
<abc: ab, bac: ab, cab: ac> ==> a (D)

In the example above, there are not only equilibria and non-equilibria but also a new category of strategies: utility-dependent equilibria. For example:

[22]:
strategy = profile.analyzed_strategies_ordinal.utility_dependent[0]
strategy
[22]:
<abc: ab, bac: b, cab: c> ==> b
[23]:
strategy.d_ranking_best_response['abc']
[23]:
<ballot = ab, threshold_utility = 0, justification = Asymptotic method>
[24]:
strategy.d_ranking_best_response['bac']
[24]:
<ballot = utility-dependent, threshold_utility = 0.836014, justification = Asymptotic method>
[25]:
strategy.d_ranking_best_response['cab']
[25]:
<ballot = utility-dependent, threshold_utility = 0.163986, justification = Asymptotic method>

What happens here? For example, the best response for voters \(bac\) is utility-dependent, with a utility threshold of 0.84 approximately. The fact that they vote \(b\) in strategy is indeed a best response if and only if their utility for their second candidate \(a\) is lower than this value. Overall, strategy is an equilibrium if and only if:

  • The utility of voters \(bac\) for \(a\) is lower than 0.84,
  • And the utility of voters \(cab\) for \(a\) is lower than 0.16.

Probability computations

The following functions compute probabilities. They are based on the assumption that voters \(abc\) have the same utility for their second candidate \(b\) and that it is drawn uniformly in [0, 1]. And similarly for voters \(acb\), \(bac\), etc.

Probability that there exists an equilibrium:

[26]:
profile.proba_equilibrium()
[26]:
1

In this example, the probability is 1 because there exists some equilibria that do not depend on the utilities.

Probability that there exists an equilibrium where voters \(abc\) cast a ballot \(ab\):

[27]:
def test_abc_vote_ab(strategy):
    return strategy.abc == 'ab'
profile.proba_equilibrium(test=test_abc_vote_ab)
[27]:
0.13709468816628012

Probability that there exists an equilibrium where candidate \(c\) wins:

[28]:
def test_c_wins(strategy):
    return 'c' in strategy.winners
profile.proba_equilibrium(test=test_c_wins)
[28]:
0

Distribution of the number of equilibria:

[29]:
profile.distribution_equilibria()
[29]:
array([0.        , 0.        , 0.86290531, 0.13709469])

The above array means that it is impossible that there is 0 or 1 equilibrium, a probability 0.86 to have 2 equilibria, and a probability 0.14 to have 3 equilibria.

You can access the distribution of equilibria, conditionally on a given test:

[30]:
profile.distribution_equilibria(test=test_abc_vote_ab)
[30]:
array([0.86290531, 0.13709469])

The above array means that there is a probability 0.86 to have no equilibrium, and a probability 0.14 to have one equilibrium (conditionally on the given test).

Distribution of the number of winners at equilibrium:

[31]:
profile.distribution_winners()
[31]:
array([0, 0, 1, 0])

The array above means that there are always 2 candidates (namely \(a\) and \(b\)) who can win at equilibrium.

You can access the distribution of winners, conditionally on a given test:

[32]:
profile.distribution_winners(test=test_abc_vote_ab)
[32]:
array([0.86290531, 0.13709469, 0.        , 0.        ])

The above array means that there is a probability 0.86 of having 0 winner (i.e. when there is no equilibrium), and a probability 0.14 to have 1 winner (conditionally on the given test).