Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to the per-slice CSV #872

Merged
merged 9 commits into from
Sep 25, 2024
83 changes: 53 additions & 30 deletions src/main/java/network/brightspots/rcv/ResultsWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,11 @@ ResultsWriter setTimestampString(String timestampString) {
// creates summary files for the votes split by a TabulateBySlice
// param: roundTalliesBySlice is map from a slice type to the round-by-round vote tallies
// param: tallyTransfersBySlice is a map from a slice type to tally transfers for that slice
// param: candidateOrder is to allow a consistent ordering of candidates, including across slices
void generateBySliceSummaryFiles(
Tabulator.BreakdownBySlice<RoundTallies> roundTalliesBySlice,
Tabulator.BreakdownBySlice<TallyTransfers> tallyTransfersBySlice)
Tabulator.BreakdownBySlice<TallyTransfers> tallyTransfersBySlice,
List<String> candidateOrder)
throws IOException {
for (ContestConfig.TabulateBySlice slice : config.enabledSlices()) {
Set<String> filenames = new HashSet<>();
Expand All @@ -271,20 +273,24 @@ void generateBySliceSummaryFiles(
String sliceFileString = getFileStringForSlice(slice, sliceId, filenames);
String outputPath = getOutputFilePathFromInstance(
String.format("%s_summary", sliceFileString));
generateSummarySpreadsheet(roundTallies, slice, sliceId, outputPath);
generateSummarySpreadsheet(roundTallies, candidateOrder, slice, sliceId, outputPath);
generateSummaryJson(roundTallies, tallyTransfers, slice, sliceId, outputPath);
}
}
}

// create a summary spreadsheet .csv file
// param: roundTallies is the round-by-count count of votes per candidate
// param: candidateOrder is to allow a consistent ordering of candidates, including across slices
// param: slice indicates which type of slice we're reporting results for (null means all)
// param: sliceId indicates the specific slice ID we're reporting results for (null means all)
// param: outputPath is the path to the output file, minus its extension
private void generateSummarySpreadsheet(
RoundTallies roundTallies, TabulateBySlice slice, String sliceId, String outputPath)
throws IOException {
RoundTallies roundTallies,
List<String> candidateOrder,
TabulateBySlice slice,
String sliceId,
String outputPath) throws IOException {
AuditableFile csvFile = new AuditableFile(outputPath + ".csv");
Logger.info("Generating summary spreadsheet: %s...", csvFile.getAbsolutePath());

Expand All @@ -310,16 +316,25 @@ private void generateSummarySpreadsheet(
}
csvPrinter.println();

// actions don't make sense in individual by-slice results
if (isNullOrBlank(sliceId)) {
addActionRows(csvPrinter);
}
final boolean isSlice = !isNullOrBlank(sliceId);
csvPrinter.print(isSlice ? "Eliminated*" : "Eliminated");
printActionSummary(csvPrinter, roundToEliminatedCandidates);

csvPrinter.print(isSlice ? "Elected*" : "Elected");
printActionSummary(csvPrinter, roundToWinningCandidates);

// Get all candidates sorted by their first round tally. This determines the display order.
List<String> sortedCandidates = roundTallies.get(1).getSortedCandidatesByTally();
// Check that all candidates are included in the candidate order
Set<String> expectedCandidates = roundTallies.get(1).getCandidates();
Set<String> providedCandidates = new HashSet<>(candidateOrder);
if (!expectedCandidates.equals(providedCandidates)) {
throw new IllegalArgumentException(
"Candidate order must include all candidates in the contest. "
+ "\nExpected: " + expectedCandidates
+ "\nProvided: " + providedCandidates);
}
yezr marked this conversation as resolved.
Show resolved Hide resolved

// For each candidate: for each round: output total votes
for (String candidate : sortedCandidates) {
for (String candidate : candidateOrder) {
String candidateDisplayName = config.getNameForCandidate(candidate);
csvPrinter.print(candidateDisplayName);
for (int round = 1; round <= numRounds; round++) {
Expand Down Expand Up @@ -366,13 +381,15 @@ private void generateSummarySpreadsheet(
}
csvPrinter.println();

csvPrinter.print("Current Round Threshold");
for (int round = 1; round <= numRounds; round++) {
csvPrinter.print(roundTallies.get(round).getWinningThreshold());
csvPrinter.print("");
csvPrinter.print("");
if (!isSlice) {
csvPrinter.print("Current Round Threshold");
for (int round = 1; round <= numRounds; round++) {
csvPrinter.print(roundTallies.get(round).getWinningThreshold());
csvPrinter.print("");
csvPrinter.print("");
}
csvPrinter.println();
}
csvPrinter.println();

List<Pair<String, StatusForRound>> statusesToPrint = new ArrayList<>();
statusesToPrint.add(new Pair<>("Overvotes",
Expand Down Expand Up @@ -435,7 +452,7 @@ private void generateSummarySpreadsheet(
// whether the value in the final round is positive.
// Note that this concept only makes sense when we're reporting the overall tabulation, so we
// omit it when generating results at the individual by-slice level.
if (sliceId == null && roundToResidualSurplus.get(numRounds).signum() == 1) {
if (!isSlice && roundToResidualSurplus.get(numRounds).signum() == 1) {
csvPrinter.print("Residual surplus");
for (int round = 1; round <= numRounds; round++) {
csvPrinter.print(roundToResidualSurplus.get(round));
Expand All @@ -447,6 +464,13 @@ private void generateSummarySpreadsheet(
csvPrinter.println();
}

if (isSlice) {
csvPrinter.println();
csvPrinter.print(String.format("*Elect/Eliminate decisions are from the full contest. "
+ "All other results on this report are at the %s level.", slice.toLowerString()));
csvPrinter.println();
}

try {
csvPrinter.flush();
csvPrinter.close();
Expand All @@ -458,15 +482,6 @@ private void generateSummarySpreadsheet(
Logger.info("Summary spreadsheet generated successfully.");
}

// "action" rows describe which candidates were eliminated or elected
private void addActionRows(CSVPrinter csvPrinter) throws IOException {
csvPrinter.print("Eliminated");
printActionSummary(csvPrinter, roundToEliminatedCandidates);

csvPrinter.print("Elected");
printActionSummary(csvPrinter, roundToWinningCandidates);
}

private void addContestSummaryRows(CSVPrinter csvPrinter, RoundTally round1Tally)
throws IOException {
BigDecimal numNoRankings =
Expand Down Expand Up @@ -533,19 +548,27 @@ private void addContestInformationRows(CSVPrinter csvPrinter,
winners.add(config.getNameForCandidate(candidateName));
}
}

csvPrinter.printRecord("Winner(s)", String.join(", ", winners));
csvPrinter.printRecord("Final Threshold", winningThreshold);

if (!isNullOrBlank(sliceId)) {
// Only slices print the slice information
csvPrinter.printRecord(slice, sliceId);
} else {
// Only non-slices print threshold information
csvPrinter.printRecord("Final Threshold", winningThreshold);
}

csvPrinter.println();
}

// creates a summary spreadsheet and JSON for the full contest (as opposed to a specific slice)
void generateOverallSummaryFiles(
RoundTallies roundTallies, TallyTransfers tallyTransfers) throws IOException {
RoundTallies roundTallies,
TallyTransfers tallyTransfers,
List<String> candidateOrder) throws IOException {
String outputPath = getOutputFilePathFromInstance("summary");
generateSummarySpreadsheet(roundTallies, null, null, outputPath);
generateSummarySpreadsheet(roundTallies, candidateOrder, null, null, outputPath);
generateSummaryJson(roundTallies, tallyTransfers, null, null, outputPath);
}

Expand Down
5 changes: 3 additions & 2 deletions src/main/java/network/brightspots/rcv/Tabulator.java
Original file line number Diff line number Diff line change
Expand Up @@ -805,8 +805,9 @@ void generateSummaryFiles(String timestamp) throws IOException {
.setSliceIds(sliceIds)
.setRoundToResidualSurplus(roundToResidualSurplus);

writer.generateOverallSummaryFiles(roundTallies, tallyTransfers);
writer.generateBySliceSummaryFiles(roundTalliesBySlices, tallyTransfersBySlice);
List<String> candidateOrder = roundTallies.get(1).getSortedCandidatesByTally();
writer.generateOverallSummaryFiles(roundTallies, tallyTransfers, candidateOrder);
writer.generateBySliceSummaryFiles(roundTalliesBySlices, tallyTransfersBySlice, candidateOrder);

if (config.isGenerateCdfJsonEnabled()) {
try {
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/network/brightspots/rcv/TabulatorTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ void precinctExample() {
@Test
@DisplayName("tabulate by batch")
void batchExample() {
runTabulationTest("batch_example");
runTabulationTest("batch_example", 2);
}

@Test
Expand Down
Loading
Loading