// ----------------------------------------------------------------------------- // 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