Merge pull request #12 from MieuxVoter/feat-median-default-grade

Implement median default grade tally
pull/14/head
Dominique Merle 3 years ago committed by GitHub
commit d542567cb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -140,17 +140,17 @@ ResultInterface result = mj.deliberate(tally);
- [x] Ranking
- [x] Release v0.1.0
- [x] Guess the amount of judges
- [ ] Allow defining a default grade
- [x] Allow defining a default grade
- [x] Static Grade (configurable)
- [x] Normalization (using least common multiple)
- [ ] Median Grade
- [x] Normalization (using Least Common Multiple)
- [x] Median Grade
- [ ] Release v0.2.0
- [ ] Publish on package repositories
- [ ] Gradle
- [ ] Maven
- [ ] … ? (please share your knowledge to help us!)
- [ ] Release v0.3.0
- [ ] Use it somewhere in another app, adjust API as needed (one last time)
- [ ] Use it somewhere in an application, adjust API as needed (one last time)
- [ ] Release v1.0.0

@ -0,0 +1,50 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
/**
* Fill the missing judgments into the grade defined by `getDefaultGrade()`.
* This is an abstract class to dry code between static default grade and median default grade.
*/
abstract public class DefaultGradeTally extends Tally implements TallyInterface {
/**
* Override this to choose the default grade for a given proposal.
*/
abstract protected Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally);
// <domi41> /me is confused with why we need constructors in an abstract class?
public DefaultGradeTally(TallyInterface tally) {
super(tally.getProposalsTallies(), tally.getAmountOfJudges());
}
public DefaultGradeTally(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges) {
super(proposalsTallies, amountOfJudges);
}
public DefaultGradeTally(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges) {
super(proposalsTallies, amountOfJudges);
}
public DefaultGradeTally(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges) {
super(proposalsTallies, amountOfJudges);
}
protected void fillWithDefaultGrade() {
int amountOfProposals = getAmountOfProposals();
for (int i = 0 ; i < amountOfProposals ; i++) {
ProposalTallyInterface proposalTally = getProposalsTallies()[i];
Integer defaultGrade = getDefaultGradeForProposal(proposalTally);
BigInteger amountOfJudgments = proposalTally.getAmountOfJudgments();
BigInteger missingAmount = this.amountOfJudges.subtract(amountOfJudgments);
int missingSign = missingAmount.compareTo(BigInteger.ZERO);
assert(0 <= missingSign); // ERROR: More judgments than judges!
if (0 < missingSign) {
BigInteger[] rawTally = proposalTally.getTally();
rawTally[defaultGrade] = rawTally[defaultGrade].add(missingAmount);
}
}
}
}

@ -0,0 +1,38 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
/**
* Fill the missing judgments into the median grade of each proposal.
* Useful when the proposals have not received the exact same amount of votes and
* the median grade is considered a sane default.
*/
public class MedianDefaultTally extends DefaultGradeTally implements TallyInterface {
public MedianDefaultTally(TallyInterface tally) {
super(tally.getProposalsTallies(), tally.getAmountOfJudges());
fillWithDefaultGrade();
}
public MedianDefaultTally(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges) {
super(proposalsTallies, amountOfJudges);
fillWithDefaultGrade();
}
public MedianDefaultTally(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges) {
super(proposalsTallies, amountOfJudges);
fillWithDefaultGrade();
}
public MedianDefaultTally(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges) {
super(proposalsTallies, amountOfJudges);
fillWithDefaultGrade();
}
@Override
protected Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally) {
ProposalTallyAnalysis analysis = new ProposalTallyAnalysis(proposalTally);
return analysis.getMedianGrade();
}
}

@ -2,10 +2,28 @@ package fr.mieuxvoter.mj;
import java.math.BigInteger;
public class TallyWithDefaultGrade extends Tally implements TallyInterface {
public class TallyWithDefaultGrade extends DefaultGradeTally implements TallyInterface {
/**
* Grades are represented as numbers, as indices in a list.
* Grades start from 0 ("worst" grade, most conservative) and go upwards.
* Values out of the range of grades defined in the tally will yield errors.
*
* Example:
*
* 0 == REJECT
* 1 == PASSABLE
* 2 == GOOD
* 3 == EXCELLENT
*/
protected Integer defaultGrade = 0;
public TallyWithDefaultGrade(TallyInterface tally, Integer defaultGrade) {
super(tally.getProposalsTallies(), tally.getAmountOfJudges());
this.defaultGrade = defaultGrade;
fillWithDefaultGrade();
}
public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges, Integer defaultGrade) {
super(proposalsTallies, amountOfJudges);
this.defaultGrade = defaultGrade;
@ -24,18 +42,9 @@ public class TallyWithDefaultGrade extends Tally implements TallyInterface {
fillWithDefaultGrade();
}
protected void fillWithDefaultGrade() {
int amountOfProposals = getAmountOfProposals();
for (int i = 0 ; i < amountOfProposals ; i++) {
ProposalTallyInterface proposal = getProposalsTallies()[i];
BigInteger amountOfJudgments = proposal.getAmountOfJudgments();
BigInteger missingAmount = this.amountOfJudges.subtract(amountOfJudgments);
int missingSign = missingAmount.compareTo(BigInteger.ZERO);
assert(0 <= missingSign); // ERROR: More judgments than judges!
if (0 < missingSign) {
proposal.getTally()[this.defaultGrade] = proposal.getTally()[this.defaultGrade].add(missingAmount);
}
}
@Override
protected Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally) {
return this.defaultGrade;
}
}

@ -20,6 +20,9 @@ class MajorityJudgmentDeliberatorTest {
@ParameterizedTest(name="#{index} {0}")
@JsonFileSource(resources = "/assertions.json")
public void testFromJson(JsonObject datum) {
// This test uses the JSON file in test/resources/
// It also allows testing the various modes of default grades.
JsonArray jsonTallies = datum.getJsonArray("tallies");
int amountOfProposals = jsonTallies.size();
BigInteger amountOfParticipants = new BigInteger(datum.get("participants").toString());
@ -39,6 +42,8 @@ class MajorityJudgmentDeliberatorTest {
TallyInterface tally;
if ("StaticDefault".equalsIgnoreCase(mode)) {
tally = new TallyWithDefaultGrade(tallies, amountOfParticipants, datum.getInt("default"));
} else if ("MedianDefault".equalsIgnoreCase(mode)) {
tally = new MedianDefaultTally(tallies, amountOfParticipants);
} else if ("Normalized".equalsIgnoreCase(mode)) {
tally = new NormalizedTally(tallies);
} else {
@ -213,7 +218,7 @@ class MajorityJudgmentDeliberatorTest {
}
@Test
@DisplayName("Test static default grade with thousands of proposals")
@DisplayName("Test static default grade with thousands of proposals and millions of judges")
public void testStaticDefaultWithThousandsOfProposals() {
int amountOfProposals = 1337;
Integer amountOfJudges = 60000000;
@ -257,15 +262,36 @@ class MajorityJudgmentDeliberatorTest {
// }
// }
@Test
@DisplayName("Test with a median default grade")
public void testMedianDefaultGrade() {
Integer amountOfJudges = 42;
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
TallyInterface tally = new MedianDefaultTally(new ProposalTallyInterface[] {
new ProposalTally(new Integer[]{ 0, 0, 1 }),
new ProposalTally(new Integer[]{ 0, 1, 0 }),
new ProposalTally(new Integer[]{ 1, 1, 1 }),
new ProposalTally(new Integer[]{ 1, 0, 1 }),
new ProposalTally(new Integer[]{ 1, 0, 0 }),
}, amountOfJudges);
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
assertEquals(5, result.getProposalResults().length);
assertEquals(1, result.getProposalResults()[0].getRank());
assertEquals(2, result.getProposalResults()[1].getRank());
assertEquals(3, result.getProposalResults()[2].getRank());
assertEquals(4, result.getProposalResults()[3].getRank());
assertEquals(5, result.getProposalResults()[4].getRank());
}
@Test
@DisplayName("Test normalized tallies with thousands of (prime) proposals")
public void testNormalizedWithThousandsOfPrimeProposals() {
// We're using primes to test the upper bounds of our LCM shenanigans.
// This test takes a long time! (3 seconds)
// List<Integer> generatedPrimes = sieveOfEratosthenes(15000);
// System.out.println(generatedPrimes);
int amountOfProposals = primes.length; // 1437
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals];
@ -282,7 +308,7 @@ class MajorityJudgmentDeliberatorTest {
assertEquals(amountOfProposals, result.getProposalResults().length);
for (int i = 0 ; i < amountOfProposals ; i++) {
assertEquals(
1 + (i % primes.length), result.getProposalResults()[i].getRank(),
1 + i, result.getProposalResults()[i].getRank(),
"Rank of Proposal #" + i
);
}
@ -292,7 +318,7 @@ class MajorityJudgmentDeliberatorTest {
@DisplayName("Test normalized tallies with thousands of proposals")
public void testNormalizedWithThousandsOfProposals() {
// This test is faster than the primes one (0.4 seconds),
// since primes are the worst case-scenario for our LCM.
// since primes are the worst-case scenario for our LCM.
int amountOfProposals = primes.length; // 1437
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
@ -309,12 +335,14 @@ class MajorityJudgmentDeliberatorTest {
assertEquals(amountOfProposals, result.getProposalResults().length);
for (int i = 0 ; i < amountOfProposals ; i++) {
assertEquals(
1 + (i % primes.length), result.getProposalResults()[i].getRank(),
1 + i, result.getProposalResults()[i].getRank(),
"Rank of Proposal #" + i
);
}
}
// …
// @Test
// public void runBenchmarks() throws Exception {
// Options options = new OptionsBuilder()

@ -4,11 +4,11 @@
"title": "Few participants",
"participants": 3,
"tallies": [
[1, 1, 1],
[1, 0, 2],
[3, 0, 0],
[2, 0, 1],
[0, 3, 0]
[ 1, 1, 1 ],
[ 1, 0, 2 ],
[ 3, 0, 0 ],
[ 2, 0, 1 ],
[ 0, 3, 0 ]
],
"ranks": [
3,
@ -22,9 +22,9 @@
"title": "Thousands of participants",
"participants": 37000,
"tallies": [
[11142, 6970, 4040, 1968, 9888, 2992],
[10141, 8971, 4043, 1965, 8884, 2996],
[14141, 8971, 1043, 1965, 7884, 2996]
[ 11142, 6970, 4040, 1968, 9888, 2992 ],
[ 10141, 8971, 4043, 1965, 8884, 2996 ],
[ 14141, 8971, 1043, 1965, 7884, 2996 ]
],
"ranks": [
1,
@ -36,11 +36,11 @@
"title": "Millions of participants",
"participants": 72327456,
"tallies": [
[5272679, 19797001, 10732688, 9612936, 1379840, 16886281, 8646031],
[16354546, 11690342, 9451800, 14245973, 817593, 12461162, 7306040],
[9849171, 17970690, 14276861, 4606692, 16404594, 6760147, 2459301],
[2645563, 12907474, 1278331, 22843261, 8025412, 8964952, 15662463],
[16293252, 12277630, 38348, 14929905, 11087753, 10634266, 7066302]
[ 5272679, 19797001, 10732688, 9612936, 1379840, 16886281, 8646031 ],
[ 16354546, 11690342, 9451800, 14245973, 817593, 12461162, 7306040 ],
[ 9849171, 17970690, 14276861, 4606692, 16404594, 6760147, 2459301 ],
[ 2645563, 12907474, 1278331, 22843261, 8025412, 8964952, 15662463 ],
[ 16293252, 12277630, 38348, 14929905, 11087753, 10634266, 7066302 ]
],
"ranks": [
3,
@ -90,6 +90,25 @@
2
]
},
{
"title": "Median Default Grade",
"participants": 10,
"mode": "MedianDefault",
"tallies": [
[ 2, 2, 2, 2, 2 ],
[ 2, 2, 1, 2, 2 ],
[ 0, 0, 4, 0, 0 ],
[ 0, 0, 0, 0, 2 ],
[ 0, 0, 0, 1, 1 ]
],
"ranks": [
4,
4,
3,
1,
2
]
},
{
"title": "Normalization",
"participants": 10,

Loading…
Cancel
Save