tests/testthat/test-stv.R

test_that('Wikipedia example works', {
    # version from Sep 2019
    # https://en.wikipedia.org/w/index.php?title=Single_transferable_vote&oldid=917689122
    data(food_election)
    stv.food <- stv(food_election, nseats = 3, eps = 1, quiet = TRUE)
    # check winners
    expect_equal(stv.food$elected, c("Chocolate", "Oranges", "Strawberries"))
    # Strawberries was elected on 4th count
    s <- summary(stv.food)
    expect_true("4" %in% names(s))
    expect_true("Strawberries" %in% s[["4"]])
    view(stv.food)
    }
)

test_that('IMS Council example works', {
    # Example of the IMS Council voting
    data(ims_election)
    stv.ims <- stv(ims_election, nseats = 5, quiet = TRUE)
    filename <- tempfile()
    png(filename=filename)
    view(stv.ims)
    plot(stv.ims)
    image(stv.ims)
    dev.off()
    expect_true(file.info(filename)['size'] > 0)
    unlink(filename)
    # there should be 29 invalid votes
    expect_equal(nrow(stv.ims$invalid.votes), 29)
    expect_equal(nrow(stv.ims$data), nrow(ims_election) - 29)
    expect_equal(stv.ims$elected[1], "Jasper")
    
})

test_that('Dublin West 2002 example works', {
    skip_on_cran() # takes longer to run
    # Example of Dublin West 2002 elections
    # https://en.wikipedia.org/wiki/Dublin_West#2002_general_election
    data(dublin_west)
    stv.dublin <- stv(dublin_west, nseats = 3, eps = 1, quiet = TRUE)
    expect_equal(stv.dublin$elected, c("Lenihan", "Higgins", "Burton"))
    expect_in(c("Ryan", "Morrissey", "Smyth"), 
              colnames(stv.dublin$elect.elim)[colSums(stv.dublin$elect.elim < 0) == 1])
})

test_that('Example of Small Committee works', {
    # Example of a small committee dataset
    # with four candidates (C) and four
    # voting committee members (uses tie-breaking)
    votes <- data.frame(C1=c(3,2,1,3), C2=c(2,1,2,4),
                        C3=c(4,3,3,1), C4=c(1,4,4,2))
    stv.small <- stv(votes, nseats = 2, quiet = TRUE)
    expect_equal(stv.small$elected, c("C4", "C2"))
    expect_in(stv.small$ties[c(1, 3)],  "fo") # forward tiebreak on two counts
})

test_that('Equal ranking and correction works', {
    votes <- data.frame(C1=c(3,2,1,3), C2=c(1,1,2,0),
                        C3=c(4,3,3,1), C4=c(1,4,2,2))
    expect_warning((stv.corr <- stv(votes, nseats = 2, equal.ranking = TRUE)))
    expect_equal(stv.corr$elected, c("C2", "C1"))
    # vote #3 was corrected by stv (C3 changed to rank 4)
    cor.votes <- correct.ranking(votes, quiet = TRUE)
    expect_equal(cor.votes[3, "C3"], 4)
})

test_that('Votes are weighted correctly', {
    # Example of weighted votes
    votes <- data.frame(C1=c(3,2,1,3), C2=c(2,1,2,4),
                        C3=c(4,3,3,1), C4=c(1,4,4,2),
                        weight = c(1, 2, 1, 2.5)
                        )
    stv.weight <- stv(votes, nseats = 2, weight.column = "weight", quiet = TRUE)
    # check that the first round is multiplied by the weights
    expect_equal(stv.weight$preferences[1,], colSums(votes[, "weight"]*(votes[, 1:4] == 1)))
})


test_that('Votes with equat ranking are weighted correctly', {
    # Example of weighted votes
    votes <- data.frame(C1=c(3,2,1,3), C2=c(1,1,2,0),
                        C3=c(4,3,3,1), C4=c(1,4,2,2),
                        weight = c(1, 2, 1, 4)
                        )
    expect_warning((stv.eq.weight <- stv(votes, nseats = 2, 
                                      weight.column = "weight", 
                                      equal.ranking = TRUE)))
    cor.votes <- correct.ranking(votes[, 1:4], quiet = TRUE)
    expect_equal(as.vector(stv.eq.weight$preferences[1,]), 
                 c(1, # from voter 3
                   2.5, # 0.5 from voter 1 (shared between C2 and C4) + 2 from voter 2
                   4, # from voter 4
                   0.5 # from voter 1 (shared between C2 and C4)
                   ))
    
})

test_that('Preferences are imputed correctly', {
    # Example of imputing preferences
    # (third voter has a conflict of interest with candidate C2)
    votes <- data.frame(C1=c(3,2,1,3), C2=c(2,1,-1,0),
                        C3=c(4,3,3,1), C4=c(1,4,2,2))
    res <- stv(votes, nseats = 2, impute.missing = TRUE, quiet = TRUE)
    cor.votes <- corrected.votes(res)
    expect_equal(as.vector(cor.votes$imputed), c(NA, 2, NA, NA))
    expect_equal(as.vector(cor.votes$new), c(1, 2, 4, 3))
    # imputed rank 2, as it is the median(c(2, 1, 4))
    # where the last 4 was derived as the median of missing ranks 
    # in vote four. The imputation can be also performed via impute.ranking()
    expect_true(all(impute.ranking(votes) == res$data))
    
    # check that weighting of imputed data works
    wvotes <- cbind(votes, weight = c(1, 2, 1, 2.5))
    wres <- stv(wvotes, nseats = 2, impute.missing = TRUE, 
                weight.column = "weight", quiet = TRUE)
    cvotes <- correct.ranking(votes, quiet = TRUE)
    expect_equal(wres$preferences[1,], 
                 colSums(wvotes[, "weight"]*(cvotes == 1)))
})

test_that('Reserving seats works', {
    # Example of using reserved seats: 
    # e.g. reserve two seats for students
    data(ims_election)
    students <- c("Declan", "Claire", "Oscar")
    stv.ims <- stv(ims_election, nseats = 5, group.nseats = 2, 
            group.members = students, quiet = TRUE) # students
    expect_true(sum(stv.ims$elected %in% students) >= 2)
    
    # check weighting with reserved seats
    votes <- cbind(ims_election, weight = runif(nrow(ims_election), 0.5, 10))
    stv.wims <- stv(votes, nseats = 5, group.nseats = 2, quiet = TRUE,
                    group.members = students, weight.column = "weight")
    expect_equal(stv.wims$preferences[1,], 
                 colSums(votes[-as.integer(rownames(stv.wims$invalid.votes)), "weight"]*(stv.wims$data == 1)))
    expect_equal(as.vector(attr(stv.wims$data, "weights")), 
                 votes[-as.integer(rownames(stv.wims$invalid.votes)), "weight"])
    expect_equal(as.vector(attr(stv.wims$invalid.votes, "weights")), 
                 votes[as.integer(rownames(stv.wims$invalid.votes)), "weight"])
    expect_true(sum(stv.wims$elected %in% students) >= 2)
})

test_that('Removing candidates and partially invalidating ballots works', {
    # Example of removing candidates from original votes
    data(ims_election)
    updated.votes <- remove.candidate(ims_election, c("Jasper", "Tilmann"))
    res <- stv(updated.votes, nseats = 5, quiet = TRUE)
    expect_false(any(c("Jasper", "Tilmann") %in% res$elected))
    
    # the same with weighting
    wvotes <- cbind(updated.votes, weight = runif(nrow(updated.votes), 0.5, 10))
    wres <- stv(wvotes, nseats = 5, weight.column = "weight", quiet = TRUE)
    expect_false(any(c("Jasper", "Tilmann") %in% wres$elected))
    expect_equal(wres$preferences[1,], 
                 colSums(wvotes[-as.integer(rownames(wres$invalid.votes)), "weight"]*(wres$data == 1)))
    
    # Example of accepting partially invalid ballots
    expect_warning((res.part <- stv(ims_election, invalid.partial = TRUE)))

    # There are now 24 invalid votes instead of 29, 
    # because 5 were corrected (ranking before the first 
    # gap/tie is valid, after that it is 0)
    cor.votes <- corrected.votes(res.part)
    expect_equal(nrow(cor.votes$new), 5)
    expect_equal(nrow(res.part$invalid.votes), 24)
    expect_equal(nrow(invalid.votes(res.part)), 24)
    expect_equal(nrow(res.part$data), nrow(ims_election) - 24)
    
    # the same with weighting
    wvotes <- cbind(ims_election, weight = runif(nrow(updated.votes), 0.5, 10))
    expect_warning((wres.part <- stv(wvotes, invalid.partial = TRUE, weight.column = "weight")))
    expect_equal(wres.part$preferences[1,], 
                 colSums(wvotes[-as.integer(rownames(wres.part$invalid.votes)), "weight"]*(wres.part$data == 1)))
    expect_equal(as.vector(attr(wres.part$data, "weights")), 
                 wvotes[-as.integer(rownames(wres.part$invalid.votes)), "weight"])
    expect_equal(as.vector(attr(wres.part$invalid.votes, "weights")), 
                 wvotes[as.integer(rownames(wres.part$invalid.votes)), "weight"])
})

Try the vote package in your browser

Any scripts or data that you put into this service are public.

vote documentation built on Feb. 28, 2026, 1:06 a.m.