| Title: | Canadian Voting and Policy Attitudes |
|---|---|
| Description: | The main focus of the package is to give weighted party support and issue attitudes by demographic groups over time. As of 0.2.0, policy-issue and vote data are read on demand from the cp3 project's hosted Parquet files (see update_cvpa_data()) rather than bundled with the package, covering several hundred policy questions across seven issue domains in addition to vote choice/intention. The vote data are described in Lucas, Romualdi, Armstrong, and Goodyear-Grant (2025) <doi:10.1017/S0008423925000241>. |
| Authors: | Dave Armstrong [aut, cre] (ORCID: <https://orcid.org/0000-0002-9358-2489>), Jack Lucas [aut] (ORCID: <https://orcid.org/0000-0002-3498-7267>), Tyler Romualdi [aut] |
| Maintainer: | Dave Armstrong <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.3.0 |
| Built: | 2026-11-14 00:29:52 UTC |
| Source: | https://github.com/cran/cvpa |
Where [update_cvpa_data()] stores downloaded Parquet files and every other cvpa function reads them from. Defaults to a per-user cache directory managed by R (see [tools::R_user_dir()]); override with 'options(cvpa.cache_dir = ...)'.
cvpa_cache_dir()cvpa_cache_dir()
The cache directory path (created if it doesn't exist yet).
# Point the cache at a temporary directory (the default is a per-user # directory under tools::R_user_dir("cvpa", "cache")). old <- options(cvpa.cache_dir = tempfile("cvpa-cache-")) cvpa_cache_dir() unlink(cvpa_cache_dir(), recursive = TRUE) options(old)# Point the cache at a temporary directory (the default is a per-user # directory under tools::R_user_dir("cvpa", "cache")). old <- options(cvpa.cache_dir = tempfile("cvpa-cache-")) cvpa_cache_dir() unlink(cvpa_cache_dir(), recursive = TRUE) options(old)
cvpa ships a tiny copy of the hosted cp3 Parquet layout – a handful of questions ('bri_econ_013', 'env_econ_027', 'vote_choice', 'vote_intention') and a random sample of respondents from each survey year – so that the examples in the help pages can run without a network connection. Point cvpa at it by setting the 'cvpa.data_url' option to this URL (and, so you don't touch your real cache, 'cvpa.cache_dir' to a temporary directory), then call [update_cvpa_data()] as usual.
cvpa_example_url()cvpa_example_url()
The example data is for illustration only: it is a subset, and estimates from it are not estimates from the full data. Because it is not the official data, [update_cvpa_data()] and the functions that read the cache warn while it is in use. Undo with 'options(cvpa.data_url = NULL)', or restore the options you saved.
A 'file://' URL (with a trailing slash) for the bundled example data, suitable for 'options(cvpa.data_url = ...)'.
old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(questions = "bri_econ_013", quiet = TRUE) # warns: example data head(question_catalog(domain = "econ")) unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(questions = "bri_econ_013", quiet = TRUE) # warns: example data head(question_catalog(domain = "econ")) unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)
Every table returned by [question_data()] carries these columns in addition to 'response', 'weight', and 'year' – eleven demographic variables (see [demographic_vars] for what each one measures) plus ‘vote_intention' and 'vote_choice', which travel with every question’s data and can be used as grouping variables just like a demographic (this is what lets you, say, break a policy question down by how people voted). Used to validate the 'grouping_vars'/'grouping_var' argument in [wtd_response()] and [response_gap_analysis()].
cvpa_grouping_varscvpa_grouping_vars
A character vector (13 elements) of the column names that can be used as grouping variables: 'vote_intention', 'vote_choice' and the eleven demographic variables in [demographic_vars].
[demographic_vars], [question_data()]
cvpa_grouping_varscvpa_grouping_vars
Every table [question_data()] returns carries these eleven demographic columns (denormalized onto each response row by cp3's data pipeline), in addition to 'vote_intention' and 'vote_choice', which are also always present and usable as grouping variables in their own right (see [cvpa_grouping_vars]). Category lists are given in the order the underlying 'code_labels' table sorts them, which is also the factor level order [question_data()] applies.
demographic_varsdemographic_vars
A data frame with 11 rows and 3 variables:
Column name as it appears in [question_data()]'s output
What the variable measures
Ordered category labels, separated by '"; "'
A data frame with one row for each of the eleven demographic variables and the columns 'variable', 'description' and 'categories' (described under Format).
[cvpa_grouping_vars], [question_data()]
demographic_varsdemographic_vars
The function does a difference of proportions test for the two identified groups for each of the parties. Note, for gender, there are not many surveys with non-binary/other response options so those are removed from consideration here.
gap_analysis( data, vote_type = c("intention", "choice"), incl_undecided = FALSE, years = NULL, grouping_var = c("age_cats", "religion", "degree", "gender", "province", "region", "language", "union_household", "community_size", "occupation"), levels = NULL, ... )gap_analysis( data, vote_type = c("intention", "choice"), incl_undecided = FALSE, years = NULL, grouping_var = c("age_cats", "religion", "degree", "gender", "province", "region", "language", "union_household", "community_size", "occupation"), levels = NULL, ... )
data |
Data from inst/extdata/integrated_with_weights.RDS. |
vote_type |
Use vote intention or choice |
incl_undecided |
If using vote intention, should undecided voters be included? |
years |
Select desired years. 'NULL' (default) uses every year in 'data' – so newly added years are never silently dropped. |
grouping_var |
For which group do you want separate estimates (gender, degree, union_household, community_size) |
levels |
Character vector of length 2 giving values of 'grouping_var' to use for comparison. 'NULL' is valid if 'grouping_var' only has two valid values. Otherwise, values must be specified. |
... |
Other arguments to be passed down, not implemented |
See [response_gap_analysis()] for the same test generalized to any policy question, not just vote choice/intention.
A data frame (a tibble, grouped by 'year') with one row per year and party for which both groups have enough respondents to test. The columns are 'year'; 'vote', the party; 'cnt_<level>' and 'n_<level>' for each of the two groups (for example 'cnt_Man' and 'n_Woman'), the weighted count of the group's respondents who chose that party and the weighted count of the group's respondents in that year; 'flag' ('"p-val reliable"' or '"p-val unreliable"', see [make_flag()]); ‘estimate1' and 'estimate2', the party’s weighted share in the first and second group; 'p.value', from a two-sample test of equal proportions ([stats::prop.test()]); 'diff', 'estimate2 - estimate1'; and 'conf.low' and 'conf.high', the limits of the 95
data(vote_data) gap_analysis(vote_data, "intention", years=2000:2022, grouping_var="gender")data(vote_data) gap_analysis(vote_data, "intention", years=2000:2022, grouping_var="gender")
Makes a flag to identify if the p-value for the difference of proportions test is reliable (all expected counts greater than 5).
make_flag(x, n)make_flag(x, n)
x |
A vector of two values of category counts. |
n |
A vector of two values of sample sizes |
'"p-val reliable"' if every expected count is at least 5, otherwise '"p-val unreliable"'.
make_flag(c(30, 40), c(100, 120)) # large counts: reliable make_flag(c(2, 3), c(10, 20)) # small counts: unreliablemake_flag(c(30, 40), c(100, 120)) # large counts: reliable make_flag(c(2, 3), c(10, 20)) # small counts: unreliable
One row per question, deduplicated across the several hundred survey-wave rows each question has in the underlying catalog (see [update_cvpa_data()]). Covers every domain in the cp3 project, including ‘"vote"' ('vote_choice'/'vote_intention') – from cvpa’s point of view vote is just one more question, decoded and analyzed the same way as any policy question (see [question_data()], [wtd_response()]).
question_catalog(domain = NULL, type = NULL)question_catalog(domain = NULL, type = NULL)
domain |
Optional character vector of domain codes to restrict to (see the ‘domain' column of this function’s own output for valid values, e.g. '"econ"', '"soim"', '"vote"'). |
type |
Optional character vector of question types to restrict to: '"b"' (binary), '"lsm"' (3-point less/same/more-style), '"nsa"' (3-point never/sometimes/always-style), or '"party"' (vote_choice, vote_intention). |
A data frame with one row per question: 'question', 'domain', 'domain_label', 'issue_label', 'question_wording', 'value_labels' (comma-separated, ascending – see [question_response_labels()] for a decoded, one-row-per-category version), 'type', and 'n_waves' (how many survey waves asked this question).
# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(quiet = TRUE) question_catalog(domain = "econ") question_catalog(type = "party") unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(quiet = TRUE) question_catalog(domain = "econ") question_catalog(type = "party") unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)
Reads the locally cached 'resp/question=<question>/data_0.parquet' file (see [update_cvpa_data()]) and, by default, decodes 'response' and every demographic/vote-grouping column (see [cvpa_grouping_vars]) into labeled factors. Works identically for a policy question and for '"vote_choice"'/'"vote_intention"' – vote is just one more question.
question_data(question, years = NULL, decode = TRUE)question_data(question, years = NULL, decode = TRUE)
question |
A question code from [question_catalog()], e.g. '"bri_econ_013"' or '"vote_choice"'. |
years |
Optional integer vector to filter 'year' to. |
decode |
If 'TRUE' (default), decode 'response' and the demographic/vote columns into labeled factors; if 'FALSE', return the raw numeric codes as stored in the Parquet file. |
A data frame with one row per respondent-question-wave: 'response', 'weight', 'year', 'response_id', 'survey_id', and the columns in [cvpa_grouping_vars].
# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(questions = "bri_econ_013", quiet = TRUE) d <- question_data("bri_econ_013") head(d) table(d$year) unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(questions = "bri_econ_013", quiet = TRUE) d <- question_data("bri_econ_013") head(d) table(d$year) unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)
A decoded, one-row-per-category view of what [question_catalog()] reports as a single comma-separated 'value_labels' string (or, for 'vote_choice'/'vote_intention', of the relevant rows of 'code_labels.parquet') – the same table [question_data()] uses internally to turn a question's raw 'response' codes into a labeled factor.
question_response_labels(question)question_response_labels(question)
question |
A question code, e.g. '"bri_econ_013"' or '"vote_choice"'. |
A data frame with 'code', 'label', and 'sort_order', ordered by 'sort_order'.
# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(quiet = TRUE) question_response_labels("bri_econ_013") question_response_labels("vote_choice") unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(quiet = TRUE) question_response_labels("bri_econ_013") question_response_labels("vote_choice") unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)
Generalizes [gap_analysis()] to any question's data (see [question_data()]), not just vote choice/intention: a difference of proportions test between two levels of 'grouping_var', for each response category, for each year.
response_gap_analysis( data, grouping_var, years = NULL, levels = NULL, avail_grps = cvpa_grouping_vars, response_var = "response" )response_gap_analysis( data, grouping_var, years = NULL, levels = NULL, avail_grps = cvpa_grouping_vars, response_var = "response" )
data |
A data frame as returned by [question_data()]. |
grouping_var |
Which grouping variable to compare two levels of; must be one of [cvpa_grouping_vars] (or 'avail_grps', if you pass a narrower/different list). |
years |
Select desired years. 'NULL' (default) uses every year in 'data'. |
levels |
Character vector of length 2 giving values of 'grouping_var' to use for comparison. 'NULL' is valid if 'grouping_var' only has two valid values in 'data'; otherwise values must be specified. |
avail_grps |
Grouping variables considered valid; defaults to [cvpa_grouping_vars]. |
response_var |
Name of the column in 'data' holding the response category; defaults to '"response"'. |
A data frame with one row per (year, response category): the two groups' counts and sample sizes (suffixed with each group's label), a reliability 'flag' (see [make_flag()]), the two estimated proportions, a p-value, a confidence interval for the difference, and 'diff'.
# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(questions = "bri_econ_013", quiet = TRUE) d <- question_data("bri_econ_013") response_gap_analysis(d, grouping_var = "gender") unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(questions = "bri_econ_013", quiet = TRUE) d <- question_data("bri_econ_013") response_gap_analysis(d, grouping_var = "gender") unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)
cvpa reads policy-attitude and vote data from Parquet files hosted alongside the cp3 web app, not from data bundled in the package. Call this first (and again whenever you want the latest data) before using [question_catalog()], [question_data()], [wtd_response()], or [response_gap_analysis()].
update_cvpa_data(questions = NULL, force = FALSE, quiet = FALSE)update_cvpa_data(questions = NULL, force = FALSE, quiet = FALSE)
questions |
Which questions' respondent-level data to fetch, in addition to the catalog: 'NULL' (default) fetches none, or a character vector of specific question codes (see [question_catalog()]). There is deliberately no "fetch everything" option – if you want the full dataset, download 'dataset.parquet' from <https://github.com/lucasjacklucas/cvpa_dataset> instead. |
force |
If 'FALSE' (default), question files already in the cache are left alone. If 'TRUE', every requested file is re-downloaded. |
quiet |
Suppress progress messages. |
The question catalog ('vbl_data.parquet') and the demographic/party value-label table ('code_labels.parquet') are small and are always refreshed. Per-question respondent-level data ('resp/question=<code>/data_0.parquet') is only fetched for the questions you name in 'questions' – there are several hundred of these across all domains, so nothing downloads them until you ask.
Downloads are written to a temporary file and checked to be valid Parquet before they replace anything in the cache, so a failed or interrupted download never leaves a corrupt file behind or overwrites a good one. If the catalog or the code labels can't be downloaded, this stops with an error (and your existing cached copies are untouched). If an individual *question* file can't be downloaded – e.g. a question that is listed in the catalog but has no respondent-level data available – it is skipped with a warning naming the question(s), and the rest are still fetched.
Data comes from the official live data, 'https://policyattitudes.ca/cp3_parquet/', unless you have changed 'options(cvpa.data_url = ...)' (for example to a local mirror, or to the small example data from [cvpa_example_url()]). If you have, this warns on every call and the functions that read the cache warn too, because the data may be a subset or out of date; 'options(cvpa.data_url = NULL)' restores the official URL.
Invisibly, 'TRUE', with an attribute '"skipped"' holding the question codes (if any) that could not be downloaded.
# These examples use the tiny example data bundled with cvpa, in a # temporary cache, so they run offline and leave your own cache alone. # For real analysis, skip the options() lines: by default cvpa downloads # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data() # just the catalog update_cvpa_data(questions = "bri_econ_013") # + one question update_cvpa_data(questions = c("vote_choice", "vote_intention")) dir(cvpa_cache_dir(), recursive = TRUE) unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)# These examples use the tiny example data bundled with cvpa, in a # temporary cache, so they run offline and leave your own cache alone. # For real analysis, skip the options() lines: by default cvpa downloads # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data() # just the catalog update_cvpa_data(questions = "bri_econ_013") # + one question update_cvpa_data(questions = c("vote_choice", "vote_intention")) dir(cvpa_cache_dir(), recursive = TRUE) unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)
Data on vote choice and intention from several surveys. The data are in long form (in intentnion/choice) and aggregate to profile across the variables 'region'-'language' described below.
A data frame with 78,614 rows and 11 variables
Factor indicating vote choice or intention depending on type
String indicating vote whever vote is intention or actual choice
Year of survey
Factor indicating region of residence: Atlantic, Quebec, Ontario, Prairies, BC, The North
Factor indicating province of residence: Newfoundland and Labrador, PEI, Nova Scotia, New Brunswick, Quebec, Ontario, Manitoba, Saskatchewan, Alberta, BC, Yukon, Northwest Territories, Nunavut
Factor identifying community size of respondent's residence: < 100,000, >= 100,000
Factor identifying respondent gender Man, Woman. Non-binary category begins in 2019 and is available in the full data on the Canadian Opinion Research Archive.
Factor identifying respondent's age category: 18-29, 30-49, 50+
Factor giving respondent's religious affiliation: Catholic, Protestant, Jewish, Other, None
Factor indicating respondent's level of education: No Degree, University Degree
Factor identifying union membership: Non-union Household, Union Household
Language spoken by respondent - English, French, English + French, Other
Respondent's Occupation: Managers/Executives/Business Owners, Professional, Routine Non-Manual, Working Class, Armed Forces, Farmer/Fisherman, Retired, Unemployed, Student, Other (incl. housewife, homemaker)
Sum of weights for demographic profile
Fill in some details
Lucas, Jack, Tyler Romualdi, David A. Armstrong II, and Elizabeth Goodyear-Grant. 2025. "Demographic Divides in Canadian Voting, 1945–2023: The Canadian Vote Intention Dataset." Canadian Journal of Political Science 58 (3): 666–80. doi:10.1017/S0008423925000241.
data(vote_data) head(vote_data) table(vote_data$type)data(vote_data) head(vote_data) table(vote_data$type)
The general engine behind [wtd_vote()]: given a data frame with a 'response' column (any set of categories – vote choice, a policy question's answer categories, whatever 'response_var' points at), 'weight', 'year', and zero or more grouping columns, compute each response category's weighted share of the total for every year/grouping-variable combination. [question_data()] produces data in exactly this shape (its 'response' column decoded to a factor by question type), so this works identically for a policy question or for vote choice/intention – see [response_gap_analysis()] for the significance-test analog.
wtd_response( data, years = NULL, grouping_vars = NULL, avail_grps = cvpa_grouping_vars, response_var = "response" )wtd_response( data, years = NULL, grouping_vars = NULL, avail_grps = cvpa_grouping_vars, response_var = "response" )
data |
A data frame as returned by [question_data()] (or any data frame with the same shape: 'response_var', 'weight', 'year', plus whichever of 'avail_grps' you want to group by). |
years |
Select desired years. 'NULL' (default) uses every year in 'data'. |
grouping_vars |
For which groups do you want separate estimates; must be a subset of 'avail_grps'. |
avail_grps |
Grouping variables considered valid; defaults to [cvpa_grouping_vars]. |
response_var |
Name of the column in 'data' holding the response category. Defaults to '"response"', the column [question_data()] produces. |
A data frame with 'year', 'grouping_vars', the response category (named 'response_var'), 'count' (summed weight), and ‘response_share' (that category’s share of the year/group total).
# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(questions = "bri_econ_013", quiet = TRUE) d <- question_data("bri_econ_013") wtd_response(d, grouping_vars = "gender") unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)# Uses the small example data bundled with cvpa (see ?cvpa_example_url), # in a temporary cache, so this runs offline and leaves your own cache # alone. For real analysis, skip the options() call and use the default, # the official live data. old <- options( cvpa.data_url = cvpa_example_url(), cvpa.cache_dir = tempfile("cvpa-cache-") ) update_cvpa_data(questions = "bri_econ_013", quiet = TRUE) d <- question_data("bri_econ_013") wtd_response(d, grouping_vars = "gender") unlink(getOption("cvpa.cache_dir"), recursive = TRUE) options(old)
Make weighted mean of vote intention/choice
wtd_vote( data, vote_type = c("intention", "choice"), incl_undecided = FALSE, years = NULL, grouping_vars = NULL, ... )wtd_vote( data, vote_type = c("intention", "choice"), incl_undecided = FALSE, years = NULL, grouping_vars = NULL, ... )
data |
Data from inst/extdata/integrated_with_weights.RDS. |
vote_type |
Use vote intention or choice |
incl_undecided |
If using vote intention, should undecided voters be included? |
years |
Select desired years. 'NULL' (default) uses every year in 'data' – so newly added years are never silently dropped. |
grouping_vars |
For which groups do you want separate estimates (region or province, gender, age_cats, degree, religion, language, union_household, community_size) |
... |
Other arguments to be passed down, not implemented |
Since 0.2.0, this is a thin wrapper around the general [wtd_response()] engine that also powers policy-question analysis (see [question_data()]) – its inputs, outputs, and validation behavior are unchanged from earlier versions.
A data frame (a tibble) with one row per combination of year, party and, if 'grouping_vars' is given, each combination of the grouping variables. Its columns are 'year'; 'vote', the party (a factor); one column for each of 'grouping_vars'; 'count', the sum of the survey weights of the respondents in that cell; and 'party_support', that party's weighted share of the votes counted in the same year and group, so the shares sum to 1 within each year and group. Respondents with a missing vote are never counted, and undecided respondents are left out unless 'incl_undecided = TRUE'.
data(vote_data) wtd_vote(vote_data, "intention", years=2000:2022)data(vote_data) wtd_vote(vote_data, "intention", years=2000:2022)