Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save briatte/c68787f59b698933b2dc5cd3b75c68a3 to your computer and use it in GitHub Desktop.
Save briatte/c68787f59b698933b2dc5cd3b75c68a3 to your computer and use it in GitHub Desktop.
Stata code for François Briatte, Camille Kelbel, ‘‘C‘est quand qu‘on va où ?’ Indécision et moment du choix de vote en 2022,” in Vincent Tiberj et al. (eds), Citoyens et partis après 2022. Éloignement, fragmentation, Paris, Presses Universitaires de France, 2024, pp.285-302.
// -----------------------------------------------------------------------------
// DEPENDENCIES
// -----------------------------------------------------------------------------
// minimum version for `import spss`
version 16
// version used for estimation
version 17
// ssc inst coefplot
which coefplot
// ssc inst estout
which estout
// ssc inst fre
which fre
// net describe st0269, from(http://www.stata-journal.com/software/sj12-3)
// net install st0269
// doi: 10.1177/1536867X1201200307
which mlogitgof
// https://github.com/mdroste/stata-scheme-modern
set scheme modern
// -----------------------------------------------------------------------------
// FILE PATHS
// -----------------------------------------------------------------------------
loc date "2023-03-02"
cap mkdir "`date'"
loc results "`date'/`date'-"
cap log using "`results'results.log"
// load dataset
import spss using "PEOPLE-Dynata-2022-09-05.sav", clear
// -----------------------------------------------------------------------------
// SURVEY WEIGHTS
// -----------------------------------------------------------------------------
count // 1978
su poids*
svyset [pw = poids] // sexe + âge + CSP + diplôme + vote T1 2022
// -----------------------------------------------------------------------------
// GLOBAL PARAMETERS
// -----------------------------------------------------------------------------
// age, sex, city size, educ., SES, fin. budget, int. in politics x campaign
gl ctrls "ib0.female ib1.age6 ib1.AGGLO ib2.edu3 ib5.occ7 ib2.budget int_campaign*"
// [NOTE]: `int_campaign*` equates to `ib2.interest4#c.campaign`
// estout: model screen view (coefficients only)
gl screen "b(2) not nocons nobase noomitted wide compress lab"
// estout: model export options
gl export_rtf "b(2) se(2) nobase noomitted nogap lab replace"
// estout: two-way table export options (expects percentages)
gl export_tbl "unstack cell(b(fmt(1))) nonum varlabels(`e(labels)') replace"
// -----------------------------------------------------------------------------
// RECODES
// -----------------------------------------------------------------------------
// sex
cap drop female
gen female:female = (Q1 == 2) if !mi(Q1)
la de female 0 "Homme" 1 "Femme", replace
// age
cap drop age6
gen age6:age6 = irecode(Q2, 0, 24, 34, 44, 54, 64, .)
la de age6 1 "18-24" 2 "25-34" 3 "35-44" 4 "45-54" 5 "55-64" 6 "65+", replace
table age6, stat(min Q2) stat(max Q2)
// education (regrouping levels 1 and 2 due to low-N at level 1)
cap drop edu6
gen edu6 = Q37 - 1
replace edu6 = 1 if edu6 == 0
tab Q37 edu6
cap drop edu3
//recode Q37 (1 2 3=1 "Bac-") (4=2 "Bac") (5 6 7=3 "Bac+"), gen(edu3)
recode Q37 (1 2 3=1 "Bac-") (4=2 "Bac") (5 6 7=3 "Bac+"), gen(edu3)
// occ. status (3 groups)
fre Q39
cap drop occ3
recode Q39 (1=1 "Emploi") (3=2 "Retraité") (else=3 "Sans emploi"), gen(occ3)
la var occ3 "Occupational status"
// occ. status (7 groups)
encode Situation_emploi, gen(occ7)
// city size
fre AGGLO
la de AGGLO 1 "Agglo < 2k" 2 "Agglo < 20k" 3 "Agglo < 100k" ///
4 "Agglo 100k+" 5 "Agglo Paris", replace
la val AGGLO AGGLO
// subjective income (financial hardship)
fre Q38
gen budget:budget = Q38 if Q38 < 4 // item 4 (DNK) is very low-N
la de budget 1 "Budget élevé" 2 "Budget=" 3 "Budget faible"
// candidate vote T1 2022
fre Q8
replace Q8 = . if Q8 == 96
la de Q8 1 "Arthaud" 2 "Roussel" 3 "Macron" 4 "Lassalle" 5 "Le Pen" ///
6 "Zemmour" 7 "Mélenchon" 8 "Hidalgo" 9 "Jadot" 10 "Pécresse" ///
11 "Poutou" 12 "Dupont-Aignant" 13 "Blanc/Nul", replace
la val Q8 Q8
// 'top 4' voter subsample + blanks/nulls
// (Pécresse treated as top 4 due to Fillon score in 2017; actual 4th = Zemmour)
cap drop Q8_top4
recode Q8 (1 2 4 6 8 9 11 12 = 99 "Autres"), gen(Q8_top4)
la var Q8_top4 "Vote T1 2022 (top 4)"
la de Q8 99 "Autres", modify
la val Q8_top4 Q8
// 'top 5' incl. Zemmour (actual 4th)
cap drop Q8_top5
clonevar Q8_top5 = Q8_top4
la var Q8_top5 "Vote T1 2022 (top 5)"
replace Q8_top5 = 6 if Q8 == 6 // Zemmour
svy: tab Q8_top5
// Macron 27% (actual: 28)
// Le Pen 23% (actual: 23)
// Mélenchon 21% (actual: 22)
// Zemmour 7% (actual: 7)
// Pécresse 4.5% (actual: 4.7)
// Blank/Null 2% (actual: 2)
// moment of voting choice (T1)
//
// 1 = on the day
// 2 = < 1 week
// 3 = 1 week to 1 month
// 4 = 1-2 months
// 5 = 2+ months
fre Q9
replace Q9 = . if Q9 == 96
// moment of voting choice is missing when candidate vote is missing
tab Q8 Q9, m
// interest in politics
fre Q32
cap drop interest4
gen interest4:interest4 = Q32
la de interest4 1 "Intérêt--" 2 "Intérêt-" 3 "Intérêt+" 4 "Intérêt++", replace
// followed campaign
d Q17_*
su Q17_* // all coded 1 (yes, often), 2 (yes, sometimes), 3 (no, never)
cap drop campaign
alpha Q17_*, gen(campaign)
replace campaign = 3-campaign // range 0-2
la var campaign "Campagne"
svy: tab campaign
// 'compound' interest: interest x campaign involvement
// NOTE: required for `mlogtest` functions to work properly
cap drop int_campaign*
tab interest4, gen(int_campaign)
foreach i of varlist int_campaign* {
replace `i' = `i' * campaign
la var `i' "`i'"
}
d int_campaign*
la var int_campaign1 "Intérêt × Campagne--"
la var int_campaign2 "Intérêt × Campagne-"
la var int_campaign3 "Intérêt × Campagne+"
la var int_campaign4 "Intérêt × Campagne++"
// -----------------------------------------------------------------------------
// VOTE ~ MOMENT OF CHOICE (original coding)
// -----------------------------------------------------------------------------
// moment of choice
eststo clear
qui estpost svy: tab Q9, per
esttab using "`results'xtab0_Q9-weighted.rtf", $export_tbl
// late deciders are significantly less interested in politics
svy: tab Q9 interest4, per col
// crosstab with full sample, for reference (low-N outside of top 5 candidates)
eststo clear
estpost tab Q8 Q9, chi2
esttab using "`results'xtab0_Q9_Q8-unweighted.rtf", ///
cell(b(fmt(0)) rowpct(fmt(1))) eqlabels(`e(eqlabels)') ///
unstack nonum varlabels() replace
// as % of candidate choice
eststo clear
qui estpost svy: tab Q8_top5 Q9, row per
esttab using "`results'xtab1_Q9_Q8_top5.rtf", $export_tbl
// as % of moment of choice
eststo clear
qui estpost svy: tab Q9 Q8_top5, row per
esttab using "`results'xtab2_Q8_top5_Q9.rtf", $export_tbl
// -----------------------------------------------------------------------------
// VOTE (top 5 candidates) ~ CONTROLS + MOMENT OF CHOICE (3 different codings)
// -----------------------------------------------------------------------------
// baseline = Macron vote
// NOTE: use `if inlist(Q8, 3, 5, 6, 7, 10)` instead of Q8_top5 to avoid
// modelling 'Autres' and blanks/nulls
eststo clear
cap drop moment3
recode Q9 (1=1 "j0") (2 3=2 "1-30j") (4 5=3 "30j+"), gen(moment3)
eststo: qui svy: mlogit Q8_top5 $ctrls ib2.moment3
cap drop moment3
recode Q9 (1=1 "j0") (2 3 4=2 "1-60j") (5=3 "60j+"), gen(moment3)
eststo: qui svy: mlogit Q8_top5 $ctrls ib2.moment3
cap drop moment3
recode Q9 (1 2=1 "0-7j") (3 4=2 "7-60j") (5=3 "60j+"), gen(moment3)
eststo: qui svy: mlogit Q8_top5 $ctrls ib2.moment3
la de moment3 1 "Decision=Late" 2 "Decision=Mid" 3 "Decision=Early", replace
la val moment3 moment3
esttab, $screen
esttab using "`results'moment_as_predictor.rtf", $export_rtf
// Autres: Early (--), Late (+)
// Blanc/Nul: Early (-) , Late (++)
// Le Pen: Late (+)
// Mélenchon: Early (--), Late (--)
// Pécresse: Early (--), Late (-)
// Zemmour: Late (--)
// last coding of `moment3`' used further below
//
// - late_decision = after campaign
// - midgroup = campaign
// - early_decision = before campaign
la var moment3 "Moment of voting choice"
la de moment3 1 "Late" 2 "Campaign" 3 "Early", replace
la val moment3 moment3
svy: tab moment3 // 32% late, 27% campaign, 40% early
// Table 3. as % of candidate choice (top 5)
eststo clear
qui estpost svy: tab Q8_top5 moment3, row per
esttab using "`results'xtab3_Q8_top5_moment3.rtf", $export_tbl
// Table 4. as % of moment of choice (top 5)
eststo clear
qui estpost svy: tab moment3 Q8_top5, row per
esttab using "`results'xtab4_moment3_Q8_top5.rtf", $export_tbl
// -----------------------------------------------------------------------------
// MOMENT OF CHOICE ~ AGE (controlling for education and occup. status)
// -----------------------------------------------------------------------------
// controlling for diploma and/or occ. status (Cautrès and Jadot 2007: 303)
qui svy: mlogit moment3 i.female i.age6 i.edu3 i.occ7
qui margins age6#female
marginsplot, by(female) ///
plotdim(, lab("Tardif" "Campagne" "Précoce")) ///
byopts(title("")) subtitle(, fc(gs14)) ///
xti ("Groupe d'âge (contrôles : âge, sexe, éducation, statut professionnel)") ///
yti("Pr (moment du vote)") ///
legend(rows(1) size(small) ti("Moment du choix de vote", col(gs0) size(small))) ///
name(age_sex, replace)
gr export "`results'mfx-age.png", replace
// -----------------------------------------------------------------------------
// MOMENT OF CHOICE ~ LEFT-RIGHT POSITIONING (incl. 'neither' and DNK)
// -----------------------------------------------------------------------------
fre Q34 // very low-N on extr-left
svy: tab Q34, per
svy: tab Q34 Q8_top5, row per
svy: tab Q8_top5 Q34, row per // MLP and EZ at right + extr-right
// L-R as 5-pt scale
//
// [NOTE] far-left and left lumped together due to low-N far-left
// right and far-right lumped together due to Le Pen voters (see above)
cap drop lr5
recode Q34 (1 2=1 "Gauche+") (3=2 "Gauche") (4=3 "Centre") ///
(5=4 "Droite") (6 7= 5 "Droite+") (8 = 6 "Ni-Ni") (9 = 7 "NSP"), gen(lr5)
la var lr5 "L-R scale"
tab lr5
svy: tab lr5 moment3, row
svy: tab moment3 lr5, row // look at Ni-Ni and NSP
// mfx for 5-pt L-R scale without Ni-Ni and DNK
gen lr5_strict:lr5_strict = lr5 if lr5 < 6
la de lr5_strict 1 "G+" 2 "G" 3 "C" 4 "D" 5 "D+", replace
qui svy: mlogit moment3 i.age6 i.female i.edu3 ib3.lr5_strict
qui margins lr5_strict#age6
marginsplot, by(age6) ///
plotdim(, lab("Tardif" "Campagne" "Précoce")) ///
byopts(title("")) subtitle(, fc(gs14)) ///
xti("Positionnement gauche-droite (contrôles : âge, sexe, diplôme)") ///
yti("Pr (moment du vote)") ///
legend(rows(1) size(small) ti("Moment du choix de vote", col(gs0) size(small))) ///
name(lr5_strict, replace)
gr export "`results'mfx-lr5.png", replace
// -----------------------------------------------------------------------------
// MODELS
// -----------------------------------------------------------------------------
eststo clear
// weighted v. unweighted
eststo m1: qui svy: mlogit moment3 $ctrls ib3.lr5
eststo u1: qui: mlogit moment3 $ctrls ib3.lr5
mlogitgof // generalized Hosmer–Lemeshow GOF test
assert r(P) > 0.1 // should be well rejected
eststo m2: qui svy: mlogit moment3 $ctrls ib3.lr5 i.Q8_top4
eststo u2: qui: mlogit moment3 $ctrls ib3.lr5 i.Q8_top4
mlogitgof // generalized Hosmer–Lemeshow GOF test
assert r(P) > 0.1 // should be well rejected
esttab, $screen bic ///
mti("weighted" "unweighted" "weighted" "unweighted")
esttab using "`results'models.rtf", $export_rtf ///
bic mti("weighted" "unweighted" "weighted" "unweighted")
// -----------------------------------------------------------------------------
// PLOT RESULTS of Models 1-2
// -----------------------------------------------------------------------------
coefplot ///
(m1, if(@ll<0 & @ul>0) keep(Late:)) ///
(m1, if(@ll>0 | @ul<0) keep(Late:)) ///
, bylabel("Tardif 1") || /// v. Précoce
(m2, if(@ll<0 & @ul>0) keep(Late:)) ///
(m2, if(@ll>0 | @ul<0) keep(Late:)) ///
, bylabel("Tardif 2") || /// v. Précoce
(m1, if(@ll<0 & @ul>0) keep(Campaign:)) ///
(m1, if(@ll>0 | @ul<0) keep(Campaign:)) ///
, bylabel("Campagne 1") || /// v. Précoce
(m2, if(@ll<0 & @ul>0) keep(Campaign:)) ///
(m2, if(@ll>0 | @ul<0) keep(Campaign:)) ///
, bylabel("Campagne 2") || /// v. Précoce
, nooffset drop(_cons) xline(0) grid(none) ///
subtitle(, size(small) /// margin(medium) justification(left)
color(gs0) bcolor(gs14)) /// bmargin(top_bottom)
coefl( int_campaign1 = "Très faible" ///
int_campaign2 = "Faible" ///
int_campaign3 = "Élevé" ///
int_campaign4 = "Très élevé" ///
2.AGGLO = "< 20 000" ///
3.AGGLO = "< 100 000" ///
4.AGGLO = "100 000+" ///
5.AGGLO = "Paris" ///
1.edu3 = "Inférieur au bac." ///
3.edu3 = "Supérieur au bac." ///
) ///
groups( ?.female = "{bf:Sexe}" ///
?.age6 = "{bf:Âge}" ///
?.AGGLO = "{bf:Agglo.}" ///
?.edu3 = "{bf:Diplôme}" ///
?.occ7 = "{bf:Statut}" ///
?.budget = "{bf:Revenu}" ///
int_campaign? = "{bf:Intérêt}" ///
?.lr5 = `""{bf:Orientation}" "{bf:politique}""' ///
*.Q8_top4 = "{bf:Vote 2022}" ///
) ///
byopts(rows(1) xrescale legend(off)) ///
p1(mcolor(gs8) ciopts(lcolor(gs8))) ///
p2(mcolor(gs0) ciopts(lcolor(gs0))) ///
ylab(,labs(*.8)) ///
name(coefs, replace)
// xlab(, grid) grid(between glpattern(solid) glwidth(*6) glcolor(gray)) ///
// ysize(5.5) xsize(6.5)
gr export "`results'models.png", replace
gr export "`results'models.pdf", replace
// English version
// , bylabel("Late v. Early") || ///
// , bylabel("Campaign v. Early") || ///
// groups( ?.female = "{bf:Gender}" ///
// ?.age6 = `""{bf:Age}" "{bf:group}""' ///
// ?.AGGLO = `""{bf:City}" "{bf:size}""' ///
// ?.edu3 = "{bf:Education}" ///
// ?.occ7 = `""{bf:Occupational}" "{bf:status}""' ///
// ?.budget = `""{bf:Subjective}" "{bf:income}""' ///
// int_campaign? = `""{bf:Interest ×}" "{bf:Campaign}""' ///
// ?.lr5 = `""{bf:Left-right}" "{bf:placement}""' ///
// *.Q8_top4 = `""{bf:Vote}" "{bf:2022}""' ///
// ) ///
// predicted probabilities, Model 1
qui svy: mlogit moment3 $ctrls ib3.lr5_strict
qui margins age6#female
marginsplot, by(female) ///
plotdim(, lab("Tardif" "Campagne" "Précoce")) ///
byopts(title("")) subtitle(, fc(gs14)) ///
xti ("Groupe d'âge (contrôles : M1)") ///
yti("Pr (moment du vote)") ///
legend(rows(1) size(small) ti("Moment du choix de vote", col(gs0) size(small))) ///
name(models_mfx11, replace)
gr export "`results'mfx-age-model1.png", replace
qui margins lr5_strict#age6
marginsplot, by(age6) ///
plotdim(, lab("Tardif" "Campagne" "Précoce")) ///
byopts(title("")) subtitle(, fc(gs14)) ///
xti("Positionnement gauche-droite (contrôles : M1)") ///
yti("Pr (moment du vote)") ///
legend(rows(1) size(small) ti("Moment du choix de vote", col(gs0) size(small))) ///
name(models_mfx12, replace)
gr export "`results'mfx-lr5-model1.png", replace
// predicted probabilities, Model 2
qui svy: mlogit moment3 $ctrls ib3.lr5_strict i.Q8_top4
qui margins age6#female
marginsplot, by(female) ///
plotdim(, lab("Tardif" "Campagne" "Précoce")) ///
byopts(title("")) subtitle(, fc(gs14)) ///
xti ("Groupe d'âge (contrôles : M2)") ///
yti("Pr (moment du vote)") ///
legend(rows(1) size(small) ti("Moment du choix de vote", col(gs0) size(small))) ///
name(models_mfx21, replace)
gr export "`results'mfx-age-model2.png", replace
qui margins lr5_strict#age6
marginsplot, by(age6) ///
plotdim(, lab("Tardif" "Campagne" "Précoce")) ///
byopts(title("")) subtitle(, fc(gs14)) ///
xti("Positionnement gauche-droite (contrôles : M2)") ///
yti("Pr (moment du vote)") ///
legend(rows(1) size(small) ti("Moment du choix de vote", col(gs0) size(small))) ///
name(models_mfx22, replace)
gr export "`results'mfx-lr5-model2.png", replace
// -----------------------------------------------------------------------------
// DIAGNOSTICS (very experimental...)
// -----------------------------------------------------------------------------
// Pfeffermann 2011-inspired method, using LPMs on 0/1 recodes of the DV
// q-weights for Model 1
qui reg poids $ctrls ib3.lr5
cap drop w_hat
predict w_hat, xb
cap drop q_poids1
gen q_poids1 = poids / w_hat
// q-weights for Model 2
qui reg poids $ctrls ib3.lr5 i.Q8_top4
cap drop w_hat
predict w_hat, xb
cap drop q_poids2
gen q_poids2 = poids / w_hat
// Late v. Early
cap gen late_v_early = .
replace late_v_early = 1 if moment3 == 1
replace late_v_early = 0 if moment3 == 3
eststo clear
// p-weighted LPM Model 1
eststo: qui: reg late_v_early $ctrls ib3.lr5 [pw = poids]
// q-weighted LPM Model 1
eststo: qui: reg late_v_early $ctrls ib3.lr5 [pw = q_poids1]
// p-weighted LPM Model 2
eststo: qui: reg late_v_early $ctrls ib3.lr5 i.Q8_top4 [pw = poids]
// q-weighted LPM Model 2
eststo: qui: reg late_v_early $ctrls ib3.lr5 i.Q8_top4 [pw = q_poids2]
// overview
esttab, $screen r2 bic mti("LPM 1" "q-w LPM 1" "LPM 2" "q-w LPM 2")
// Campaign v. Early
cap gen campaign_v_early = .
replace campaign_v_early = 1 if moment3 == 2
replace campaign_v_early = 0 if moment3 == 3
eststo clear
// p-weighted LPM Model 1
eststo: qui: reg campaign_v_early $ctrls ib3.lr5 [pw = poids]
// q-weighted LPM Model 1
eststo: qui: reg campaign_v_early $ctrls ib3.lr5 [pw = q_poids1]
// p-weighted LPM Model 2
eststo: qui: reg campaign_v_early $ctrls ib3.lr5 i.Q8_top4 [pw = poids]
// q-weighted LPM Model 2
eststo: qui: reg campaign_v_early $ctrls ib3.lr5 i.Q8_top4 [pw = q_poids2]
// overview
esttab, $screen r2 bic mti("LPM 1" "q-w LPM 1" "LPM 2" "q-w LPM 2")
// using some version of this might be more correct
// https://www.stata.com/statalist/archive/2009-01/msg00689.html
// -----------------------------------------------------------------------------
// ROBUSTNESS CHECKS
// -----------------------------------------------------------------------------
eststo clear
// re-estimate Model 1
eststo: qui svy: mlogit moment3 $ctrls ib3.lr5
// re-estimate Model 2
cap drop Q8_top
clonevar Q8_top = Q8_top4
eststo: qui svy: mlogit moment3 $ctrls ib3.lr5 i.Q8_top
// remove Blank/Null
cap drop Q8_top
clonevar Q8_top = Q8_top4
replace Q8_top = 99 if Q8 == 13
eststo: qui svy: mlogit moment3 $ctrls ib3.lr5 i.Q8_top
// add Zemmour
cap drop Q8_top
clonevar Q8_top = Q8_top4
replace Q8_top = Q8 if Q8 == 6
eststo: qui svy: mlogit moment3 $ctrls ib3.lr5 i.Q8_top
// add Jadot
cap drop Q8_top
clonevar Q8_top = Q8_top4
replace Q8_top = Q8 if Q8 == 9
eststo: qui svy: mlogit moment3 $ctrls ib3.lr5 i.Q8_top
// add Zemmour and Jadot
cap drop Q8_top
clonevar Q8_top = Q8_top4
replace Q8_top = Q8 if Q8 == 6 | Q8 == 9
eststo: qui svy: mlogit moment3 $ctrls ib3.lr5 i.Q8_top
esttab, $screen
esttab using "`results'robustness-checks.rtf", $export_rtf
cap log close
// kthxbye
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment