This page downloads 6 studies (about 13 MB) the
first time it runs. options(qesR.cache = "disk") keeps them
on disk for later sessions.
This page goes from qes_harmonize() to a weighted
estimate, study by study, and shows what qesR records on the way: the
comparability grade of each study’s question, the reason for every
missing value, the weight that fits each question, and the provenance of
each cell. It is built on the full data files.
The harmonization is experimental: targets, grades
and mappings may still change from one version of qesR to the next. For
each study and harmonized variable (“target”), the rules that ship with
qesR say which question feeds the target and how each of its codes maps
to the target’s levels; nothing is matched by name. Some studies have no
validated weight, so their weight columns are NA; their
answers are harmonized like the others’ (the weights table gives each study’s
weight).
Which studies have the question
qes_spec() gives one row per target and one column per
study, each cell the grade of that study’s question against the target’s
anchor question (the full grid is on the coverage page). Two targets here: the
referendum question on Quebec becoming an independent country, and the
vote reported after the election.
grid <- qes_spec(targets = c("sov_indep", "vote_prov_recall"), lang = "en")
knitr::kable(grid[, c("target", "label", "qes2022", "qes2018", "qes2014", "qes2012")])| target | label | qes2022 | qes2018 | qes2014 | qes2012 |
|---|---|---|---|---|---|
| vote_prov_recall | Provincial vote (recall) | comparable | comparable | comparable | identical |
| sov_indep | Referendum vote: independent country | comparable | comparable | identical | identical |
The crosswalk view says which question each grade is about, and why:
xw <- qes_spec("crosswalk", targets = "sov_indep", lang = "en")
knitr::kable(xw[, c("study", "wave", "source_var", "grade", "grade_reason")])| study | wave | source_var | grade | grade_reason |
|---|---|---|---|---|
| qes2012 | post | q52 | identical | Anchor row of the target. |
| qes2014 | post | Q19 | identical | Same stem as the anchor in English and French, same options (yes, no, don’t know, prefer not to answer), asked of everyone, web. |
| qes2018 | post | q26 | comparable | The stem adds a temporal adverb (‘today’, ‘aujourd’hui’); otherwise the same wording and options as the anchor. |
| qes2022 | cps | cps_qc_referendum | comparable | The stem adds a temporal adverb; don’t know is offered but there is no prefer-not-to-answer option. |
identical means the same question, options and universe
as the anchor; comparable the same stimulus with
differences that should not move the shares; approximate a
difference that can. The variable
reference gives the wording and levels of every study’s
question.
Harmonize
studies = NULL, the default, takes the Quebec Election
Studies that have a question for at least one of the targets; the Durand
panels, the CROP polls and the 1998 panel are left out unless you name
them. qes2008 has no recommended weight (both of its
weights are calibrated on the vote or on turnout), so its rows have no
weight and qes_design() leaves them out below, with a
message. layout = "long" gives one row per respondent and
wave, so each answer sits on the row of the wave that asked it, next to
that wave’s weight. missing = "reasons" adds a column
<target>__na with the reason of every missing
value.
h <- qes_harmonize(targets = c("sov_indep", "vote_prov_recall"), layout = "long",
missing = "reasons", lang = "en")
#> Using the cached copy of '2022 Quebec Election Study v1.dta'.
#> Using the cached copy of 'Quebec Election Study 2018.dta'.
#> Using the cached copy of 'Quebec Election Study 2014.sav'.
#> Using the cached copy of 'Quebec Election Study 2012 (STATA).dta'.
#> Using the cached copy of 'Quebec Election Study 2012 (SPSS).sav'.
#> Using the cached copy of 'Quebec Election Study 2008 (SPSS).sav'.
#> Using the cached copy of 'Quebec Election Study 2007 (SPSS).sav'.
#> Levels a study's question did not offer are structural zeros, not an absence of support: vote_prov_recall: qes2022 (PVQ, ON, ADQ), qes2018 (PVQ, PCQ, ON, ADQ), qes2014 (PCQ, ADQ), qes2012 (PCQ, ADQ), qes2008 (CAQ, PCQ, ON), qes2007 (CAQ, PCQ, ON); sov_indep: qes2022 (would_not_vote), qes2018 (would_not_vote), qes2014 (would_not_vote), qes2012 (would_not_vote). qes_provenance(x, level = "cell") lists them.
table(h$study, h$wave)
#>
#> cps pes post
#> qes2007 0 0 2175
#> qes2008 0 0 1151
#> qes2012 0 0 1505
#> qes2014 0 0 1517
#> qes2018 0 0 3072
#> qes2022 1521 1220 0The messages are part of the result. The first lists the
structural zeros: levels a study’s question did not offer, such
as a party missing from its list. Their share in that study is 0 because
nobody could choose them, not because nobody supported them.
qes2022 asked the referendum question in its
campaign-period wave (cps) and the reported vote in its
post-election wave (pes), which have different weights.
Why values are missing
Every missing value has a reason. Counts of the valid answers and of each reason, for the referendum question:
reasons <- table(h$study, h$sov_indep__na)
reasons <- reasons[, colSums(reasons) > 0, drop = FALSE]
knitr::kable(cbind(valid = tapply(!is.na(h$sov_indep), h$study, sum), reasons[, , drop = FALSE]))| valid | dk | refused | not_in_wave | not_asked | |
|---|---|---|---|---|---|
| qes2007 | 0 | 0 | 0 | 0 | 2175 |
| qes2008 | 0 | 0 | 0 | 0 | 1151 |
| qes2012 | 1323 | 156 | 26 | 0 | 0 |
| qes2014 | 1353 | 148 | 16 | 0 | 0 |
| qes2018 | 2558 | 463 | 51 | 0 | 0 |
| qes2022 | 1284 | 237 | 0 | 1220 | 0 |
dk is “don’t know” and refused a refusal;
not_in_wave marks the qes2022 post-election
rows, since the question was asked in the other wave.
?qes_harmonize lists every reason.
Grades
min_grade keeps only the cells at or above a grade and
sets the others to NA, with reason
below_grade. With min_grade = "identical",
only the studies that asked the anchor question itself remain:
strict <- qes_harmonize(targets = "sov_indep", layout = "long", missing = "reasons",
min_grade = "identical", quiet = TRUE,
lang = "en")
table(strict$study, strict$sov_indep__na)[, c("dk", "refused", "below_grade")]
#>
#> dk refused below_grade
#> qes2007 0 0 0
#> qes2008 0 0 0
#> qes2012 156 26 0
#> qes2014 148 16 0
#> qes2018 0 0 3072
#> qes2022 0 0 1521qes2018 and qes2022 added “today” to the
question (and qes2022 offered no “prefer not to answer”),
so both are graded comparable and are dropped here. Whether
a comparable cell belongs in your analysis is a judgement
the grade reason helps you make; qesR never makes it for you.
Weights
Each wave has at most one recommended weight, normalized to mean 1 in
each study and wave. attr(, "qes_weight_guide") says which
wave each target came from and which weight fits it:
guide <- attr(h, "qes_weight_guide")
knitr::kable(guide[, c("target", "study", "wave", "weight_column", "weight_var")])| target | study | wave | weight_column | weight_var |
|---|---|---|---|---|
| vote_prov_recall | qes2022 | pes | weight | pes_weight_general |
| sov_indep | qes2022 | cps | weight | cps_weight_general |
| vote_prov_recall | qes2018 | post | weight | pond |
| sov_indep | qes2018 | post | weight | pond |
| vote_prov_recall | qes2014 | post | weight | POND |
| sov_indep | qes2014 | post | weight | POND |
| vote_prov_recall | qes2012 | post | weight | pond |
| sov_indep | qes2012 | post | weight | pond |
| vote_prov_recall | qes2008 | post | weight | NA |
| sov_indep | qes2008 | NA | NA | NA |
| vote_prov_recall | qes2007 | post | weight | pond |
| sov_indep | qes2007 | NA | NA | NA |
In the long layout the weight column holds, on each row,
the weight of that row’s wave, so one design serves both targets.
A weighted estimate
qes_design() hands the data to the survey
package: each study is a stratum and, in the long layout, the respondent
is the sampling unit. Estimates are computed within each study with
svyby(); a dash marks the levels a study did not offer,
instead of a 0.
d <- qes_design(h)
#> 1151 row(s) with no value of 'weight' are left out of the design (not in a wave with that weight, or its weight needs review): qes2008 1151.
cells <- qes_provenance(h, level = "cell")
spec <- qes_spec("spec")
# Weighted percentage of each level of `target` by study, with its
# standard error; a dash marks the levels the study's question did not offer.
pct <- function(x) formatC(100 * x, format = "f", digits = 1)
share_table <- function(design, target) {
est <- survey::svyby(stats::as.formula(paste0("~", target)), ~study, design,
survey::svymean, na.rm = TRUE)
lv <- levels(h[[target]])
tg <- spec$tables$targets
names_lv <- spec$tables$levels$name[spec$tables$levels$levels_id ==
tg$levels_id[tg$target == target]]
out <- sapply(lv, function(l) {
paste0(pct(est[[paste0(target, l)]]), " (", pct(est[[paste0("se.", target, l)]]), ")")
})
out <- matrix(out, nrow = nrow(est), dimnames = list(est$study, lv))
for (s in rownames(out)) {
off <- cells$levels_not_offered[cells$study == s & cells$target == target]
gone <- lv[names_lv %in% strsplit(off, ";", fixed = TRUE)[[1]]]
out[s, gone] <- "\u2014"
}
out
}
knitr::kable(share_table(d, "sov_indep"))| Yes | No | Would not vote / would spoil | |
|---|---|---|---|
| qes2007 | 0.0 (0.0) | 0.0 (0.0) | 0.0 (0.0) |
| qes2012 | 40.4 (1.5) | 59.6 (1.5) | — |
| qes2014 | 34.8 (1.5) | 65.2 (1.5) | — |
| qes2018 | 34.6 (1.0) | 65.4 (1.0) | — |
| qes2022 | 34.3 (1.8) | 65.7 (1.8) | — |
Each cell is a weighted percentage with its standard error in
parentheses. No study offered “would not vote” as an answer to this
question, so that level is a structural zero everywhere. The standard
errors treat each weighted sample as a probability sample. These studies
are web surveys whose weights adjust to census margins, so read the
standard errors as a rough guide only (see
?qes_design).
Reported vote describes the electorate, so keep the respondents who
could vote: eligible_voter is TRUE for those
aged 18 or older on election day and, where asked, Canadian citizens.
qes2018 sampled people aged 16 and over:
table(h$study[h$wave %in% c("post", "pes")],
h$eligible_voter[h$wave %in% c("post", "pes")], useNA = "ifany")
#>
#> FALSE TRUE <NA>
#> qes2007 0 2133 42
#> qes2008 0 1142 9
#> qes2012 0 1484 21
#> qes2014 0 1517 0
#> qes2018 255 2799 18
#> qes2022 0 1220 0
voters <- subset(d, eligible_voter %in% TRUE)
knitr::kable(share_table(voters, "vote_prov_recall"))| PLQ | PQ | CAQ | QS | PVQ | PCQ | ON | ADQ | Other party | |
|---|---|---|---|---|---|---|---|---|---|
| qes2007 | 25.3 (1.3) | 31.1 (1.4) | — | 4.8 (0.7) | 6.5 (0.8) | — | — | 31.6 (1.4) | 0.6 (0.2) |
| qes2012 | 25.0 (1.4) | 38.6 (1.6) | 25.5 (1.4) | 6.5 (0.8) | 1.0 (0.3) | — | 2.3 (0.4) | — | 1.1 (0.3) |
| qes2014 | 35.9 (1.6) | 29.8 (1.6) | 23.1 (1.4) | 8.2 (0.8) | 1.0 (0.3) | — | 0.7 (0.3) | — | 1.3 (0.3) |
| qes2018 | 23.3 (1.0) | 19.6 (0.9) | 35.8 (1.2) | 16.3 (0.9) | — | — | — | — | 5.1 (0.6) |
| qes2022 | 17.1 (2.0) | 15.6 (1.2) | 33.0 (1.9) | 17.2 (1.4) | — | 13.5 (1.2) | — | — | 3.5 (1.0) |
Keeping eligible voters does not recalibrate the weights, which target the population each study sampled. A party marked with a dash was not on that study’s list of answers, so its share there cannot be compared with the other studies.
Pooling studies into one estimate is possible
(qes_design(pool = "equal") gives each study the same
total), but the studies differ in population, mode and wording; compare
them side by side, as above, before pooling.
Provenance
qes_provenance(level = "cell") gives, for each study and
target, the question applied, its grade, the weight, the levels not
offered and the count of valid values and of each missing reason:
knitr::kable(cells[, c("study", "target", "source_var", "grade", "weight_var",
"levels_not_offered", "n_valid", "n_dk", "n_not_in_wave")])| study | target | source_var | grade | weight_var | levels_not_offered | n_valid | n_dk | n_not_in_wave |
|---|---|---|---|---|---|---|---|---|
| qes2022 | vote_prov_recall | pes_votechoice | comparable | pes_weight_general | PVQ;ON;ADQ | 1101 | 2 | 301 |
| qes2022 | sov_indep | cps_qc_referendum | comparable | cps_weight_general | would_not_vote | 1284 | 237 | 0 |
| qes2018 | vote_prov_recall | q6 | comparable | pond | PVQ;PCQ;ON;ADQ | 2016 | 0 | 0 |
| qes2018 | sov_indep | q26 | comparable | pond | would_not_vote | 2558 | 463 | 0 |
| qes2014 | vote_prov_recall | Q3 | comparable | POND | PCQ;ADQ | 1283 | 0 | 0 |
| qes2014 | sov_indep | Q19 | identical | POND | would_not_vote | 1353 | 148 | 0 |
| qes2012 | vote_prov_recall | q25 | identical | pond | PCQ;ADQ | 1274 | 0 | 0 |
| qes2012 | sov_indep | q52 | identical | pond | would_not_vote | 1323 | 156 | 0 |
| qes2008 | vote_prov_recall | q12a | comparable | NA | CAQ;PCQ;ON | 898 | 2 | 0 |
| qes2008 | sov_indep | NA | NA | NA | NA | 0 | 0 | 0 |
| qes2007 | vote_prov_recall | q12 | comparable | pond | CAQ;PCQ;ON | 1727 | 8 | 0 |
| qes2007 | sov_indep | NA | NA | NA | NA | 0 | 0 | 0 |
These counts are per respondent of the study, as in the respondent
layout, so n_valid and the counts of the reasons add up to
the study’s respondents; the table in Why values are missing
counts rows of the long layout instead. Every qes2022
respondent took part in the campaign wave, so none is
not_in_wave for sov_indep, while 301 did not
take part in the post-election wave and are not_in_wave for
vote_prov_recall.
qesR records which files and which version of the rules produced
every cell: level = "study" names the data file of each
study, and level = "spec" the version of the rules. The
same files and the same qesR version give the same values.
qes_cite() cites qesR with the rules, then each
dataset:
spec_record <- qes_provenance(h, level = "spec")
spec_record[, c("spec_version", "qesR_version")]
#> spec_version qesR_version
#> 1 4.7.0 0.9.2
cat(qes_cite(h, lang = "en"), sep = "\n\n")
#> Gareau-Paquette, Thomas, 2026, "qesR: Access Quebec Election Study Datasets", R package version 0.9.2, https://github.com/ThomasGareau/qesR; harmonization spec 4.7.0 (content hash 30697daa0042aab3078507b665867748)
#>
#> Mahéo, Valérie-Anne; Bélanger, Éric; Stephenson, Laura B; Harell, Allison, 2023, "2022 Quebec Election Study", https://doi.org/10.7910/DVN/PAQBDR, Harvard Dataverse, V1.1, UNF:6:I/DFDdqJv7wNEoyyRdxaIw== [licence: CC BY-NC 4.0, https://creativecommons.org/licenses/by-nc/4.0/]
#>
#> Bélanger, Éric; Nadeau, Richard; Mahéo, Valérie-Anne; Daoust, Jean-François, 2023, "Étude électorale québécoise 2018", https://doi.org/10.5683/SP3/NWTGWS, Borealis, V1, UNF:6:luhys2QSLNTONPOXO4LYpg==
#>
#> Bélanger, Éric; Nadeau, Richard, 2023, "Étude électorale québécoise 2014", https://doi.org/10.5683/SP3/64F7WR, Borealis, V1, UNF:6:OoiAJ3ShbycsxmWCefqrjw==
#>
#> Bélanger, Éric; Nadeau, Richard; Henderson, Ailsa; Hepburn, Eve, 2023, "Étude électorale québécoise 2012", https://doi.org/10.5683/SP2/WXUPXT, Borealis, V1, UNF:6:nG192rAWV0IlYSpRg4WBaQ==
#>
#> Bélanger, Éric; Nadeau, Richard, 2023, "Étude électorale québécoise 2008", https://doi.org/10.5683/SP2/8KEYU3, Borealis, V1, UNF:6:6wfopjsb0foTuDDWQPDfXg==
#>
#> Bélanger, Éric; Nadeau, Richard; Crête, Jean; Stephenson, Laura; Tanguay, Brian, 2023, "Étude électorale québécoise 2007", https://doi.org/10.5683/SP2/6XGOKA, Borealis, V1, UNF:6:fNjQ+LF7dCVuIrjEyQuOyg==Five sovereignty targets, one pooled variable
A target is one question stimulus. Other studies asked about
sovereignty with other wordings, so they feed other targets, never
merged into sov_indep:
sov <- qes_spec("crosswalk", targets = "sovereignty", lang = "en")
sov <- sov[sov$rule != "none", ]
rownames(sov) <- NULL
knitr::kable(sov[, c("target", "study", "source_var", "grade", "weight_var")])| target | study | source_var | grade | weight_var |
|---|---|---|---|---|
| sov_indep | qes2012 | q52 | identical | pond |
| sov_indep | qes2014 | Q19 | identical | POND |
| sov_indep | qes2018 | q26 | comparable | pond |
| sov_indep | qes2022 | cps_qc_referendum | comparable | cps_weight_general |
| sov_favour | qes2018_panel | rts_q7 | identical | weight_rts |
| sov_sovereign_country | qes2012_panel | intvoteref | identical | pondam1 |
| sov_partnership_1995 | qes2007 | q19 | identical | pond |
| sov_partnership_1995 | qes2008 | q19 | comparable | NA |
| sov_partnership_1995 | qes1998 | q16a_crop | comparable | ponder3 |
| sov_partnership_1995 | qes2007_panel | intref1 | comparable | pondam1 |
| sov_partnership_1995_push | qes2007 | q19 | identical | pond |
| sov_partnership_1995_push | qes2008 | q19 | comparable | NA |
| sov_partnership_1995_push | qes2007_panel | intref1 | comparable | pondam1 |
| sov_partnership_1995_push | qes1998 | q16a_crop | comparable | ponder3 |
sov_sovereign_country asks about a “sovereign country”
(qes2012_panel), sov_favour is a four-point
scale of favour (qes2018_panel), and
sov_partnership_1995 repeats the question of the 1995
referendum, sovereignty with an offer of partnership to the rest of
Canada (qes2007, qes2008,
qes2007_panel and the CROP respondents of
qes1998). Only qes2007 and the 2018 panel have
a usable weight for these questions: both weights of
qes2008 are calibrated on the vote or on turnout, and the
others’ weights are not validated (see the weights table). The other studies’ rows
are left out of weighted estimates, with a message. Put the targets side
by side and read each one against its own question.
To follow support across every study anyway, the pooled variable
sov_support takes each study’s referendum question,
whatever its wording, and says which one each value comes from
(sov_support__type), with the grade of that question
(sov_support__grade); the favourable or opposed scale of
qes2018_panel is collapsed to yes or no and graded
approximate. vote_choice does the same for the vote (the
reported vote, else the vote intention), and pol_interest
for interest in politics:
pooled <- qes_spec("pooled", targets = "sov_support", lang = "en")
knitr::kable(pooled[, c("type_name", "member", "precedence", "default", "grade_cap")])| type_name | member | precedence | default | grade_cap |
|---|---|---|---|---|
| independence | sov_indep | 1 | TRUE | NA |
| sovereign_country | sov_sovereign_country | 2 | TRUE | NA |
| partnership_1995_push | sov_partnership_1995_push | 3 | TRUE | NA |
| partnership_1995 | sov_partnership_1995 | 4 | TRUE | NA |
| favour | sov_favour | 5 | TRUE | approximate |