1. A small correction with large consequences
I would like to begin by thanking student Jovan Todorov, who drew my attention to an important technical detail of Serbian local-election law: when the 3% threshold is checked, the relevant denominator is not only the sum of valid votes cast for electoral lists, but the broader category of votes cast, which in Serbian election administration includes valid and invalid ballots found in the ballot box. That point matters because my earlier blog post explained, correctly, how mandates are recalculated after repeated voting by replacing the old result from the repeated polling station with the new result and then reapplying the electoral formula. But this legal detail changes one specific step in that calculation: the threshold test.
This is not merely a lawyer’s footnote. In a competitive local election, a list near the 3% threshold may qualify for seat allocation if the denominator is computed using only valid list votes, but fail to qualify if the denominator includes invalid ballots as well. Once that happens, the entire mandate distribution can change. In other words, the D’Hondt procedure itself may remain exactly the same, but the set of lists allowed to enter that procedure changes. That is a different kind of correction from the one discussed in my previous post: there, the focus was the replacement of annulled polling-station results by repeated-vote results; here, the focus is the legal base used to determine whether a list may participate in seat allocation at all.
This matters in practice because repeated voting has been a live issue in Serbia’s 2026 local elections. In Knjaževac, for example, local media reported that voting was repeated at polling station no. 8 after the original vote there had been annulled, and that the polling station had 817 registered voters. That kind of case makes the legal and computational question very concrete: once the repeated result is inserted into the aggregate totals, what exactly is the legally correct way to test the 3% threshold and then distribute mandates?
2. What changes and what does not
The core logic remains the same as before. Repeated voting does not create a new electoral theory. It simply changes the legally valid totals. If the original municipality-level total for a list was \(V_p\), and if the old result at repeated polling stations is replaced by the new one, then the corrected total is:
$$ V_p^* = V_p – \sum_{r \in R} v_{pr}^{(old)} + \sum_{r \in R} v_{pr}^{(new)}. $$
That part does not change. Nor does the D’Hondt allocation rule itself. If a list is eligible to participate in seat allocation, its seats are still determined by ranking the quotients obtained by dividing its corrected vote total by \(1,2,3,\dots\), together with the quotients of the other eligible lists. What changes is the threshold test. In the Serbian local-election framework, Article 61 states that seats are distributed only among lists that have won at least 3% of the votes cast, while Article 62 then allocates seats using the votes won by the lists through the highest-quotient rule. The law separately records valid and invalid ballots, which is precisely why this correction matters.
So the corrected legal logic is the following. Let \(I^{(0)}\) be the original number of invalid ballots in the electoral unit, and let \(I^{(old)}\) and \(I^{(new)}\) be the invalid ballots at the repeated polling stations before and after repeated voting. Then the corrected invalid-ballot total is:
$$ I^* = I^{(0)} – I^{(old)} + I^{(new)}. $$
The corrected threshold denominator is then:
$$ T^* = \sum_{p=1}^{P} V_p^* + I^*. $$
And the 3% threshold test becomes:
$$ \frac{V_p^*}{T^*} \geq 0.03. $$
That is the entire correction. The denominator for the threshold is broader than the denominator used inside the D’Hondt formula. D’Hondt continues to use only the corrected valid votes for the lists. Invalid ballots matter for threshold eligibility, not for quotient computation.
3. Why this is analytically important
This distinction may look minor, but it has real analytical consequences. In a repeated-voting case, there are now two separate corrections. The first is the familiar correction of vote totals: old polling-station results are replaced by the repeated results. The second is the legal correction in the threshold denominator: if the repeated polling station also changes the number of invalid ballots, then the 3% threshold must be checked against the corrected total of valid plus invalid ballots cast. If one ignores that second correction and uses only the corrected valid votes as the denominator, one may wrongly include a list in seat allocation.
That is precisely the kind of issue election forensics should not overlook. Election forensics is not only about detecting suspicious statistical patterns. It is also about reconstructing the legally correct numerical mechanism by which votes become mandates. If the law says that threshold eligibility depends on all ballots cast, then an analyst who uses only valid list votes for the threshold is not reproducing the Serbian electoral rule accurately, even if the D’Hondt calculation itself is coded correctly.
4. Political implications of including invalid ballots in the threshold base
From a political-strategic perspective, including invalid ballots in the threshold denominator usually benefits larger and already established lists, while excluding them usually benefits smaller lists near the threshold. The reason is mechanical: when invalid ballots are included, the denominator becomes larger, so the same number of valid votes produces a lower percentage. That makes it harder for borderline lists to cross 3%. In many real local elections, those borderline lists are more likely to be opposition lists, local civic groups, or smaller entrants than the main ruling list or the strongest opposition list. For that reason, including invalid ballots will often work to the advantage of the dominant larger actors and to the disadvantage of smaller challengers. Still, this is not a rule about “government versus opposition” in the abstract. The true dividing line is not political label but position relative to the threshold: whichever list is hovering around 3% is the list most affected by whether invalid ballots are included or excluded.
5. Complete R code with a numerical example
The R code below implements the corrected Serbian logic. It does four things: it replaces old repeated-station votes with new ones; it recalculates invalid ballots; it checks the threshold using valid + invalid ballots cast; and it allocates mandates among eligible lists using D’Hondt with Serbian tie-breaking by larger total votes.
#=================================================
# Repeated voting – Mandates allocation
#=================================================
serbia_repeat_dhondt <- function(original_votes,
repeat_old,
repeat_new,
seats,
original_invalid = 0,
repeat_old_invalid = 0,
repeat_new_invalid = 0,
threshold = 0.03) {
# original_votes = original valid votes by list / originalni važeći glasovi po listama
# repeat_old = old valid votes at repeated stations / stari važeći glasovi na ponovljenim mestima
# repeat_new = new valid votes at repeated stations / novi važeći glasovi na ponovljenim mestima
# original_invalid = original invalid ballots / originalni nevažeći listići
# repeat_old_invalid = old invalid ballots at repeated stations / stari nevažeći listići
# repeat_new_invalid = new invalid ballots at repeated stations / novi nevažeći listići
# seats = total seats / ukupan broj mandata
# Align party names / Uskladi nazive lista
parties <- names(original_votes)
repeat_old <- repeat_old[parties]
repeat_new <- repeat_new[parties]
# Correct valid totals / Koriguj važeće glasove
corrected_votes <- original_votes - repeat_old + repeat_new
# Correct invalid totals / Koriguj nevažeće listiće
corrected_invalid <- original_invalid - repeat_old_invalid + repeat_new_invalid
# Total ballots cast = valid + invalid / Ukupan broj ubačenih listića = važeći + nevažeći
total_cast <- sum(corrected_votes) + corrected_invalid
# Wrong threshold base for comparison / Pogrešna osnova za poređenje
total_valid_only <- sum(corrected_votes)
# Eligibility under wrong and correct rules / Učešće po pogrešnom i ispravnom pravilu
eligible_valid_only <- corrected_votes / total_valid_only >= threshold
eligible_cast <- corrected_votes / total_cast >= threshold
# D'Hondt allocator / D’Hondt raspodela
allocate_dhondt <- function(votes, seats) {
qtab <- do.call(rbind, lapply(names(votes), function(p) {
divs <- seq_len(seats)
data.frame(
party = rep(p, seats),
total_votes = rep(unname(votes[p]), seats),
divisor = divs,
quotient = unname(votes[p]) / divs,
stringsAsFactors = FALSE
)
}))
# Serbian tie-break: higher total votes wins / Srpsko pravilo: prednost većem ukupnom broju glasova
qtab <- qtab[order(-qtab$quotient, -qtab$total_votes, qtab$divisor, qtab$party), ]
winners <- qtab[seq_len(seats), ]
seat_counts <- table(factor(winners$party, levels = names(votes)))
list(seat_counts = seat_counts, ranking = winners)
}
# Seats under wrong threshold base / Mandati po pogrešnoj osnovi cenzusa
seats_valid_only <- allocate_dhondt(corrected_votes[eligible_valid_only], seats)
# Seats under correct Serbian rule / Mandati po ispravnom srpskom pravilu
seats_cast <- allocate_dhondt(corrected_votes[eligible_cast], seats)
list(
corrected_votes = corrected_votes,
corrected_invalid = corrected_invalid,
total_valid_only = total_valid_only,
total_cast = total_cast,
eligible_valid_only = eligible_valid_only,
eligible_cast = eligible_cast,
seats_valid_only = seats_valid_only$seat_counts,
seats_cast = seats_cast$seat_counts,
ranking_valid_only = seats_valid_only$ranking,
ranking_cast = seats_cast$ranking
)
}
# Example / Primer
original_votes <- c(A = 12000, B = 8000, C = 5000, D = 690)
repeat_old <- c(A = 300, B = 200, C = 100, D = 20)
repeat_new <- c(A = 250, B = 180, C = 90, D = 130)
original_invalid <- 1150
repeat_old_invalid <- 30
repeat_new_invalid <- 80
S <- 33
res <- serbia_repeat_dhondt(
original_votes = original_votes,
repeat_old = repeat_old,
repeat_new = repeat_new,
seats = S,
original_invalid = original_invalid,
repeat_old_invalid = repeat_old_invalid,
repeat_new_invalid = repeat_new_invalid,
threshold = 0.03
)
print("Corrected valid votes / Korigovani važeći glasovi:")
print(res$corrected_votes)
print("Corrected invalid ballots / Korigovani nevažeći listići:")
print(res$corrected_invalid)
print("Eligibility if threshold uses only valid votes / Cenzus ako koristi samo važeće glasove:")
print(res$eligible_valid_only)
print("Eligibility if threshold uses valid + invalid ballots / Cenzus ako koristi važeće + nevažeće:")
print(res$eligible_cast)
print("Seats under wrong threshold base / Mandati po pogrešnoj osnovi:")
print(res$seats_valid_only)
print("Seats under correct Serbian rule / Mandati po ispravnom srpskom pravilu:")
print(res$seats_cast)
Using this example, the corrected totals are: A = 11,950, B = 7,980, C = 4,990, D = 800, Invalid ballots = 1,200
So the corrected total of valid votes is 25,720, while the corrected total of ballots cast is 26,920. That means list D has 3.11% if one wrongly uses only valid votes, but only 2.97% if one correctly uses valid plus invalid ballots. Under the wrong denominator D enters the seat allocation; under the correct Serbian denominator D does not. The resulting seat distributions are:
- Wrong threshold base (valid only): A = 16, B = 10, C = 6, D = 1
- Correct Serbian rule (valid + invalid): A = 16, B = 11, C = 6, D excluded
So the legal correction changes the mandate distribution by one seat: D loses the seat it would wrongly receive, and B keeps the eleventh seat. This is exactly the kind of small technical correction that can matter a great deal in practice.
6. Manual verification
The manual check is simple and decisive.
Start with the corrected totals: A = 11,950, B = 7,980, C = 4,990, D = 800, Invalid = 1,200
First compute the threshold. If one wrongly uses only valid list votes, the denominator is: 11,950 + 7,980 + 4,990 + 800 = 25,720.
Then list D has: 800 / 25,720 = 0.03110 = 3.11%.
So under the wrong rule, D qualifies. If one uses the correct Serbian rule, the denominator is: 25,720 + 1,200 = 26,920.
Then list D has: 800 / 26,920 = 0.02972 = 2.97%.
So under the correct rule, D does not qualify.
Now check the decisive D’Hondt quotients under the wrong threshold base, where D is allowed into seat allocation. The key quotients around the lower boundary of the 33-seat allocation are:
- C / 6 = 4,990 / 6 = 831.67
- D / 1 = 800 / 1 = 800.00
- B / 10 = 7,980 / 10 = 798.00
- A / 15 = 11,950 / 15 = 796.67
- A / 16 = 11,950 / 16 = 746.88
- B / 11 = 7,980 / 11 = 725.45
Because D’s first quotient is 800, it enters the top 33 quotients. That gives D one seat and pushes B’s eleventh quotient (725.45) below the boundary. So with the wrong threshold base the distribution is: A = 16, B = 10, C = 6, D = 1.
Under the correct Serbian rule, D is excluded before D’Hondt is even applied. Then the 33rd seat goes to B’s eleventh quotient instead of D’s first quotient, and the distribution becomes: A = 16, B = 11, C = 6.
That is the complete logic. The D’Hondt formula itself did not change. The decisive change happened one step earlier: in the legal threshold denominator.
7. Comment and conclusion
This correction is conceptually narrow but substantively important. It does not overturn the standard theory of mandate recalculation after repeated voting. It refines it. The broader logic remains unchanged: old repeated-station results are replaced by the new ones, corrected totals are computed, and the legal allocation rule is applied again. What changes in the Serbian local-election setting is the threshold denominator. If invalid ballots are legally part of the “votes cast” base for the 3% threshold, then they must be included there. Ignoring them can produce a formally neat but legally wrong result.
That is also why this point belongs in election forensics. Election forensics is strongest when it combines statistical reasoning with exact institutional reading. A technically elegant formula is not enough if it is attached to the wrong legal denominator. Conversely, once the legal rule is interpreted correctly, the numerical implementation becomes straightforward. This post therefore serves as a correction and refinement of the previous one: the general logic of repeated-vote recalculation still stands, but for Serbian local elections the threshold step must be handled with special care.
8. Python code
Below is the equivalent Python implementation.
from typing import Dict, Any
def serbia_repeat_dhondt(original_votes: Dict[str, float],
repeat_old: Dict[str, float],
repeat_new: Dict[str, float],
seats: int,
original_invalid: float = 0,
repeat_old_invalid: float = 0,
repeat_new_invalid: float = 0,
threshold: float = 0.03) -> Dict[str, Any]:
# original_votes = original valid votes by list / originalni važeći glasovi po listama
# repeat_old = old valid votes at repeated stations / stari važeći glasovi na ponovljenim mestima
# repeat_new = new valid votes at repeated stations / novi važeći glasovi na ponovljenim mestima
# original_invalid = original invalid ballots / originalni nevažeći listići
# repeat_old_invalid = old invalid ballots at repeated stations / stari nevažeći listići
# repeat_new_invalid = new invalid ballots at repeated stations / novi nevažeći listići
# seats = total seats / ukupan broj mandata
# Align party names / Uskladi nazive lista
parties = list(original_votes.keys())
old_aligned = {p: repeat_old.get(p, 0) for p in parties}
new_aligned = {p: repeat_new.get(p, 0) for p in parties}
# Correct valid totals / Koriguj važeće glasove
corrected_votes = {
p: original_votes[p] - old_aligned[p] + new_aligned[p]
for p in parties
}
# Correct invalid totals / Koriguj nevažeće listiće
corrected_invalid = original_invalid - repeat_old_invalid + repeat_new_invalid
# Total ballots cast / Ukupan broj ubačenih listića
total_valid_only = sum(corrected_votes.values())
total_cast = total_valid_only + corrected_invalid
# Threshold tests / Provera cenzusa
eligible_valid_only = {
p: (corrected_votes[p] / total_valid_only) >= threshold
for p in parties
}
eligible_cast = {
p: (corrected_votes[p] / total_cast) >= threshold
for p in parties
}
# D'Hondt allocator / D’Hondt raspodela
def allocate_dhondt(votes: Dict[str, float], seats: int):
qtab = []
for p, v in votes.items():
for d in range(1, seats + 1):
qtab.append({
"party": p,
"total_votes": v,
"divisor": d,
"quotient": v / d
})
# Serbian tie-break: higher total votes wins / Srpsko pravilo: prednost većem ukupnom broju glasova
qtab_sorted = sorted(
qtab,
key=lambda row: (-row["quotient"], -row["total_votes"], row["divisor"], row["party"])
)
winners = qtab_sorted[:seats]
seat_counts = {p: 0 for p in votes.keys()}
for row in winners:
seat_counts[row["party"]] += 1
return {"seat_counts": seat_counts, "ranking": winners}
# Seats under wrong threshold base / Mandati po pogrešnoj osnovi cenzusa
votes_valid_only = {p: corrected_votes[p] for p in parties if eligible_valid_only[p]}
seats_valid_only = allocate_dhondt(votes_valid_only, seats)
# Seats under correct Serbian rule / Mandati po ispravnom srpskom pravilu
votes_cast = {p: corrected_votes[p] for p in parties if eligible_cast[p]}
seats_cast = allocate_dhondt(votes_cast, seats)
return {
"corrected_votes": corrected_votes,
"corrected_invalid": corrected_invalid,
"total_valid_only": total_valid_only,
"total_cast": total_cast,
"eligible_valid_only": eligible_valid_only,
"eligible_cast": eligible_cast,
"seats_valid_only": seats_valid_only["seat_counts"],
"seats_cast": seats_cast["seat_counts"],
"ranking_valid_only": seats_valid_only["ranking"],
"ranking_cast": seats_cast["ranking"]
}
# Example / Primer
original_votes = {"A": 12000, "B": 8000, "C": 5000, "D": 690}
repeat_old = {"A": 300, "B": 200, "C": 100, "D": 20}
repeat_new = {"A": 250, "B": 180, "C": 90, "D": 130}
original_invalid = 1150
repeat_old_invalid = 30
repeat_new_invalid = 80
S = 33
res = serbia_repeat_dhondt(
original_votes=original_votes,
repeat_old=repeat_old,
repeat_new=repeat_new,
seats=S,
original_invalid=original_invalid,
repeat_old_invalid=repeat_old_invalid,
repeat_new_invalid=repeat_new_invalid,
threshold=0.03
)
print("Corrected valid votes / Korigovani važeći glasovi:")
print(res["corrected_votes"])
print("\nCorrected invalid ballots / Korigovani nevažeći listići:")
print(res["corrected_invalid"])
print("\nEligibility if threshold uses only valid votes / Cenzus ako koristi samo važeće glasove:")
print(res["eligible_valid_only"])
print("\nEligibility if threshold uses valid + invalid ballots / Cenzus ako koristi važeće + nevažeće:")
print(res["eligible_cast"])
print("\nSeats under wrong threshold base / Mandati po pogrešnoj osnovi:")
print(res["seats_valid_only"])
print("\nSeats under correct Serbian rule / Mandati po ispravnom srpskom pravilu:")
print(res["seats_cast"])
References
Kohler, U., & Zeh, J. (2012). Apportionment methods. The Stata Journal, 12(3), 375–392. https://doi.org/10.1177/1536867X1201200303 (Sage Journals)
Medzihorsky, J. (2019). Rethinking the D’Hondt method. Political Research Exchange, 1(1), Article 1625712. https://doi.org/10.1080/2474736X.2019.1625712 (Taylor & Francis Online)
Gallagher, M., & Mitchell, P. (Eds.). (2005). The politics of electoral systems. Oxford University Press. https://doi.org/10.1093/0199257566.001.0001
Legal and contextual sources
Republic of Serbia. Law on Local Elections (English version; Articles 58–62). Venice Commission document repository. (Venice Commission)
Paragraf Lex. Zakon o lokalnim izborima. (Paragraf)
Za Media. Ponovljeno glasanje na biračkom mestu br. 8 u Knjaževcu 29. aprila. 25 April 2026. (zamedia.rs)
Euronews Srbija. Ponovljeno glasanje u Knjaževcu: Izlaznost 39,41 odsto. 29 April 2026. (euronews.rs)