Repeated Voting, Mandate Recalculation, and Election Forensics

Reading Time: 5 minutes

1. Why repeated voting matters

Repeated voting sounds like a narrow administrative detail, but it is not. In proportional systems, a repeated vote at just one polling station can change not only the local tally at that station, but also the final allocation of seats in the whole electoral unit. That is why repeated voting matters to lawyers, election officials, observers, journalists, and election forensics researchers alike: once the vote at one polling station is annulled and repeated, the legally relevant result is no longer the first aggregate count, but the revised aggregate count after the repeated station is inserted back into the total. In Serbia, this is not a hypothetical issue. After the March 2026 local elections, repeated voting was ordered at polling station no. 8 in Knjaževac after the Higher Court in Zaječar annulled the original vote there; local reporting stated that the repeated vote was scheduled for 29 April 2026 and that the station had 817 registered voters. Serbia’s election administration portal also published Knjaževac documents related to repeated local elections in 2026.

For election forensics, this is especially important because repeated voting shows how legal procedure and numerical structure meet. The law may say that voting must be repeated at a particular polling station, but the analytically important question is what happens next. The answer is simple in principle: the old result from the annulled station is removed, the new result is inserted, and the mandate distribution is recalculated under the same electoral rule. In Serbia’s local elections, that means recalculating under the 3% threshold rule and the highest-quotient system, with the special minority-list rule also taken into account where relevant.

2. The core idea: final valid totals, not first totals

The central theoretical point is straightforward. A repeated vote does not create a special, parallel theory of elections. It triggers an ordinary recalculation under the same electoral formula that governed the original election. In the literature on electoral systems and apportionment, votes are converted into seats through a rule; if the legally valid vote totals change, the seat allocation changes mechanically as well. The repeated-vote problem is therefore best treated as a special case of a general rule: mandates follow the final valid totals, not the provisional first totals. That is entirely consistent with the broader electoral-systems literature and with the mathematical logic of divisor methods such as D’Hondt.

That point is worth stating clearly because public debate often drifts into vaguer language. Journalists may say that “the election is repeated,” or political actors may say that “the result remains the same” or “the result has changed,” but from an analytical perspective the key object is the electoral unit total after legal correction. Once that is understood, the logic becomes transparent. We do not need a mystical doctrine of repeated voting. We need a careful rule of replacement and recalculation. The original result at the repeated polling station is replaced by the repeated result, all other polling stations remain fixed unless the law says otherwise, and the seat-allocation rule is applied again.

In Serbia’s local-election law, this logic is visible in the structure of the rules themselves. Article 58 provides that voting at a polling station shall be repeated if the results there cannot be determined or if voting there has been annulled. Article 59 then makes clear that the general report on results is compiled after repeated voting and related legal remedies are resolved. In other words, the law itself treats the repeated vote not as an appendix to the result, but as part of the process that produces the final valid result.

3. Mathematical formulation

Now we can state the problem formally. Suppose there are \(P\) parties or electoral lists and \(S\) seats to allocate in one electoral unit. Let the original legally recorded total for party \(p\) be \(V_p\). Let \(R\) be the set of polling stations at which voting is repeated. For each repeated station \(r \in R\), let \(v_{pr}^{(old)} \) denote the original result for party \(p\) at that station, and let \(v_{pr}^{(new)} \) denote the repeated-vote result.

The corrected vote total is:

$$ V_p^* = V_p – \sum_{r \in R} v_{pr}^{(old)} + \sum_{r \in R} v_{pr}^{(new)}. $$

The corrected mandate vector is then:

$$ m^{*} = A(V_1^{*}, V_2^{*}, \dots, V_P^{*}; S) $$

where \(A(\cdot)\) is the legally prescribed seat-allocation rule. If the rule is D’Hondt, then \(A\) is the highest-averages procedure based on divisors \(1,2,3,\dots\). If the rule includes a threshold, that threshold is applied to the corrected totals. If the rule includes special minority-list treatment, that too must be applied to the corrected totals.

This formulation is simple, but it is powerful. It tells us that repeated voting is, mathematically, a replacement problem followed by an allocation problem. First, replace the old station-level vector with the new one. Second, rerun the allocation rule. The mandate shift is then just:

$$ \Delta m = m^* – m^{(0)}, $$

where \(m^{(0)}\) is the original seat vector. If one wants to model uncertainty before the repeated vote occurs, one can replace the known repeated result with a random vector and simulate possible seat outcomes. But for legal and administrative purposes, the decisive object is the realized corrected total after the repeat vote is actually held.

4. Serbian local-election law: what changes, what does not

For Serbian local elections, the legal context matters. Under the current local-election framework, repeated voting is conducted when the result at a polling station cannot be determined or when the vote there is annulled, and it must be held within ten days of the relevant decision. Only after repeat voting and related remedies are resolved is the general report on local-election results finalized. That means the legally relevant seat allocation is the one calculated from the final corrected totals, not from the first announced municipal tally.

The seat-allocation rule itself is equally important. Article 61 sets a 3% threshold for participation in seat distribution. Article 62 then specifies the highest-quotient system: votes are divided by consecutive integers from 1 up to the number of councillors, the quotients are ranked, and seats are assigned according to the highest quotients. The same article also states a tie-break rule: if two lists have the same quotient for the decisive seat, priority goes to the list with the larger total number of votes. The law further contains a special rule for minority lists: where applicable, quotients of minority lists below 3% are increased by 35%. These details matter because a blog post about repeated voting that ignored threshold rules, tie-breaking, or minority-list provisions would be incomplete in the Serbian context.

This is also where implementation becomes practically interesting. Many simple demonstrations of D’Hondt in textbooks or online code snippets calculate quotients correctly but ignore legal tie-breaking. That is acceptable for teaching the basic idea, but not always for production-grade election analysis. In a real repeated-voting case, especially when the repeated station is small, tied quotients can easily occur near the last mandate. So a serious implementation should handle not only the core D’Hondt quotient ranking, but also the jurisdiction-specific tie-break rule.

5. R implementation

The code below implements the logic cleanly. It recalculates mandates after repeated voting, includes a threshold parameter, includes an optional minority-list flag and Serbian-style 35% quotient uplift, and explicitly handles the Serbian tie-break rule for equal quotients by giving priority to the list with more total votes.

# D'Hondt seat allocation / D'Hondt raspodela mandata
dhondt_repeated <- function(original_votes, repeat_old, repeat_new, seats,
                            threshold = 0.03, minority = NULL, bonus = 1.35) {
  # original_votes = original municipality totals / originalni ukupni glasovi
  # repeat_old = old votes from repeated stations / stari glasovi sa ponovljenih mesta
  # repeat_new = new votes from repeated stations / novi glasovi sa ponovljenih mesta
  # seats = total mandates / ukupan broj mandata
  
  # Align party names / Uskladi nazive lista
  parties <- names(original_votes)
  repeat_old <- repeat_old[parties]
  repeat_new <- repeat_new[parties]
  
  # Default: no minority lists / Podrazumevano: nema manjinskih lista
  if (is.null(minority)) minority <- rep(FALSE, length(parties))
  names(minority) <- parties
  
  # Corrected totals / Korigovani ukupni glasovi
  corrected_votes <- original_votes - repeat_old + repeat_new
  
  # Total valid votes / Ukupan broj važećih glasova
  total_valid <- sum(corrected_votes)
  
  # Threshold eligibility / Provera cenzusa
  eligible <- (corrected_votes / total_valid) >= threshold | minority
  
  # Build quotient table / Napravi tabelu količnika
  qtab <- do.call(rbind, lapply(parties[eligible], function(p) {
    divs <- seq_len(seats)
    q <- unname(corrected_votes[p]) / divs
    
    # Serbian minority uplift if applicable / Srpski manjinski bonus ako važi
    if (minority[p] && (corrected_votes[p] / total_valid) < threshold) {
      q <- q * bonus
    }
    
    data.frame(
      party = rep(p, seats),
      total_votes = rep(unname(corrected_votes[p]), seats),
      divisor = divs,
      quotient = q,
      stringsAsFactors = FALSE
    )
  }))
  
  # Rank quotients: quotient first, then higher total votes / Rangiraj količnike: količnik pa veći ukupan broj glasova
  qtab <- qtab[order(-qtab$quotient, -qtab$total_votes, qtab$divisor, qtab$party), ]
  
  # Allocate seats / Dodeli mandate
  winners <- qtab[seq_len(seats), ]
  seat_counts <- table(factor(winners$party, levels = parties))
  
  # Return results / Vrati rezultate
  list(
    corrected_votes = corrected_votes,
    seat_counts = seat_counts,
    ranking = winners
  )
}

# Example / Primer
original_votes <- c(A = 10000, B = 8000, C = 4000)
repeat_old     <- c(A = 800,   B = 400,  C = 200)
repeat_new     <- c(A = 500,   B = 700,  C = 300)
S <- 10

# Original seats / Originalni mandati
orig <- dhondt_repeated(
  original_votes = original_votes,
  repeat_old = c(A = 0, B = 0, C = 0),
  repeat_new = c(A = 0, B = 0, C = 0),
  seats = S,
  threshold = 0
)

# Corrected seats / Korigovani mandati
corr <- dhondt_repeated(
  original_votes = original_votes,
  repeat_old = repeat_old,
  repeat_new = repeat_new,
  seats = S,
  threshold = 0
)

# Print outputs / Prikaži izlaz
orig$seat_counts
corr$corrected_votes
corr$seat_counts
corr$seat_counts - orig$seat_counts

Expected output:

# Print outputs / Prikaži izlaz
> orig$seat_counts

A B C 
5 4 1 
> corr$corrected_votes
   A    B    C 
9700 8300 4100 
> corr$seat_counts

A B C 
4 4 2 
> corr$seat_counts - orig$seat_counts

 A  B  C 
-1  0  1

This code does two things that matter analytically. First, it encodes the replacement logic correctly: the repeated station does not add votes on top of the old result; it replaces the old result. Second, it makes the tie-breaking rule explicit. That is not cosmetic. In the worked example below, the original allocation actually contains a tie at quotient 2000, so a legally serious implementation should not leave tie resolution to accidental ordering in software.

6. Manual verification of the example

Now let us verify the example by hand.

Original totals: A = 10, 000, B = 8,000, C = 4,000, Seats = 10.

Relevant D’Hondt quotients:

  • A: 10000, 5000, 3333.33, 2500, 2000, 1666.67, …
  • B: 8000, 4000, 2666.67, 2000, 1600, …
  • C: 4000, 2000, 1333.33, …

The ten highest quotients are:

  1. 10000 (A)
  2. 8000 (B)
  3. 5000 (A)
  4. 4000 (B)
  5. 4000 (C)
  6. 3333.33 (A)
  7. 2666.67 (B)
  8. 2500 (A)
  9. 2000 (A)
  10. 2000 (B)

Why not C for the last seat? Because the original example contains a three-way tie at 2000 among A, B, and C, and Serbian law gives priority to the list with the larger total number of votes. So A beats B and C for one of those seats, and B beats C for the other. Therefore the original seat distribution is: A = 5, B = 4, C = 1.

Now apply repeated voting.

Old repeated-station contribution: A = 800, B = 400, C = 200.

New repeated-station result: A = 500, B = 700, C = 300.

Corrected totals:

  • A = 10000 – 800 + 500 = 9700
  • B = 8000 – 400 + 700 = 8300
  • C = 4000 – 200 + 300 = 4100

Corrected D’Hondt quotients:

  • A: 9700, 4850, 3233.33, 2425, 1940, …
  • B: 8300, 4150, 2766.67, 2075, 1660, …
  • C: 4100, 2050, 1366.67, …

The ten highest corrected quotients are:

  1. 9700 (A)
  2. 8300 (B)
  3. 4850 (A)
  4. 4150 (B)
  5. 4100 (C)
  6. 3233.33 (A)
  7. 2766.67 (B)
  8. 2425 (A)
  9. 2075 (B)
  10. 2050 (C)

So the corrected seat distribution is: A = 4, B = 4, C = 2.

And the mandate shift is: A = -1, B = 0, C = +1.

So yes: the corrected logic really does yield a new D’Hondt allocation, and the example is a perfectly good illustration of how a relatively small change at repeated polling stations can alter the final seat distribution. But it also reveals an important refinement: a legally robust implementation should include explicit tie-breaking rather than relying on default software ordering.

7. Why this matters for election forensics

Repeated voting matters for election forensics for two reasons. The first is obvious: if a repeated vote changes the legal result, then any serious post-election analysis must work from the final valid totals, not the preliminary ones. The second is subtler: repeated voting can be a diagnostic event. It shows how sensitive seat allocation can be to small changes in vote vectors, especially under divisor methods. In a highly competitive local contest, a few hundred votes at one polling station may not just change percentages; they may flip a mandate. That makes repeated voting analytically important even when public debate treats it as a footnote.

This is precisely why the topic belongs in an election forensics blog. Election forensics is not only about detecting improbable patterns in aggregates. It is also about understanding the mechanics through which legal corrections, repeated voting, and electoral formulas translate disputed ballots into final representation. The theory is simple, but its implications are not. A repeated vote is small in geography, potentially large in consequence, and always a reminder that representation depends not only on how people vote, but also on how institutions count, correct, and convert votes into seats.

References

Balinski, M. L., & Young, H. P. (1994). Apportionment. In K. J. Arrow, A. K. Sen, & K. Suzumura (Eds.), Handbook of Social Choice and Welfare. Elsevier. doi:10.1016/S0927-0507(05)80096-9

de Córdoba, G. F., Fernández, J. R., & Torres, J. L. (2009). Institutionalizing uncertainty: The choice of electoral formulas. Public Choice, 141(3–4), 421–444. doi:10.1007/s11127-009-9460-9

Gallagher, M., & Mitchell, P. (Eds.). (2005). The Politics of Electoral Systems. Oxford University Press. doi:10.1093/0199257566.001.0001

Kohler, U. (2012). Apportionment methods. The Stata Journal, 12(3), 375–392. doi:10.1177/1536867X1201200303

Medzihorsky, J. (2019). Rethinking the D’Hondt method. Political Research Exchange, 1(1), Article 1625712. doi:10.1080/2474736X.2019.1625712

Two notes. First, the direct literature with DOI specifically devoted to partial revoting and mandate recalculation is rather scarce; therefore, the formalization in this text is derived from the general logic of vote aggregation, valid electoral law and standard literature on the distribution of mandates. Second, the example with original votes 10,000–8,000–4,000 is useful precisely because it contains a tie in the original distribution and thus shows that “correct D'Hondt” in actual practice often means: D'Hondt plus the legal rule to resolve the tie.

Python version

from typing import Dict, List, Optional, Any

def dhondt_repeated(
    original_votes: Dict[str, float],
    repeat_old: Dict[str, float],
    repeat_new: Dict[str, float],
    seats: int,
    threshold: float = 0.03,
    minority: Optional[Dict[str, bool]] = None,
    bonus: float = 1.35
) -> Dict[str, Any]:
    # original_votes = original municipality totals / originalni ukupni glasovi
    # repeat_old = old votes from repeated stations / stari glasovi sa ponovljenih mesta
    # repeat_new = new votes from repeated stations / novi glasovi sa ponovljenih mesta
    # seats = total mandates / ukupan broj mandata

    # Align party names / Uskladi nazive lista
    parties: List[str] = list(original_votes.keys())

    # Default missing values to zero / Nedostajuće vrednosti postavi na nulu
    repeat_old_aligned = {p: repeat_old.get(p, 0) for p in parties}
    repeat_new_aligned = {p: repeat_new.get(p, 0) for p in parties}

    # Default: no minority lists / Podrazumevano: nema manjinskih lista
    if minority is None:
        minority = {p: False for p in parties}
    else:
        minority = {p: minority.get(p, False) for p in parties}

    # Corrected totals / Korigovani ukupni glasovi
    corrected_votes = {
        p: original_votes[p] - repeat_old_aligned[p] + repeat_new_aligned[p]
        for p in parties
    }

    # Total valid votes / Ukupan broj važećih glasova
    total_valid = sum(corrected_votes.values())

    # Threshold eligibility / Provera cenzusa
    eligible = {
        p: ((corrected_votes[p] / total_valid) >= threshold) or minority[p]
        for p in parties
    }

    # Build quotient table / Napravi tabelu količnika
    qtab: List[Dict[str, Any]] = []
    for p in parties:
        if not eligible[p]:
            continue

        for divisor in range(1, seats + 1):
            quotient = corrected_votes[p] / divisor

            # Serbian minority uplift if applicable / Srpski manjinski bonus ako važi
            if minority[p] and (corrected_votes[p] / total_valid) < threshold:
                quotient *= bonus

            qtab.append({
                "party": p,
                "total_votes": corrected_votes[p],
                "divisor": divisor,
                "quotient": quotient
            })

    # Rank quotients: quotient first, then higher total votes / Rangiraj količnike: količnik pa veći ukupan broj glasova
    qtab_sorted = sorted(
        qtab,
        key=lambda row: (-row["quotient"], -row["total_votes"], row["divisor"], row["party"])
    )

    # Allocate seats / Dodeli mandate
    winners = qtab_sorted[:seats]
    seat_counts = {p: 0 for p in parties}
    for row in winners:
        seat_counts[row["party"]] += 1

    # Return results / Vrati rezultate
    return {
        "corrected_votes": corrected_votes,
        "seat_counts": seat_counts,
        "ranking": winners
    }


# Example / Primer
original_votes = {"A": 10000, "B": 8000, "C": 4000}
repeat_old = {"A": 800, "B": 400, "C": 200}
repeat_new = {"A": 500, "B": 700, "C": 300}
S = 10

# Original seats / Originalni mandati
orig = dhondt_repeated(
    original_votes=original_votes,
    repeat_old={"A": 0, "B": 0, "C": 0},
    repeat_new={"A": 0, "B": 0, "C": 0},
    seats=S,
    threshold=0
)

# Corrected seats / Korigovani mandati
corr = dhondt_repeated(
    original_votes=original_votes,
    repeat_old=repeat_old,
    repeat_new=repeat_new,
    seats=S,
    threshold=0
)

print("Original seats / Originalni mandati:")
print(orig["seat_counts"])

print("\nCorrected vote totals / Korigovani ukupni glasovi:")
print(corr["corrected_votes"])

print("\nCorrected seats / Korigovani mandati:")
print(corr["seat_counts"])

print("\nMandate shift / Promena mandata:")
shift = {p: corr["seat_counts"][p] - orig["seat_counts"][p] for p in original_votes}
print(shift)

print("\nTop quotients after correction / Najveći količnici posle korekcije:")
for row in corr["ranking"]:
    print(row) 

Expected output:

Original seats / Originalni mandati:
{'A': 5, 'B': 4, 'C': 1}

Corrected vote totals / Korigovani ukupni glasovi:
{'A': 9700, 'B': 8300, 'C': 4100}

Corrected seats / Korigovani mandati:
{'A': 4, 'B': 4, 'C': 2}

Mandate shift / Promena mandata:
{'A': -1, 'B': 0, 'C': 1}

Komentariši

Vaša email adresa neće biti objavljivana. Neophodna polja su označena sa *