OSCR

Threat and blame frames in political rhetoric about societal issues lead to neural and political polarization.

Code ↔ Paper

4 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 4 matches
  1. [1] § Materials and methods › Study 1 › Statistics ↔ Analysis_Script.ipynb, lines 710 to 823 · score 0.73 · regression coefficients, regression models, standard error, GL, covariates, agreement · Report a mismatch
  2. [2] § Materials and methods › Study 1 › Paradigm ↔ analyses/fMRI_behaveffects.R, lines 1 to 43 · score 0.65 · issue positions, climate change, demographics, education, healthcare, covariates · Report a mismatch
  3. [3] § Results › Study 1 ↔ Analysis_Script.ipynb, lines 710 to 823 · score 0.63 · full regression models, Standard Error, personality, covariates, variables, age · Report a mismatch
  4. [4] § Materials and methods › Study 1 › Paradigm ↔ Analysis_Script.ipynb, lines 825 to 880 · score 0.61 · issue positions, climate change, demographics, education, healthcare, age · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 1,476 lines · 46 KB · no license · 3 matches

  1. # %%
  2. # --- Import libraries ---
  3. import pandas as pd
  4. import statsmodels.formula.api as smf
  5. import plotly.express as px
  6. import plotly.graph_objects as go
  7. import numpy as np
  8. import os
  9. # %%
  10. # --- Import data ---
  11. exp1_path = "/Users/elisavanderplas/Desktop/vdPlas_2025_Politics/exp1/behavioural_exp1.csv"
  12. exp2_path = "/Users/elisavanderplas/Desktop/vdPlas_2025_Politics/exp2/behavioural_exp2.csv"
  13. exp2_demo_path = "/Users/elisavanderplas/Desktop/vdPlas_2025_Politics/exp2/subjectLog_exp2.csv"
  14. data_exp1 = pd.read_csv(exp1_path, sep = ';')
  15. data_exp2 = pd.read_csv(exp2_path, sep = ',')
  16. demo_exp2 = pd.read_csv(exp2_demo_path)
  17. # %%
  18. # --- Prep data Exp 1 ---
  19. data_exp1['Video'] = pd.Categorical(data_exp1['Video'], categories = [1,2,3,4,5,6,7,8,9], ordered=False)
  20. data_exp1['Video'] = data_exp1['Video'].cat.rename_categories(["immigration_threat","immigration_blame","immigration_uncertain", "environment_threat","environment_blame","environment_uncertain", "healthcare_threat","healthcare_blame","healthcare_uncertain"])
  21. data_exp1['topic'] = pd.Categorical(data_exp1['video_2'], categories = [1,2,3], ordered=False)
  22. data_exp1['topic'] = data_exp1['topic'].cat.rename_categories(["immigration","environment","healthcare"])
  23. data_exp1['frame'] = pd.Categorical(data_exp1['video_3'], categories = [1,2,3], ordered=False)
  24. data_exp1['frame'] = data_exp1['frame'].cat.rename_categories(["threat","blame","uncertain"])
  25. data_exp1['gender'] = data_exp1['geslacht'] - 1 #back to 0 and 1 with 0 is male
  26. data_exp1['online_use'] = data_exp1['V1_6'] #1 = Uses Social Media (such as Facebook, Twitter or Youtube) to follow the news
  27. data_exp1['paper_read'] = data_exp1['V2a'] #1 = Reads political news in newspapers
  28. data_exp1['online_read'] = data_exp1['V2b'] #1 = Reads political news o social media
  29. data_exp1['interest_politics'] = data_exp1['V3'] #1-4 Likert from "Very interested in political issues" to "Not interested at all"
  30. data_exp1['CDA'] = data_exp1['V5_1']
  31. data_exp1['D66'] = data_exp1['V5_2']
  32. data_exp1['GL'] = data_exp1['V5_3']
  33. data_exp1['PvdA'] = data_exp1['V5_4']
  34. data_exp1['PVV'] = data_exp1['V5_5']
  35. data_exp1['SP'] = data_exp1['V5_6']
  36. data_exp1['VVD'] = data_exp1['V5_7']
  37. data_exp1['50P'] = data_exp1['V5_8']
  38. data_exp1['leftright'] = data_exp1['V_6']
  39. data_exp1['importance_gezondheidsz'] = data_exp1['V13_1']
  40. data_exp1['importance_immigr'] = data_exp1['V13_2']
  41. data_exp1['importance_milieu'] = data_exp1['V13_3']
  42. data_exp1['video_agreement'] = data_exp1['V10_4']
  43. data_exp1['video_realiability'] =data_exp1['V10_5']
  44. data_exp1['video_sharing'] = data_exp1['V10_6']
  45. # Drop columns that start with 'V'
  46. cols_to_drop = [col for col in data_exp1.columns if col.startswith('V')]
  47. df_exp1 = data_exp1.drop(columns=cols_to_drop)
  48. # Identify columns that start with 'INDEX'
  49. index_cols = [col for col in df_exp1.columns if col.startswith(('INDEX', 'BIG', 'online', 'geboortejaar', 'paper'))]
  50. # Clean and replace
  51. for col in index_cols:
  52. # Convert 'NULL#' to NaN
  53. df_exp1[col] = df_exp1[col].replace('NULL#', np.nan)
  54. # Convert column to numeric (in case it's still object type)
  55. df_exp1[col] = pd.to_numeric(df_exp1[col], errors='coerce')
  56. # Compute median (ignoring NaN)
  57. median_value = df_exp1[col].median()
  58. # Fill NaN with median and convert to int
  59. df_exp1[col] = df_exp1[col].fillna(median_value).astype(float)
  60. # Set 'uncertain' as reference category using pandas Categorical
  61. df_exp1['frame_reference'] = pd.Categorical(df_exp1['frame'], categories=['uncertain', 'threat', 'blame'])
  62. # %% [markdown]
  63. # Study 1
  64. # %%
  65. report_text = f"In Study 1 we analyzed the data of {len(df_exp1)} Dutch adults ({np.mean(df_exp1.gender)*100}% female)"
  66. print(report_text)
  67. # %%
  68. # Create descriptive statistics
  69. desc_stats = df_exp1.describe(include='all').T
  70. # Select relevant statistics
  71. desc_stats = desc_stats[['mean', 'std', 'min', 'max']]
  72. # Calculate mean + 3*std
  73. desc_stats['mean_plus_3std'] = desc_stats['mean'] + (3 * desc_stats['std'])
  74. # Round to 2 decimal places for better readability
  75. desc_stats = desc_stats.round(2)
  76. # Reorder columns
  77. desc_stats = desc_stats[['mean', 'std', 'min', 'max', 'mean_plus_3std']]
  78. print("\n" + "=" * 80)
  79. print("Descriptive Statistics for Numeric Variables Only:")
  80. print("=" * 80)
  81. # Get numeric columns
  82. numeric_cols = df_exp1.select_dtypes(include=[np.number]).columns
  83. numeric_stats = df_exp1[numeric_cols].describe().T
  84. numeric_stats['mean_plus_3std'] = numeric_stats['mean'] + (3 * numeric_stats['std'])
  85. numeric_stats = numeric_stats[['mean', 'std', 'min', 'max', 'mean_plus_3std']].round(2)
  86. print(numeric_stats)
  87. # If you want to save to CSV
  88. numeric_stats.to_csv('descriptive_statistics_study1.csv')
  89. # %%
  90. # --- Model 1 : Negative Arousal ---
  91. model_neg_arousal = smf.ols(
  92. 'NEG_AFFECT ~ C(frame_reference) + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + online_read + interest_politics + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_MIGRATION + INDEX_CLIMATE + INDEX_HEALTH + INDEX_CYNICISM ',
  93. data=df_exp1
  94. ).fit()
  95. print(model_neg_arousal.summary())
  96. model_pos_arousal = smf.ols(
  97. 'POS_AFFECT ~ C(frame_reference) + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + online_read + interest_politics + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_MIGRATION + INDEX_CLIMATE + INDEX_HEALTH + INDEX_CYNICISM ',
  98. data=df_exp1
  99. ).fit()
  100. print(model_pos_arousal.summary())
  101. # %%
  102. # --- Model 2 : Fear---
  103. model_fear = smf.ols(
  104. 'FEAR ~ C(frame_reference) + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + online_read + interest_politics + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_MIGRATION + INDEX_CLIMATE + INDEX_HEALTH + INDEX_CYNICISM ',
  105. data=df_exp1
  106. ).fit()
  107. print(model_fear.summary())
  108. # %%
  109. # --- Model 3 : Anger ---
  110. model_anger = smf.ols(
  111. 'ANGER ~ C(frame_reference) + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + online_read + interest_politics + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_MIGRATION + INDEX_CLIMATE + INDEX_HEALTH + INDEX_CYNICISM ',
  112. data=df_exp1
  113. ).fit()
  114. print(model_anger.summary())
  115. # %%
  116. import pandas as pd
  117. import plotly.graph_objects as go
  118. import numpy as np
  119. # Extract coefficients from Study 1 anger model
  120. def extract_study1_coefficients(model):
  121. """Extract threat and blame coefficients from Study 1 model"""
  122. coefficients = {}
  123. for param in model.params.index:
  124. if 'frame_reference' in param:
  125. if '[T.threat]' in param:
  126. coefficients['threat'] = {
  127. 'coef': model.params[param],
  128. 'se': model.bse[param],
  129. 'p_value': model.pvalues[param]
  130. }
  131. print(f"Study 1 Anger - Threat: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
  132. elif '[T.blame]' in param:
  133. coefficients['blame'] = {
  134. 'coef': model.params[param],
  135. 'se': model.bse[param],
  136. 'p_value': model.pvalues[param]
  137. }
  138. print(f"Study 1 Anger - Blame: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
  139. return coefficients
  140. # Extract coefficients from your Study 1 models
  141. study1_anger_coefs = extract_study1_coefficients(model_anger)
  142. study1_fear_coefs = extract_study1_coefficients(model_fear) #
  143. # Create the updated plot with Study 1 coefficients
  144. fig = go.Figure()
  145. # Define conditions and colors
  146. conditions = ['Threat', 'Blame']
  147. fear_color = '#56B4E9' # light blue
  148. anger_color = '#D55E00' # dark pink / orange-red
  149. # Add Fear data
  150. fig.add_trace(go.Scatter(
  151. name='Fear',
  152. x=conditions,
  153. y=[study1_fear_coefs['threat']['coef'], study1_fear_coefs['blame']['coef']],
  154. error_y=dict(
  155. type='data',
  156. array=[study1_fear_coefs['threat']['se'], study1_fear_coefs['blame']['se']],
  157. visible=True,
  158. thickness=3,
  159. width=6,
  160. color='black'
  161. ),
  162. mode='markers',
  163. marker=dict(
  164. size=16,
  165. color=fear_color,
  166. symbol='diamond',
  167. line=dict(width=3, color='black')
  168. ),
  169. hovertemplate='Fear - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
  170. ))
  171. # Add Anger data
  172. fig.add_trace(go.Scatter(
  173. name='Anger',
  174. x=conditions,
  175. y=[study1_anger_coefs['threat']['coef'], study1_anger_coefs['blame']['coef']],
  176. error_y=dict(
  177. type='data',
  178. array=[study1_anger_coefs['threat']['se'], study1_anger_coefs['blame']['se']],
  179. visible=True,
  180. thickness=3,
  181. width=6,
  182. color='black' #
  183. ),
  184. mode='markers',
  185. marker=dict(
  186. size=16,
  187. color=anger_color,
  188. symbol='diamond',
  189. line=dict(width=3, color='black')
  190. ),
  191. hovertemplate='Anger - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
  192. ))
  193. # Calculate significance stars based on p-values
  194. def get_significance_stars(p_value):
  195. if p_value < 0.001:
  196. return '***'
  197. elif p_value < 0.01:
  198. return '**'
  199. elif p_value < 0.05:
  200. return '*'
  201. else:
  202. return 'n.s.'
  203. # Add significance annotations
  204. y_offset_fear = max(study1_fear_coefs['threat']['coef'] + study1_fear_coefs['threat']['se'],
  205. study1_fear_coefs['blame']['coef'] + study1_fear_coefs['blame']['se']) * 0.2
  206. y_offset_anger = max(study1_anger_coefs['threat']['coef'] + study1_anger_coefs['threat']['se'],
  207. study1_anger_coefs['blame']['coef'] + study1_anger_coefs['blame']['se']) * 0.2
  208. # Fear significance annotations
  209. fig.add_annotation(
  210. x='Threat',
  211. y=study1_fear_coefs['threat']['coef'] + study1_fear_coefs['threat']['se'] + y_offset_fear,
  212. text=get_significance_stars(study1_fear_coefs['threat']['p_value']),
  213. showarrow=False,
  214. font=dict(
  215. size=22 if get_significance_stars(study1_fear_coefs['threat']['p_value']) != 'n.s.' else 16,
  216. color='black',
  217. family='Arial Black'
  218. ),
  219. yshift=10
  220. )
  221. fig.add_annotation(
  222. x='Blame',
  223. y=study1_fear_coefs['blame']['coef'] + study1_fear_coefs['blame']['se'] + y_offset_fear,
  224. text=get_significance_stars(study1_fear_coefs['blame']['p_value']),
  225. showarrow=False,
  226. font=dict(
  227. size=22 if get_significance_stars(study1_fear_coefs['blame']['p_value']) != 'n.s.' else 16,
  228. color='black',
  229. family='Arial Black'
  230. ),
  231. yshift=10
  232. )
  233. # Anger significance annotations
  234. fig.add_annotation(
  235. x='Threat',
  236. y=study1_anger_coefs['threat']['coef'] + study1_anger_coefs['threat']['se'] + y_offset_anger,
  237. text=get_significance_stars(study1_anger_coefs['threat']['p_value']),
  238. showarrow=False,
  239. font=dict(
  240. size=22 if get_significance_stars(study1_anger_coefs['threat']['p_value']) != 'n.s.' else 16,
  241. color='black',
  242. family='Arial Black'
  243. ),
  244. yshift=10
  245. )
  246. fig.add_annotation(
  247. x='Blame',
  248. y=study1_anger_coefs['blame']['coef'] + study1_anger_coefs['blame']['se'] + y_offset_anger,
  249. text=get_significance_stars(study1_anger_coefs['blame']['p_value']),
  250. showarrow=False,
  251. font=dict(
  252. size=22 if get_significance_stars(study1_anger_coefs['blame']['p_value']) != 'n.s.' else 16,
  253. color='black',
  254. family='Arial Black'
  255. ),
  256. yshift=10
  257. )
  258. # Customize layout
  259. fig.update_layout(
  260. title=dict(
  261. text="<b>(b.) Study 1</b>",
  262. x=0.05,
  263. xanchor='left',
  264. font=dict(size=20, family='Arial Black')
  265. ),
  266. xaxis=dict(
  267. title="",
  268. tickmode='array',
  269. tickvals=conditions,
  270. ticktext=conditions,
  271. tickfont=dict(size=16, family='Arial'),
  272. showgrid=False,
  273. range=[-0.3, 1.3]
  274. ),
  275. yaxis=dict(
  276. title="Simplification frame impact<br>on negative arousal (a.u.)",
  277. title_font=dict(size=18, family='Arial'),
  278. tickfont=dict(size=14, family='Arial'),
  279. gridcolor='lightgray',
  280. gridwidth=1,
  281. zeroline=True,
  282. zerolinewidth=2,
  283. zerolinecolor='black'
  284. ),
  285. showlegend=True,
  286. legend=dict(
  287. orientation="h",
  288. yanchor="bottom",
  289. y=1.02,
  290. xanchor="center",
  291. x=0.5,
  292. font=dict(size=14, family='Arial'),
  293. bgcolor='rgba(255,255,255,0.8)'
  294. ),
  295. template="plotly_white",
  296. width=400,
  297. height=400,
  298. margin=dict(l=80, r=40, t=80, b=60)
  299. )
  300. # Add a subtle background
  301. fig.update_layout(
  302. plot_bgcolor='white',
  303. paper_bgcolor='white'
  304. )
  305. fig.show()
  306. # %%
  307. # --- Model 4A : PVV attitudes ---
  308. model_pvv = smf.ols(
  309. 'PVV ~ C(topic) + NEG_AFFECT + C(frame) + C(topic):NEG_AFFECT + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + online_read + interest_politics + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_MIGRATION + INDEX_CLIMATE + INDEX_HEALTH + INDEX_CYNICISM + INDEX_MIGRATION :C(frame_reference) + INDEX_MIGRATION :C(frame_reference):NEG_AFFECT',
  310. data=df_exp1
  311. ).fit()
  312. print(model_pvv.summary())
  313. # --- Model 4B : GL attitudes ---
  314. model_gl = smf.ols(
  315. 'GL ~ C(topic) + NEG_AFFECT + C(frame)+ C(topic):NEG_AFFECT + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + online_read + interest_politics + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_MIGRATION + INDEX_CLIMATE + INDEX_HEALTH + INDEX_CYNICISM + INDEX_CLIMATE:C(frame_reference) + INDEX_CLIMATE:C(frame_reference):NEG_AFFECT',
  316. data=df_exp1
  317. ).fit()
  318. print(model_gl.summary())
  319. # %%
  320. import plotly.graph_objects as go
  321. from plotly.subplots import make_subplots
  322. import numpy as np
  323. # Create subplot figure with 1 row and 2 columns
  324. fig = make_subplots(rows=1, cols=2,
  325. subplot_titles=('A) GL Attitudes by Negative Affect and Topic',
  326. 'B) PVV Attitudes by Negative Affect and Topic'),
  327. shared_yaxes=False)
  328. # Define topics and colors
  329. topics = ['immigration', 'environment', 'healthcare']
  330. colors = ['red', 'green', 'blue'] # red for immigration, green for environment, blue for healthcare
  331. # negative affect values for prediction
  332. neg_affect_range = np.linspace(df_exp1['NEG_AFFECT'].min(), df_exp1['NEG_AFFECT'].max(), 50)
  333. # --- LEFT PLOT: GL Attitudes by Negative Affect ---
  334. for i, topic in enumerate(topics):
  335. # Calculate predicted GL attitudes for this topic across negative affect range
  336. if topic == 'immigration':
  337. # Reference category for topic
  338. topic_effect = 0
  339. topic_affect_interaction = 0
  340. elif topic == 'environment':
  341. topic_effect = -0.6007 # C(topic)[T.environment]
  342. topic_affect_interaction = 0.2263 # C(topic)[T.environment]:NEG_AFFECT
  343. else: # healthcare
  344. topic_effect = -0.3747 # C(topic)[T.healthcare]
  345. topic_affect_interaction = 0.1127 # C(topic)[T.healthcare]:NEG_AFFECT
  346. predicted_gl = (11.1089 + # Intercept
  347. topic_effect +
  348. neg_affect_range * 0.0335 + # NEG_AFFECT main effect
  349. neg_affect_range * topic_affect_interaction)
  350. fig.add_trace(
  351. go.Scatter(x=neg_affect_range, y=predicted_gl,
  352. mode='lines',
  353. name=f'{topic}',
  354. line=dict(color=colors[i], width=3),
  355. showlegend=True),
  356. row=1, col=1
  357. )
  358. # --- RIGHT PLOT: PVV Attitudes by Negative Affect ---
  359. for i, topic in enumerate(topics):
  360. # Calculate predicted PVV attitudes for this topic across negative affect range
  361. if topic == 'immigration':
  362. # Reference category for topic
  363. topic_effect = 0
  364. topic_affect_interaction = 0
  365. elif topic == 'environment':
  366. topic_effect = 1.4084 # C(topic)[T.environment]
  367. topic_affect_interaction = -0.2823 # C(topic)[T.environment]:NEG_AFFECT
  368. else: # healthcare
  369. topic_effect = 0.9733 # C(topic)[T.healthcare]
  370. topic_affect_interaction = -0.3822 # C(topic)[T.healthcare]:NEG_AFFECT
  371. predicted_pvv = (5.2341 + # Intercept
  372. topic_effect +
  373. neg_affect_range * 0.3433 + # NEG_AFFECT main effect
  374. neg_affect_range * topic_affect_interaction)
  375. fig.add_trace(
  376. go.Scatter(x=neg_affect_range, y=predicted_pvv,
  377. mode='lines',
  378. name=f'PVV - {topic}',
  379. line=dict(color=colors[i], width=3, dash='dash'),
  380. showlegend=False),
  381. row=1, col=2
  382. )
  383. # Update layout
  384. fig.update_layout(
  385. title_text="Party Attitudes by Negative Affect and Video Topic",
  386. height=500,
  387. width=1000,
  388. template="plotly_white"
  389. )
  390. # Update axes labels
  391. fig.update_xaxes(title_text="Negative Affect", row=1, col=1)
  392. fig.update_xaxes(title_text="Negative Affect", row=1, col=2)
  393. fig.update_yaxes(title_text="GL Attitudes", row=1, col=1)
  394. fig.update_yaxes(title_text="PVV Attitudes", row=1, col=2)
  395. fig.show()
  396. # %%
  397. # --- Model 4A :Agreement ---
  398. model_agreement = smf.ols(
  399. 'video_agreement ~ C(frame):NEG_AFFECT + NEG_AFFECT + C(frame) + C(topic) + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_MIGRATION + INDEX_CLIMATE + INDEX_HEALTH + INDEX_CYNICISM ',
  400. data=df_exp1
  401. ).fit()
  402. print(model_agreement.summary())
  403. # %%
  404. # Create overall importance as mean of the three indices
  405. df_exp1['ISSUE_IMPORTANCE'] = df_exp1[['INDEX_CLIMATE', 'INDEX_HEALTH', 'INDEX_CYNICISM']].mean(axis=1)
  406. # Run the model with overall importance
  407. model_importance= smf.ols(
  408. 'ISSUE_IMPORTANCE ~ NEG_AFFECT + C(frame_reference) + NEG_AFFECT + C(frame_reference) + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_CYNICISM',
  409. data=df_exp1
  410. ).fit()
  411. print(model_importance.summary())
  412. # %%
  413. ## Model 5 - Sharing
  414. model_sharing = smf.ols(
  415. 'video_sharing ~ NEG_AFFECT + C(frame_reference) + NEG_AFFECT + BIG5_O + BIG5_C + BIG5_E + BIG5_A + BIG5_N + opleiding + lft4 + gender + leftright + INDEX_AUTHO + INDEX_MIGRATION + INDEX_CLIMATE + INDEX_HEALTH + INDEX_CYNICISM ',
  416. data=df_exp1
  417. ).fit()
  418. print(model_sharing.summary())
  419. # %%
  420. import pandas as pd
  421. import plotly.graph_objects as go
  422. import numpy as np
  423. # Extract coefficients from Study 1 anger model
  424. def extract_study1_coefficients(model):
  425. """Extract threat and blame coefficients from Study 1 model"""
  426. coefficients = {}
  427. for param in model.params.index:
  428. if 'frame_reference' in param:
  429. if '[T.threat]' in param:
  430. coefficients['threat'] = {
  431. 'coef': model.params[param],
  432. 'se': model.bse[param],
  433. 'p_value': model.pvalues[param]
  434. }
  435. print(f"Study 1 Anger - Threat: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
  436. elif '[T.blame]' in param:
  437. coefficients['blame'] = {
  438. 'coef': model.params[param],
  439. 'se': model.bse[param],
  440. 'p_value': model.pvalues[param]
  441. }
  442. print(f"Study 1 Anger - Blame: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
  443. return coefficients
  444. # Extract coefficients from your Study 1 models
  445. sharing_coefs = extract_study1_coefficients(model_sharing)
  446. importance_coefs = extract_study1_coefficients(model_importance)
  447. # Create the plot with the specified color scheme and formatting
  448. fig = go.Figure()
  449. # Define conditions and colors from your specification
  450. conditions = ['Threat', 'Blame']
  451. sharing_color = '#2C4A52' # Dark teal (agreement/upper diamond)
  452. importance_color = '#E6C87F' # Muted yellow-beige (importance/lower diamond)
  453. error_color = '#222222' # Black (slightly gray)
  454. grid_color = '#D3D3D3' # Light gray
  455. # Adjust coefficients by moving everything 0.5 down on y-axis
  456. adjusted_sharing_threat = sharing_coefs['threat']['coef'] - 0.5
  457. adjusted_sharing_blame = sharing_coefs['blame']['coef'] - 0.5
  458. adjusted_importance_threat = importance_coefs['threat']['coef'] - 0.5
  459. adjusted_importance_blame = importance_coefs['blame']['coef'] - 0.5
  460. # Add Video Sharing data with diamond markers (NO connecting lines)
  461. fig.add_trace(go.Scatter(
  462. name='Video Sharing',
  463. x=conditions,
  464. y=[adjusted_sharing_threat, adjusted_sharing_blame],
  465. error_y=dict(
  466. type='data',
  467. array=[sharing_coefs['threat']['se'], sharing_coefs['blame']['se']],
  468. visible=True,
  469. thickness=3,
  470. width=6,
  471. color=error_color
  472. ),
  473. mode='markers',
  474. marker=dict(
  475. size=20,
  476. color=sharing_color,
  477. symbol='diamond',
  478. line=dict(width=3, color=error_color)
  479. ),
  480. hovertemplate='Video Sharing - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
  481. ))
  482. # Add Issue Importance data with diamond markers
  483. fig.add_trace(go.Scatter(
  484. name='Issue Importance',
  485. x=conditions,
  486. y=[adjusted_importance_threat, adjusted_importance_blame],
  487. error_y=dict(
  488. type='data',
  489. array=[importance_coefs['threat']['se'], importance_coefs['blame']['se']],
  490. visible=True,
  491. thickness=3,
  492. width=6,
  493. color=error_color
  494. ),
  495. mode='markers',
  496. marker=dict(
  497. size=20,
  498. color=importance_color,
  499. symbol='diamond',
  500. line=dict(width=3, color=error_color)
  501. ),
  502. hovertemplate='Issue Importance - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
  503. ))
  504. # Calculate significance stars based on p-values
  505. def get_significance_stars(p_value):
  506. if p_value < 0.001:
  507. return '***'
  508. elif p_value < 0.01:
  509. return '**'
  510. elif p_value < 0.05:
  511. return '*'
  512. else:
  513. return 'n.s.'
  514. # Add significance annotations
  515. y_offset = 0.3
  516. # Video Sharing significance annotations
  517. fig.add_annotation(
  518. x='Threat',
  519. y=adjusted_sharing_threat + sharing_coefs['threat']['se'] + y_offset,
  520. text=get_significance_stars(sharing_coefs['threat']['p_value']),
  521. showarrow=False,
  522. font=dict(
  523. size=20 if get_significance_stars(sharing_coefs['threat']['p_value']) != 'n.s.' else 14, # LESS BOLD sizing
  524. color='black',
  525. family='Arial'
  526. ),
  527. yshift=10
  528. )
  529. fig.add_annotation(
  530. x='Blame',
  531. y=adjusted_sharing_blame + sharing_coefs['blame']['se'] + y_offset,
  532. text=get_significance_stars(sharing_coefs['blame']['p_value']),
  533. showarrow=False,
  534. font=dict(
  535. size=20 if get_significance_stars(sharing_coefs['blame']['p_value']) != 'n.s.' else 14, # LESS BOLD sizing
  536. color='black',
  537. family='Arial'
  538. ),
  539. yshift=10
  540. )
  541. # Issue Importance significance annotations
  542. fig.add_annotation(
  543. x='Threat',
  544. y=adjusted_importance_threat + importance_coefs['threat']['se'] + y_offset,
  545. text=get_significance_stars(importance_coefs['threat']['p_value']),
  546. showarrow=False,
  547. font=dict(
  548. size=20 if get_significance_stars(importance_coefs['threat']['p_value']) != 'n.s.' else 14,
  549. color='black',
  550. family='Arial'
  551. ),
  552. yshift=10
  553. )
  554. fig.add_annotation(
  555. x='Blame',
  556. y=adjusted_importance_blame + importance_coefs['blame']['se'] + y_offset,
  557. text=get_significance_stars(importance_coefs['blame']['p_value']),
  558. showarrow=False,
  559. font=dict(
  560. size=20 if get_significance_stars(importance_coefs['blame']['p_value']) != 'n.s.' else 14,
  561. color='black',
  562. family='Arial'
  563. ),
  564. yshift=10
  565. )
  566. # Customize layout
  567. fig.update_layout(
  568. title=dict(
  569. text="(a.) Study 1",
  570. x=0.05,
  571. xanchor='left',
  572. font=dict(size=22, family='Arial')
  573. ),
  574. xaxis=dict(
  575. title="",
  576. tickmode='array',
  577. tickvals=conditions,
  578. ticktext=conditions,
  579. tickfont=dict(size=18, family='Arial'),
  580. showgrid=False,
  581. range=[-0.3, 1.3]
  582. ),
  583. yaxis=dict(
  584. title="Simplification frame impact<br>on political behaviour (a.u.)",
  585. title_font=dict(size=20, family='Arial'),
  586. tickfont=dict(size=16, family='Arial'),
  587. gridcolor=grid_color,
  588. gridwidth=1,
  589. zeroline=True,
  590. zerolinewidth=2,
  591. zerolinecolor='black'
  592. ),
  593. showlegend=True,
  594. legend=dict(
  595. orientation="h",
  596. yanchor="bottom",
  597. y=1.02,
  598. xanchor="center",
  599. x=0.5,
  600. font=dict(size=16, family='Arial'),
  601. bgcolor='rgba(255,255,255,0.8)'
  602. ),
  603. template="plotly_white",
  604. width=400,
  605. height=500,
  606. margin=dict(l=80, r=40, t=80, b=60)
  607. )
  608. fig.update_layout(
  609. plot_bgcolor='white',
  610. paper_bgcolor='white'
  611. )
  612. fig.show()
  613. # %%
  614. ##Print all model outputs
  615. # Create a function to extract model results
  616. def extract_regression_results(model, model_name):
  617. """Extract regression coefficients and statistics from a statsmodels model."""
  618. # Get summary table
  619. results_summary = model.summary2().tables[1]
  620. # Reset index to get variable names as column
  621. results_df = results_summary.reset_index()
  622. # Rename columns
  623. results_df.columns = ['Variable', 'coef', 'std_err', 't', 'p_value', 'CI_lower', 'CI_upper']
  624. # Add significance stars
  625. def add_stars(p):
  626. if p < 0.001:
  627. return '***'
  628. elif p < 0.01:
  629. return '**'
  630. elif p < 0.05:
  631. return '*'
  632. elif p < 0.1:
  633. return '.'
  634. else:
  635. return ''
  636. results_df['sig'] = results_df['p_value'].apply(add_stars)
  637. # Format coefficient with standard error and significance
  638. results_df['coef_se'] = results_df.apply(
  639. lambda x: f"{x['coef']:.3f}{x['sig']}\n({x['std_err']:.3f})", axis=1
  640. )
  641. # Add model name
  642. results_df['Model'] = model_name
  643. # Add model statistics
  644. n_obs = model.nobs
  645. r_squared = model.rsquared
  646. adj_r_squared = model.rsquared_adj
  647. f_stat = model.fvalue
  648. f_pvalue = model.f_pvalue
  649. stats_row = pd.DataFrame({
  650. 'Variable': ['N', 'R²', 'Adj. R²', 'F-statistic', 'F p-value'],
  651. 'coef_se': [f"{n_obs:.0f}", f"{r_squared:.3f}", f"{adj_r_squared:.3f}",
  652. f"{f_stat:.3f}", f"{f_pvalue:.3f}"],
  653. 'Model': [model_name] * 5
  654. })
  655. return pd.concat([results_df[['Variable', 'coef_se', 'Model']], stats_row], ignore_index=True)
  656. # Extract results from each model
  657. pvv_results = extract_regression_results(model_pvv, "PVV attitudes")
  658. gl_results = extract_regression_results(model_gl, "GL attitudes")
  659. video_agreement_results = extract_regression_results(model_agreement, "Video agreement")
  660. issue_importance_results = extract_regression_results(model_importance, "Issue importance")
  661. # Combine all results
  662. all_results = pd.concat([pvv_results, gl_results, video_agreement_results, issue_importance_results],
  663. ignore_index=True)
  664. # Pivot to wide format for the table
  665. table_s1_3 = all_results.pivot_table(
  666. index='Variable',
  667. columns='Model',
  668. values='coef_se',
  669. aggfunc='first'
  670. )
  671. # Reorder columns if needed
  672. desired_order = ['PVV attitudes', 'GL attitudes', 'Video agreement', 'Issue importance']
  673. table_s1_3 = table_s1_3[desired_order]
  674. # Sort variables in a logical order (intercept first, then main predictors, then covariates)
  675. def sort_key(var):
  676. if var == 'const' or var == 'Intercept':
  677. return (0, var)
  678. elif 'frame' in var.lower() or 'video' in var.lower():
  679. return (1, var)
  680. elif 'topic' in var.lower():
  681. return (2, var)
  682. elif 'gender' in var.lower() or 'age' in var.lower():
  683. return (3, var)
  684. elif 'BIG5' in var or 'personality' in var.lower():
  685. return (4, var)
  686. elif 'INDEX' in var:
  687. return (5, var)
  688. elif 'affect' in var.lower() or 'FEAR' in var or 'ANGER' in var:
  689. return (6, var)
  690. elif 'interest' in var.lower() or 'read' in var.lower():
  691. return (7, var)
  692. else:
  693. return (8, var)
  694. table_s1_3 = table_s1_3.reindex(sorted(table_s1_3.index, key=sort_key))
  695. # Add descriptive header
  696. print("=" * 100)
  697. print("Supplementary Table S1.3")
  698. print("Full Regression Models from Study 1 with All Covariates Included")
  699. print("=" * 100)
  700. print("\nNote: Coefficients shown with standard errors in parentheses.")
  701. print("Significance levels: *p < .05, **p < .01, ***p < .001\n")
  702. # Display the table
  703. with pd.option_context('display.max_rows', None, 'display.width', 1000):
  704. print(table_s1_3)
  705. # Save to CSV
  706. table_s1_3.to_csv('Supplementary_Table_S1_3_Full_Regression_Models.csv')
  707. print("\nTable saved to 'Supplementary_Table_S1_3_Full_Regression_Models.csv'")
  708. # %% [markdown]
  709. # Study 2
  710. # %%
  711. import pandas as pd
  712. import numpy as np
  713. import statsmodels.api as sm
  714. import statsmodels.formula.api as smf
  715. from scipy import stats
  716. from statsmodels.stats.anova import anova_lm
  717. from sklearn.preprocessing import StandardScaler
  718. # Factors - Convert to categorical with labels
  719. data_exp2['subj_idx'] = data_exp2['subj_idx'].astype('category')
  720. # Create factors
  721. data_exp2['Topic'] = data_exp2['Topic'].replace({1: 'immigration', 2: 'climate change', 3: 'healthcare'}).astype('category')
  722. data_exp2['Appraisal'] = data_exp2['Appraisal '].replace({1: 'uncertain', 2: 'threat', 3: 'blame'}).astype('category')
  723. # Merge demographics from demo_exp2
  724. expected_demo_cols = ['sj_nr', 'gender2', 'age', 'education', 'pol_autho', 'pol_scep',
  725. 'issue_positions_immigration', 'issue_positions_climate', 'issue_positions_health']
  726. # Find which expected columns actually exist
  727. available_demo_cols = [col for col in expected_demo_cols if col in demo_exp2.columns]
  728. print(f"Available demographic columns for merging: {available_demo_cols}")
  729. # Merge demographics
  730. data_exp2 = data_exp2.merge(
  731. demo_exp2[available_demo_cols],
  732. left_on='subj_idx',
  733. right_on='sj_nr',
  734. how='left'
  735. )
  736. print(f"Study 2 data shape after merge: {data_exp2.shape}")
  737. # Rename columns
  738. rename_mapping = {}
  739. if 'gender2' in data_exp2.columns:
  740. rename_mapping['gender2'] = 'gender'
  741. if 'education' in data_exp2.columns:
  742. rename_mapping['education'] = 'edu'
  743. if 'issue_positions_immigration' in data_exp2.columns:
  744. rename_mapping['issue_positions_immigration'] = 'PositionImm'
  745. if 'issue_positions_climate' in data_exp2.columns:
  746. rename_mapping['issue_positions_climate'] = 'PositionCl'
  747. if 'issue_positions_health' in data_exp2.columns:
  748. rename_mapping['issue_positions_health'] = 'PositionHe'
  749. data_exp2 = data_exp2.rename(columns=rename_mapping)
  750. # Set 'uncertain' as reference category using pandas Categorical
  751. data_exp2['frame_reference'] = pd.Categorical(data_exp2['Appraisal'], categories=['uncertain', 'threat', 'blame'])
  752. # For clarity
  753. data_exp2['video_sharing'] = data_exp2['clap']
  754. # %%
  755. report_text = f"In Study 2 we analyzed the data of {len(data_exp2.sj_nr.unique())} Dutch adults ({np.mean(data_exp2.gender)*100}% female))"
  756. print(report_text)
  757. # %%
  758. # Create descriptive statistics
  759. desc_stats = data_exp2.describe(include='all').T
  760. # Select relevant statistics
  761. desc_stats = desc_stats[['mean', 'std', 'min', 'max']]
  762. # Calculate mean + 3*std
  763. desc_stats['mean_plus_3std'] = desc_stats['mean'] + (3 * desc_stats['std'])
  764. # Round to 2 decimal places for better readability
  765. desc_stats = desc_stats.round(2)
  766. # Reorder columns
  767. desc_stats = desc_stats[['mean', 'std', 'min', 'max', 'mean_plus_3std']]
  768. print("\n" + "=" * 80)
  769. print("Descriptive Statistics for Numeric Variables Only:")
  770. print("=" * 80)
  771. # Get numeric columns
  772. numeric_cols = data_exp2.select_dtypes(include=[np.number]).columns
  773. numeric_stats = data_exp2[numeric_cols].describe().T
  774. numeric_stats['mean_plus_3std'] = numeric_stats['mean'] + (3 * numeric_stats['std'])
  775. numeric_stats = numeric_stats[['mean', 'std', 'min', 'max', 'mean_plus_3std']].round(2)
  776. print(numeric_stats)
  777. # If you want to save to CSV
  778. numeric_stats.to_csv('descriptive_statistics_study2.csv')
  779. # %%
  780. import pandas as pd
  781. import numpy as np
  782. import statsmodels.api as sm
  783. from statsmodels.formula.api import ols
  784. import matplotlib.pyplot as plt
  785. import seaborn as sns
  786. # Negative Affect model
  787. negative_appraisal = ols('NEG_AFFECT ~ C(frame_reference) + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
  788. print("=== NEGATIVE AFFECT MODEL ===")
  789. print(negative_appraisal.summary())
  790. print("\n")
  791. # Fear model
  792. fear_appraisal = ols('FEAR ~ C(frame_reference) + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
  793. print("=== FEAR MODEL ===")
  794. print(fear_appraisal.summary())
  795. print("\n")
  796. # Anger model
  797. anger_appraisal = ols('ANGER ~ C(frame_reference) + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
  798. print("=== ANGER MODEL ===")
  799. print(anger_appraisal.summary())
  800. print("\n")
  801. # PART 2: BEHAVIOURAL EFFECTS
  802. # Importance model
  803. imp_appraisal = ols('importance ~ C(frame_reference) + NEG_AFFECT + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
  804. print("=== IMPORTANCE MODEL ===")
  805. print(imp_appraisal.summary())
  806. print("\n")
  807. # Clap model
  808. clap_appraisal = ols('video_sharing ~ C(frame_reference) + NEG_AFFECT + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
  809. print("=== CLAP MODEL ===")
  810. print(clap_appraisal.summary())
  811. print("\n")
  812. # %%
  813. import pandas as pd
  814. import plotly.graph_objects as go
  815. import numpy as np
  816. # Create the plot
  817. fig = go.Figure()
  818. # Extract coefficients from Study 1 anger model
  819. def extract_study2_coefficients(model):
  820. """Extract threat and blame coefficients from Study 2 model"""
  821. coefficients = {}
  822. for param in model.params.index:
  823. if 'frame_reference' in param:
  824. if '[T.threat]' in param:
  825. coefficients['threat'] = {
  826. 'coef': model.params[param],
  827. 'se': model.bse[param],
  828. 'p_value': model.pvalues[param]
  829. }
  830. print(f"Study 2 Anger - Threat: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
  831. elif '[T.blame]' in param:
  832. coefficients['blame'] = {
  833. 'coef': model.params[param],
  834. 'se': model.bse[param],
  835. 'p_value': model.pvalues[param]
  836. }
  837. print(f"Study 2 Anger - Blame: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
  838. return coefficients
  839. fear_coefs = extract_study2_coefficients(fear_appraisal)
  840. anger_coefs = extract_study2_coefficients(anger_appraisal)
  841. clap_coefs = extract_study2_coefficients(clap_appraisal)
  842. imp_coefs = extract_study2_coefficients(imp_appraisal)
  843. # Define conditions and colors
  844. conditions = ['Threat', 'Blame']
  845. fear_color = '#56B4E9' # light blue
  846. anger_color = '#D55E00' # dark pink / orange-red
  847. error_color = '#222222' # Black error bars
  848. grid_color = '#D3D3D3' # Light gray
  849. # Add Fear data
  850. fig.add_trace(go.Scatter(
  851. name='Fear',
  852. x=conditions,
  853. y=[fear_coefs['threat']['coef'], fear_coefs['blame']['coef']],
  854. error_y=dict(
  855. type='data',
  856. array=[fear_coefs['threat']['se'], fear_coefs['blame']['se']],
  857. visible=True,
  858. thickness=3,
  859. width=6,
  860. color=error_color
  861. ),
  862. mode='markers',
  863. marker=dict(
  864. size=20,
  865. color=fear_color,
  866. symbol='diamond',
  867. line=dict(width=3, color=error_color)
  868. ),
  869. hovertemplate='Fear - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
  870. ))
  871. # Add Anger data
  872. fig.add_trace(go.Scatter(
  873. name='Anger',
  874. x=conditions,
  875. y=[anger_coefs['threat']['coef'], anger_coefs['blame']['coef']],
  876. error_y=dict(
  877. type='data',
  878. array=[anger_coefs['threat']['se'], anger_coefs['blame']['se']],
  879. visible=True,
  880. thickness=3,
  881. width=6,
  882. color=error_color
  883. ),
  884. mode='markers',
  885. marker=dict(
  886. size=20,
  887. color=anger_color,
  888. symbol='diamond',
  889. line=dict(width=3, color=error_color)
  890. ),
  891. hovertemplate='Anger - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
  892. ))
  893. # Calculate significance
  894. def get_significance_stars(p_value):
  895. if p_value < 0.001:
  896. return '***'
  897. elif p_value < 0.01:
  898. return '**'
  899. elif p_value < 0.05:
  900. return '*'
  901. else:
  902. return 'n.s.'
  903. # Add significance annotations
  904. y_offset = 1.5
  905. # Fear significance
  906. fig.add_annotation(
  907. x='Threat',
  908. y=fear_coefs['threat']['coef'] + fear_coefs['threat']['se'] + y_offset,
  909. text=get_significance_stars(fear_coefs['threat']['p_value']),
  910. showarrow=False,
  911. font=dict(
  912. size=20 if get_significance_stars(fear_coefs['threat']['p_value']) != 'n.s.' else 14,
  913. color='black',
  914. family='Arial'
  915. ),
  916. yshift=10
  917. )
  918. fig.add_annotation(
  919. x='Blame',
  920. y=fear_coefs['blame']['coef'] + fear_coefs['blame']['se'] + y_offset,
  921. text=get_significance_stars(fear_coefs['blame']['p_value']),
  922. showarrow=False,
  923. font=dict(
  924. size=20 if get_significance_stars(fear_coefs['blame']['p_value']) != 'n.s.' else 14,
  925. color='black',
  926. family='Arial'
  927. ),
  928. yshift=10
  929. )
  930. # Anger significance
  931. fig.add_annotation(
  932. x='Threat',
  933. y=anger_coefs['threat']['coef'] + anger_coefs['threat']['se'] + y_offset,
  934. text=get_significance_stars(anger_coefs['threat']['p_value']),
  935. showarrow=False,
  936. font=dict(
  937. size=20 if get_significance_stars(anger_coefs['threat']['p_value']) != 'n.s.' else 14,
  938. color='black',
  939. family='Arial'
  940. ),
  941. yshift=10
  942. )
  943. fig.add_annotation(
  944. x='Blame',
  945. y=anger_coefs['blame']['coef'] + anger_coefs['blame']['se'] + y_offset,
  946. text=get_significance_stars(anger_coefs['blame']['p_value']),
  947. showarrow=False,
  948. font=dict(
  949. size=20 if get_significance_stars(anger_coefs['blame']['p_value']) != 'n.s.' else 14,
  950. color='black',
  951. family='Arial'
  952. ),
  953. yshift=10
  954. )
  955. # Customize layout
  956. fig.update_layout(
  957. title=dict(
  958. x=0.05,
  959. xanchor='left',
  960. font=dict(size=22, family='Arial')
  961. ),
  962. xaxis=dict(
  963. title="",
  964. tickmode='array',
  965. tickvals=conditions,
  966. ticktext=conditions,
  967. tickfont=dict(size=18, family='Arial'),
  968. showgrid=False,
  969. range=[-0.3, 1.3]
  970. ),
  971. yaxis=dict(
  972. title="Simplification frame impact<br>on negative arousal (a.u.)",
  973. title_font=dict(size=20, family='Arial'),
  974. tickfont=dict(size=16, family='Arial'),
  975. gridcolor=grid_color,
  976. gridwidth=1,
  977. zeroline=True,
  978. zerolinewidth=2,
  979. zerolinecolor='black'
  980. ),
  981. showlegend=True,
  982. legend=dict(
  983. orientation="h",
  984. yanchor="bottom",
  985. y=1.02,
  986. xanchor="center",
  987. x=0.5,
  988. font=dict(size=16, family='Arial'),
  989. bgcolor='rgba(255,255,255,0.8)'
  990. ),
  991. template="plotly_white",
  992. width=400,
  993. height=400,
  994. margin=dict(l=80, r=40, t=80, b=60)
  995. )
  996. fig.update_layout(
  997. plot_bgcolor='white',
  998. paper_bgcolor='white'
  999. )
  1000. fig.show()
  1001. # %%
  1002. import pandas as pd
  1003. import plotly.graph_objects as go
  1004. import numpy as np
  1005. # Create the plot
  1006. fig = go.Figure()
  1007. # Define conditions a
  1008. conditions = ['Threat', 'Blame']
  1009. sharing_color = '#2C4A52' # Dark teal (agreement/upper diamond)
  1010. importance_color = '#E6C87F' # Muted yellow-beige (importance/lower diamond)
  1011. error_color = '#222222' # Black (slightly gray)
  1012. grid_color = '#D3D3D3' # Light gray
  1013. # Extract coefficients
  1014. threat_imp_coef = 2.7773
  1015. threat_imp_se = 2.660
  1016. blame_imp_coef = 5.2652
  1017. blame_imp_se = 2.505
  1018. threat_clap_coef = -1.2202
  1019. threat_clap_se = 0.827
  1020. blame_clap_coef = -2.1850
  1021. blame_clap_se = 0.779
  1022. # Adjust coefficients
  1023. adjusted_threat_imp = threat_imp_coef - 0.5
  1024. adjusted_blame_imp = blame_imp_coef - 0.5
  1025. adjusted_threat_clap = threat_clap_coef - 0.5
  1026. adjusted_blame_clap = blame_clap_coef - 0.5
  1027. # Add Video Sharing data
  1028. fig.add_trace(go.Scatter(
  1029. name='Video Sharing',
  1030. x=conditions,
  1031. y=[adjusted_threat_clap, adjusted_blame_clap],
  1032. error_y=dict(
  1033. type='data',
  1034. array=[threat_clap_se, blame_clap_se],
  1035. visible=True,
  1036. thickness=3,
  1037. width=6,
  1038. color=error_color
  1039. ),
  1040. mode='markers',
  1041. marker=dict(
  1042. size=20,
  1043. color=sharing_color,
  1044. symbol='diamond',
  1045. line=dict(width=3, color=error_color)
  1046. ),
  1047. hovertemplate='Video Sharing - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
  1048. ))
  1049. # Add Issue Importance
  1050. fig.add_trace(go.Scatter(
  1051. name='Issue Importance',
  1052. x=conditions,
  1053. y=[adjusted_threat_imp, adjusted_blame_imp],
  1054. error_y=dict(
  1055. type='data',
  1056. array=[threat_imp_se, blame_imp_se],
  1057. visible=True,
  1058. thickness=3,
  1059. width=6,
  1060. color=error_color
  1061. ),
  1062. mode='markers',
  1063. marker=dict(
  1064. size=20,
  1065. color=importance_color,
  1066. symbol='diamond',
  1067. line=dict(width=3, color=error_color)
  1068. ),
  1069. hovertemplate='Issue Importance - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
  1070. ))
  1071. # Calculate significance
  1072. def get_significance_stars(p_value):
  1073. if p_value < 0.001:
  1074. return '***'
  1075. elif p_value < 0.01:
  1076. return '**'
  1077. elif p_value < 0.05:
  1078. return '*'
  1079. else:
  1080. return 'n.s.'
  1081. # Add significance annotations
  1082. y_offset = 1.0
  1083. # Video Sharing
  1084. fig.add_annotation(
  1085. x='Threat',
  1086. y=adjusted_threat_clap + threat_clap_se + y_offset,
  1087. text=get_significance_stars(0.140),
  1088. showarrow=False,
  1089. font=dict(
  1090. size=20 if get_significance_stars(0.140) != 'n.s.' else 14,
  1091. color='black',
  1092. family='Arial'
  1093. ),
  1094. yshift=10
  1095. )
  1096. fig.add_annotation(
  1097. x='Blame',
  1098. y=adjusted_blame_clap + blame_clap_se + y_offset,
  1099. text=get_significance_stars(0.005),
  1100. showarrow=False,
  1101. font=dict(
  1102. size=20 if get_significance_stars(0.005) != 'n.s.' else 14,
  1103. color='black',
  1104. family='Arial'
  1105. ),
  1106. yshift=10
  1107. )
  1108. # Issue Importance
  1109. fig.add_annotation(
  1110. x='Threat',
  1111. y=adjusted_threat_imp + threat_imp_se + y_offset,
  1112. text=get_significance_stars(0.297),
  1113. showarrow=False,
  1114. font=dict(
  1115. size=20 if get_significance_stars(0.297) != 'n.s.' else 14,
  1116. color='black',
  1117. family='Arial'
  1118. ),
  1119. yshift=10
  1120. )
  1121. fig.add_annotation(
  1122. x='Blame',
  1123. y=adjusted_blame_imp + blame_imp_se + y_offset,
  1124. text=get_significance_stars(0.036),
  1125. showarrow=False,
  1126. font=dict(
  1127. size=20 if get_significance_stars(0.036) != 'n.s.' else 14,
  1128. color='black',
  1129. family='Arial'
  1130. ),
  1131. yshift=10
  1132. )
  1133. # Customize layout
  1134. fig.update_layout(
  1135. title=dict(
  1136. text="(b.) Study 2",
  1137. x=0.05,
  1138. xanchor='left',
  1139. font=dict(size=22, family='Arial')
  1140. ),
  1141. xaxis=dict(
  1142. title="",
  1143. tickmode='array',
  1144. tickvals=conditions,
  1145. ticktext=conditions,
  1146. tickfont=dict(size=18, family='Arial'),
  1147. showgrid=False,
  1148. range=[-0.3, 1.3]
  1149. ),
  1150. yaxis=dict(
  1151. title="Simplification frame impact<br>on political behaviour (a.u.)",
  1152. title_font=dict(size=20, family='Arial'),
  1153. tickfont=dict(size=16, family='Arial'),
  1154. gridcolor=grid_color,
  1155. gridwidth=1,
  1156. zeroline=True,
  1157. zerolinewidth=2,
  1158. zerolinecolor='black'
  1159. ),
  1160. showlegend=True,
  1161. legend=dict(
  1162. orientation="h",
  1163. yanchor="bottom",
  1164. y=1.02,
  1165. xanchor="center",
  1166. x=0.5,
  1167. font=dict(size=16, family='Arial'),
  1168. bgcolor='rgba(255,255,255,0.8)'
  1169. ),
  1170. template="plotly_white",
  1171. width=400,
  1172. height=500,
  1173. margin=dict(l=80, r=40, t=80, b=60)
  1174. )
  1175. fig.update_layout(
  1176. plot_bgcolor='white',
  1177. paper_bgcolor='white'
  1178. )
  1179. fig.show()
  1180. # %%
  1181. ##Print all model outputs
  1182. # Create a function to extract model results
  1183. def extract_regression_results(model, model_name):
  1184. """Extract regression coefficients and statistics from a statsmodels model."""
  1185. # Get summary table
  1186. results_summary = model.summary2().tables[1]
  1187. # Reset index to get variable names as column
  1188. results_df = results_summary.reset_index()
  1189. # Rename columns
  1190. results_df.columns = ['Variable', 'coef', 'std_err', 't', 'p_value', 'CI_lower', 'CI_upper']
  1191. # Add significance stars
  1192. def add_stars(p):
  1193. if p < 0.001:
  1194. return '***'
  1195. elif p < 0.01:
  1196. return '**'
  1197. elif p < 0.05:
  1198. return '*'
  1199. elif p < 0.1:
  1200. return '.'
  1201. else:
  1202. return ''
  1203. results_df['sig'] = results_df['p_value'].apply(add_stars)
  1204. # Format coefficient with standard error and significance
  1205. results_df['coef_se'] = results_df.apply(
  1206. lambda x: f"{x['coef']:.3f}{x['sig']}\n({x['std_err']:.3f})", axis=1
  1207. )
  1208. # Add model name
  1209. results_df['Model'] = model_name
  1210. # Add model statistics
  1211. n_obs = model.nobs
  1212. r_squared = model.rsquared
  1213. adj_r_squared = model.rsquared_adj
  1214. f_stat = model.fvalue
  1215. f_pvalue = model.f_pvalue
  1216. stats_row = pd.DataFrame({
  1217. 'Variable': ['N', 'R²', 'Adj. R²', 'F-statistic', 'F p-value'],
  1218. 'coef_se': [f"{n_obs:.0f}", f"{r_squared:.3f}", f"{adj_r_squared:.3f}",
  1219. f"{f_stat:.3f}", f"{f_pvalue:.3f}"],
  1220. 'Model': [model_name] * 5
  1221. })
  1222. return pd.concat([results_df[['Variable', 'coef_se', 'Model']], stats_row], ignore_index=True)
  1223. # Extract results from each model
  1224. video_agreement_results = extract_regression_results(clap_appraisal, "Video agreement")
  1225. issue_importance_results = extract_regression_results(imp_appraisal, "Issue importance")
  1226. # Combine all results
  1227. all_results = pd.concat([pvv_results, gl_results, video_agreement_results, issue_importance_results],
  1228. ignore_index=True)
  1229. # Pivot to wide format for the table
  1230. table_s1_3 = all_results.pivot_table(
  1231. index='Variable',
  1232. columns='Model',
  1233. values='coef_se',
  1234. aggfunc='first'
  1235. )
  1236. # Reorder columns if needed
  1237. desired_order = ['PVV attitudes', 'GL attitudes', 'Video agreement', 'Issue importance']
  1238. table_s1_3 = table_s1_3[desired_order]
  1239. # Sort variables in a logical order (intercept first, then main predictors, then covariates)
  1240. def sort_key(var):
  1241. if var == 'const' or var == 'Intercept':
  1242. return (0, var)
  1243. elif 'frame' in var.lower() or 'video' in var.lower():
  1244. return (1, var)
  1245. elif 'topic' in var.lower():
  1246. return (2, var)
  1247. elif 'gender' in var.lower() or 'age' in var.lower():
  1248. return (3, var)
  1249. elif 'BIG5' in var or 'personality' in var.lower():
  1250. return (4, var)
  1251. elif 'INDEX' in var:
  1252. return (5, var)
  1253. elif 'affect' in var.lower() or 'FEAR' in var or 'ANGER' in var:
  1254. return (6, var)
  1255. elif 'interest' in var.lower() or 'read' in var.lower():
  1256. return (7, var)
  1257. else:
  1258. return (8, var)
  1259. table_s1_3 = table_s1_3.reindex(sorted(table_s1_3.index, key=sort_key))
  1260. # Add descriptive header
  1261. print("=" * 100)
  1262. print("Supplementary Table S1.3")
  1263. print("Full Regression Models from Study 2 with All Covariates Included")
  1264. print("=" * 100)
  1265. print("\nNote: Coefficients shown with standard errors in parentheses.")
  1266. print("Significance levels: *p < .05, **p < .01, ***p < .001\n")
  1267. # Display the table
  1268. with pd.option_context('display.max_rows', None, 'display.width', 1000):
  1269. print(table_s1_3)
  1270. # Save to CSV
  1271. table_s1_3.to_csv('Supplementary_Table_S2_4_Full_Regression_Models.csv')
  1272. print("\nTable saved to 'Supplementary_Table_S2_4_Full_Regression_Models.csv'")
  1273. # %%

Analysis_Script.ipynb at commit 973cb78, no license · at the source

Overview

Authors: Elisa van der Plas1,2,3,4, Lara Todorova2, Karin Heidlmayr2, Giedo Jansen5,4, Martin Rosema4, Alan G Sanfey2
  1. Wellcome Centre for Human Neuroimaging, University College London, London, WC1N 3BG UK
  2. Donders Institute for Brain, Cognition and Behavior, Radboud University Nijmegen, Nijmegen, 6525 EN The Netherlands
  3. Max-Planck Institute for Psycholinguistics, Nijmegen, 6525 XD The Netherlands
  4. Department of Public Administration, University of Twente Enschede, Enschede, 7522 NB The Netherlands
  5. Hugo Sinzheimer Instituut, Amsterdam Institute for Advanced Labour Studies, University of Amsterdam, Amsterdam, The Netherlands
Journal: Scientific reports, volume 16, issue 1, article 14304
Dates: received 17 August 2025; accepted 4 March 2026; published online 20 March 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41598-026-43389-9 · PMID 41862580 · PMCID PMC13144340 · OpenAlex W7139024715
Open access: gold, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: human (organism), systems (subfield)
Methods: Statistics, Preprocessing, Connectivity, fMRI & imaging
Keywords: Societal issues, Neuroscience, Affective polarization, Inter-brain correlations, Normative voting, Psychology
MeSH: Politics*, Prefrontal Cortex*, Voting*, Adult, Brain Mapping, Female, Humans, Magnetic Resonance Imaging, Male, Young Adult (* major topic)
Topic: Psychology of Moral and Emotional Judgment (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Wellcome Trust (203376, 203376/2/16/Z); Tech4People (Inside the Emotional Brain of V oters)
Citations: not cited yet (Europe PMC); 74 references in the paper

Abstract

Online content about societal issues like climate change and immigration are often presented via frames of threat and blame. Here, we investigated how exposure to such framing in the context of an online short video-clip impacts voting behavior and associated brain activity. In a large-scale online study of 1825 Dutch participants, we found that online threat and blame framed video-clips increased agreement with the clips themselves but decreased issue voting, that is, voting in line with the intensity of one’s political beliefs. A follow-up fMRI study with 27 participants replicated this behavioral finding. It also showed that video-clips with threat- or blame-frames, compared to neutral video-clips, were represented more dissimilarly across participants in the dorsolateral prefrontal cortex (DLPFC)—a region involved in narrative understanding. These findings suggest that subtle framing of online political content can influence voter decisions and even the fundamental act of communication itself within a society.

Supplementary Information: The online version contains supplementary material available at 10.1038/s41598-026-43389-9.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 4 matches between paragraphs and lines of code.

elisavanderplas/PoliticalAttitudes_fMRI

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 973cb783661be11970214f98f36958505803bb4d, 11 August 2026
Languages: Jupyter (1), R (1)
Size: 7 files, 2 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, 1 notebook
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: ggplot2 (1 file), Matplotlib (1 file), NumPy (1 file), pandas (1 file), Plotly (1 file), reshape2 (1 file), scikit-learn (1 file), SciPy (1 file), seaborn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
3 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 2 scripts, each with its path and the digest of its content;
  • 4 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability

All data, code and materials are available at: https://Github.com/elisavanderplas/PoliticalAttitudes_fMRI.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 6 authors, 6 keywords, 10 MeSH terms, 2 funders, 57 references.

Cite

This paper

van der Plas, E., Todorova, L., Heidlmayr, K., Jansen, G., Rosema, M., & Sanfey, A. G. (2026). Threat and blame frames in political rhetoric about societal issues lead to neural and political polarization. Scientific reports, 16(1), 14304. https://doi.org/10.1038/s41598-026-43389-9

BibTeX

@article{vanderplas2026threat,
author = {van der Plas, Elisa and Todorova, Lara and Heidlmayr, Karin and Jansen, Giedo and Rosema, Martin and Sanfey, Alan G},
title = {{Threat and blame frames in political rhetoric about societal issues lead to neural and political polarization}},
journal = {Scientific reports},
year = {2026},
month = mar,
volume = {16},
number = {1},
pages = {14304},
publisher = {Nature Publishing Group},
issn = {2045-2322},
doi = {10.1038/s41598-026-43389-9},
url = {https://doi.org/10.1038/s41598-026-43389-9},
pmid = {41862580},
pmcid = {PMC13144340}
}

RIS

TY - JOUR
AU - van der Plas, Elisa
AU - Todorova, Lara
AU - Heidlmayr, Karin
AU - Jansen, Giedo
AU - Rosema, Martin
AU - Sanfey, Alan G
TI - Threat and blame frames in political rhetoric about societal issues lead to neural and political polarization
T2 - Scientific reports
J2 - Sci Rep
PY - 2026
DA - 2026/03/20
VL - 16
IS - 1
SP - 14304
SN - 2045-2322
PB - Nature Publishing Group
DO - 10.1038/s41598-026-43389-9
UR - https://doi.org/10.1038/s41598-026-43389-9
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41598-026-43389-9",
"type": "article-journal",
"title": "Threat and blame frames in political rhetoric about societal issues lead to neural and political polarization",
"container-title": "Scientific reports",
"author": [
{
"family": "van der Plas",
"given": "Elisa"
},
{
"family": "Todorova",
"given": "Lara"
},
{
"family": "Heidlmayr",
"given": "Karin"
},
{
"family": "Jansen",
"given": "Giedo"
},
{
"family": "Rosema",
"given": "Martin"
},
{
"family": "Sanfey",
"given": "Alan G"
}
],
"container-title-short": "Sci Rep",
"volume": "16",
"issue": "1",
"page": "14304",
"DOI": "10.1038/s41598-026-43389-9",
"PMID": "41862580",
"PMCID": "PMC13144340",
"ISSN": "2045-2322",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41598-026-43389-9",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
20
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1073/pnas.2306295121 [code]
Individuals who see the good in the bad engage distinctive default network coordination during post-encoding rest.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: seaborn, scikit-learn, pandas, 3 other tools, systems, 5 references
[2] doi:10.1101/2024.12.03.626542 [code]
Inter-subject correlations and their behavioral associations vary across movies: Implications for generalizability
Journal: bioRxiv (preprint)
In common: statsmodels, ggplot2, seaborn, 5 other tools, 4 references
[3] doi:10.1038/s41467-024-51990-7 [code]
Hyperscanning shows friends explore and strangers converge in conversation.
Journal: Nature communications
In common: Plotly, statsmodels, ggplot2, 6 other tools, 3 references
[4] doi:10.1038/s41598-022-18237-1 [code]
Personality similarity predicts synchronous neural responses in fMRI and EEG data.
Journal: Scientific reports
In common: statsmodels, ggplot2, seaborn, 3 other tools, 5 references
[5] doi:10.1038/s42003-026-09794-6 [code]
The role of dorsal anterior cingulate cortex in dynamic attitude changes in naturalistic settings.
Journal: Communications biology
In common: statsmodels, seaborn, scikit-learn, 4 other tools, systems, 4 references
[6] doi:10.1162/opmi_a_00126 [code]
A Deep Learning Approach to Analyzing Continuous-Time Cognitive Processes.
Journal: Open mind : discoveries in cognitive science
In common: Plotly, statsmodels, ggplot2, 6 other tools, 2 references
[7] doi:10.1162/imag_a_00134 [code]
Broad brain networks support curiosity-motivated incidental learning of naturalistic dynamic stimuli with and without monetary incentives.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: reshape2, ggplot2, scikit-learn, 4 other tools, 3 references
[8] doi:10.1073/pnas.2401317121 [code]
Deciphering the neural responses to a naturalistic persuasive message.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: ggplot2, seaborn, scikit-learn, 4 other tools, 3 references
[9] doi:10.1038/s42003-026-10169-0 [code]
Shared representations in brains and models reveal a two-route cortical organization during scene perception.
Journal: Communications biology
In common: Plotly, statsmodels, seaborn, 5 other tools, 2 references
[10] doi:10.1002/hbm.70164 [code]
Stimulus Selection Influences Prediction of Individual Phenotypes in Naturalistic Conditions.
Journal: Human brain mapping
In common: statsmodels, seaborn, scikit-learn, 4 other tools, 3 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.1038/s41598-026-43389-9.svg)](https://openscicode.org/badge/doi/10.1038/s41598-026-43389-9)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.1038/s41598-026-43389-9.svg
   :target: https://openscicode.org/badge/doi/10.1038/s41598-026-43389-9
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1038/s41598-026-43389-9"><img src="https://openscicode.org/badge/doi/10.1038/s41598-026-43389-9.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.