Package {cvpa}


Type: Package
Title: Canadian Voting and Policy Attitudes
Version: 0.3.0
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>.
License: MIT + file LICENSE
URL: https://policyattitudes.ca/app/, https://github.com/lucasjacklucas/cvpa_dataset
BugReports: https://github.com/lucasjacklucas/cvpa_dataset/issues
Depends: R (≥ 3.5.0)
Imports: broom, dplyr, nanoparquet, rio, rlang, stats, tidyr
Suggests: ggplot2, knitr, rmarkdown, testthat (≥ 3.1.5), withr
VignetteBuilder: knitr
Encoding: UTF-8
Config/roxygen2/version: 8.0.0
Config/testthat/edition: 3
NeedsCompilation: no
Packaged: 2026-10-28 12:09:07 UTC; david
Author: Dave Armstrong ORCID iD [aut, cre], Jack Lucas ORCID iD [aut], Tyler Romualdi [aut]
Maintainer: Dave Armstrong <davearmstrong.ps@gmail.com>
Repository: CRAN
Date/Publication: 2026-11-13 16:40:22 UTC

Base URL for the hosted cp3 policy-attitude Parquet files

Description

Override with 'options(cvpa.data_url = ...)' to point the package at a different mirror (e.g. a local copy of the cvpa_dataset repo's 'site/static/cp3_parquet/' during development, or the small bundled example data from [cvpa_example_url()]). Anything other than the official URL, 'https://policyattitudes.ca/cp3_parquet/', makes [update_cvpa_data()] – and the functions that read the data it cached – warn. Reset with 'options(cvpa.data_url = NULL)'.

Usage

.cvpa_data_base_url()

Value

A single character string: the base URL of the cp3 Parquet files (the 'cvpa.data_url' option if set, otherwise the official URL).


Local cache directory used by cvpa

Description

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 = ...)'.

Usage

cvpa_cache_dir()

Value

The cache directory path (created if it doesn't exist yet).

Examples

# 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)

URL of the small example dataset bundled with cvpa

Description

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.

Usage

cvpa_example_url()

Details

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.

Value

A 'file://' URL (with a trailing slash) for the bundled example data, suitable for 'options(cvpa.data_url = ...)'.

Examples

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)

Grouping variables available for any question

Description

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()].

Usage

cvpa_grouping_vars

Value

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].

See Also

[demographic_vars], [question_data()]

Examples

cvpa_grouping_vars

Demographic variables available on every question's response data

Description

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.

Usage

demographic_vars

Format

A data frame with 11 rows and 3 variables:

variable

Column name as it appears in [question_data()]'s output

description

What the variable measures

categories

Ordered category labels, separated by '"; "'

Value

A data frame with one row for each of the eleven demographic variables and the columns 'variable', 'description' and 'categories' (described under Format).

See Also

[cvpa_grouping_vars], [question_data()]

Examples

demographic_vars

Analyze Significance of Voting Gaps

Description

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.

Usage

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,
  ...
)

Arguments

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

Details

See [response_gap_analysis()] for the same test generalized to any policy question, not just vote choice/intention.

Value

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

Examples

data(vote_data)
gap_analysis(vote_data, "intention", years=2000:2022, grouping_var="gender")


Make Reliability Flag for Weighted Proportions Test

Description

Makes a flag to identify if the p-value for the difference of proportions test is reliable (all expected counts greater than 5).

Usage

make_flag(x, n)

Arguments

x

A vector of two values of category counts.

n

A vector of two values of sample sizes

Value

'"p-val reliable"' if every expected count is at least 5, otherwise '"p-val unreliable"'.

Examples

make_flag(c(30, 40), c(100, 120))   # large counts: reliable
make_flag(c(2, 3), c(10, 20))       # small counts: unreliable

List available policy and vote questions

Description

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()]).

Usage

question_catalog(domain = NULL, type = NULL)

Arguments

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).

Value

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).

Examples

# 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)

Fetch and decode one question's respondent-level data

Description

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.

Usage

question_data(question, years = NULL, decode = TRUE)

Arguments

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.

Value

A data frame with one row per respondent-question-wave: 'response', 'weight', 'year', 'response_id', 'survey_id', and the columns in [cvpa_grouping_vars].

Examples

# 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)

Response category labels for one question

Description

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.

Usage

question_response_labels(question)

Arguments

question

A question code, e.g. '"bri_econ_013"' or '"vote_choice"'.

Value

A data frame with 'code', 'label', and 'sort_order', ordered by 'sort_order'.

Examples

# 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)

Analyze Significance of Response Gaps for any question

Description

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.

Usage

response_gap_analysis(
  data,
  grouping_var,
  years = NULL,
  levels = NULL,
  avail_grps = cvpa_grouping_vars,
  response_var = "response"
)

Arguments

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"'.

Value

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'.

Examples

# 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)

Download or refresh cvpa's local cache of the cp3 policy-attitude data

Description

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()].

Usage

update_cvpa_data(questions = NULL, force = FALSE, quiet = FALSE)

Arguments

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.

Details

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.

Value

Invisibly, 'TRUE', with an attribute '"skipped"' holding the question codes (if any) that could not be downloaded.

Examples

# 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)

Vote Choice/Intention Data

Description

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.

Format

A data frame with 78,614 rows and 11 variables

vote

Factor indicating vote choice or intention depending on type

type

String indicating vote whever vote is intention or actual choice

year

Year of survey

region

Factor indicating region of residence: Atlantic, Quebec, Ontario, Prairies, BC, The North

province

Factor indicating province of residence: Newfoundland and Labrador, PEI, Nova Scotia, New Brunswick, Quebec, Ontario, Manitoba, Saskatchewan, Alberta, BC, Yukon, Northwest Territories, Nunavut

community_size

Factor identifying community size of respondent's residence: < 100,000, >= 100,000

gender

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.

age_cats

Factor identifying respondent's age category: 18-29, 30-49, 50+

religion

Factor giving respondent's religious affiliation: Catholic, Protestant, Jewish, Other, None

degree

Factor indicating respondent's level of education: No Degree, University Degree

union_household

Factor identifying union membership: Non-union Household, Union Household

language

Language spoken by respondent - English, French, English + French, Other

occupation

Respondent's Occupation: Managers/Executives/Business Owners, Professional, Routine Non-Manual, Working Class, Armed Forces, Farmer/Fisherman, Retired, Unemployed, Student, Other (incl. housewife, homemaker)

weight

Sum of weights for demographic profile

Details

Fill in some details

References

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.

Examples

data(vote_data)
head(vote_data)
table(vote_data$type)


Weighted proportion of each response category, by year and group

Description

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.

Usage

wtd_response(
  data,
  years = NULL,
  grouping_vars = NULL,
  avail_grps = cvpa_grouping_vars,
  response_var = "response"
)

Arguments

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.

Value

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).

Examples

# 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

Description

Make weighted mean of vote intention/choice

Usage

wtd_vote(
  data,
  vote_type = c("intention", "choice"),
  incl_undecided = FALSE,
  years = NULL,
  grouping_vars = NULL,
  ...
)

Arguments

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

Details

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.

Value

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'.

Examples

data(vote_data)
wtd_vote(vote_data, "intention", years=2000:2022)