Threat and blame frames in political rhetoric about societal issues lead to neural and political polarization.
The 4 matches
- [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] § 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] § 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] § 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
- # %%
- # --- Import libraries ---
- import pandas as pd
- import statsmodels.formula.api as smf
- import plotly.express as px
- import plotly.graph_objects as go
- import numpy as np
- import os
- # %%
- # --- Import data ---
- exp1_path = "/Users/elisavanderplas/Desktop/vdPlas_2025_Politics/exp1/behavioural_exp1.csv"
- exp2_path = "/Users/elisavanderplas/Desktop/vdPlas_2025_Politics/exp2/behavioural_exp2.csv"
- exp2_demo_path = "/Users/elisavanderplas/Desktop/vdPlas_2025_Politics/exp2/subjectLog_exp2.csv"
- data_exp1 = pd.read_csv(exp1_path, sep = ';')
- data_exp2 = pd.read_csv(exp2_path, sep = ',')
- demo_exp2 = pd.read_csv(exp2_demo_path)
- # %%
- # --- Prep data Exp 1 ---
- data_exp1['Video'] = pd.Categorical(data_exp1['Video'], categories = [1,2,3,4,5,6,7,8,9], ordered=False)
- 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"])
- data_exp1['topic'] = pd.Categorical(data_exp1['video_2'], categories = [1,2,3], ordered=False)
- data_exp1['topic'] = data_exp1['topic'].cat.rename_categories(["immigration","environment","healthcare"])
- data_exp1['frame'] = pd.Categorical(data_exp1['video_3'], categories = [1,2,3], ordered=False)
- data_exp1['frame'] = data_exp1['frame'].cat.rename_categories(["threat","blame","uncertain"])
- data_exp1['gender'] = data_exp1['geslacht'] - 1 #back to 0 and 1 with 0 is male
- data_exp1['online_use'] = data_exp1['V1_6'] #1 = Uses Social Media (such as Facebook, Twitter or Youtube) to follow the news
- data_exp1['paper_read'] = data_exp1['V2a'] #1 = Reads political news in newspapers
- data_exp1['online_read'] = data_exp1['V2b'] #1 = Reads political news o social media
- data_exp1['interest_politics'] = data_exp1['V3'] #1-4 Likert from "Very interested in political issues" to "Not interested at all"
- data_exp1['CDA'] = data_exp1['V5_1']
- data_exp1['D66'] = data_exp1['V5_2']
- data_exp1['GL'] = data_exp1['V5_3']
- data_exp1['PvdA'] = data_exp1['V5_4']
- data_exp1['PVV'] = data_exp1['V5_5']
- data_exp1['SP'] = data_exp1['V5_6']
- data_exp1['VVD'] = data_exp1['V5_7']
- data_exp1['50P'] = data_exp1['V5_8']
- data_exp1['leftright'] = data_exp1['V_6']
- data_exp1['importance_gezondheidsz'] = data_exp1['V13_1']
- data_exp1['importance_immigr'] = data_exp1['V13_2']
- data_exp1['importance_milieu'] = data_exp1['V13_3']
- data_exp1['video_agreement'] = data_exp1['V10_4']
- data_exp1['video_realiability'] =data_exp1['V10_5']
- data_exp1['video_sharing'] = data_exp1['V10_6']
- # Drop columns that start with 'V'
- cols_to_drop = [col for col in data_exp1.columns if col.startswith('V')]
- df_exp1 = data_exp1.drop(columns=cols_to_drop)
- # Identify columns that start with 'INDEX'
- index_cols = [col for col in df_exp1.columns if col.startswith(('INDEX', 'BIG', 'online', 'geboortejaar', 'paper'))]
- # Clean and replace
- for col in index_cols:
- # Convert 'NULL#' to NaN
- df_exp1[col] = df_exp1[col].replace('NULL#', np.nan)
- # Convert column to numeric (in case it's still object type)
- df_exp1[col] = pd.to_numeric(df_exp1[col], errors='coerce')
- # Compute median (ignoring NaN)
- median_value = df_exp1[col].median()
- # Fill NaN with median and convert to int
- df_exp1[col] = df_exp1[col].fillna(median_value).astype(float)
- # Set 'uncertain' as reference category using pandas Categorical
- df_exp1['frame_reference'] = pd.Categorical(df_exp1['frame'], categories=['uncertain', 'threat', 'blame'])
- # %% [markdown]
- # Study 1
- # %%
- report_text = f"In Study 1 we analyzed the data of {len(df_exp1)} Dutch adults ({np.mean(df_exp1.gender)*100}% female)"
- print(report_text)
- # %%
- # Create descriptive statistics
- desc_stats = df_exp1.describe(include='all').T
- # Select relevant statistics
- desc_stats = desc_stats[['mean', 'std', 'min', 'max']]
- # Calculate mean + 3*std
- desc_stats['mean_plus_3std'] = desc_stats['mean'] + (3 * desc_stats['std'])
- # Round to 2 decimal places for better readability
- desc_stats = desc_stats.round(2)
- # Reorder columns
- desc_stats = desc_stats[['mean', 'std', 'min', 'max', 'mean_plus_3std']]
- print("\n" + "=" * 80)
- print("Descriptive Statistics for Numeric Variables Only:")
- print("=" * 80)
- # Get numeric columns
- numeric_cols = df_exp1.select_dtypes(include=[np.number]).columns
- numeric_stats = df_exp1[numeric_cols].describe().T
- numeric_stats['mean_plus_3std'] = numeric_stats['mean'] + (3 * numeric_stats['std'])
- numeric_stats = numeric_stats[['mean', 'std', 'min', 'max', 'mean_plus_3std']].round(2)
- print(numeric_stats)
- # If you want to save to CSV
- numeric_stats.to_csv('descriptive_statistics_study1.csv')
- # %%
- # --- Model 1 : Negative Arousal ---
- model_neg_arousal = smf.ols(
- '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 ',
- data=df_exp1
- ).fit()
- print(model_neg_arousal.summary())
- model_pos_arousal = smf.ols(
- '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 ',
- data=df_exp1
- ).fit()
- print(model_pos_arousal.summary())
- # %%
- # --- Model 2 : Fear---
- model_fear = smf.ols(
- '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 ',
- data=df_exp1
- ).fit()
- print(model_fear.summary())
- # %%
- # --- Model 3 : Anger ---
- model_anger = smf.ols(
- '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 ',
- data=df_exp1
- ).fit()
- print(model_anger.summary())
- # %%
- import pandas as pd
- import plotly.graph_objects as go
- import numpy as np
- # Extract coefficients from Study 1 anger model
- def extract_study1_coefficients(model):
- """Extract threat and blame coefficients from Study 1 model"""
- coefficients = {}
- for param in model.params.index:
- if 'frame_reference' in param:
- if '[T.threat]' in param:
- coefficients['threat'] = {
- 'coef': model.params[param],
- 'se': model.bse[param],
- 'p_value': model.pvalues[param]
- }
- print(f"Study 1 Anger - Threat: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
- elif '[T.blame]' in param:
- coefficients['blame'] = {
- 'coef': model.params[param],
- 'se': model.bse[param],
- 'p_value': model.pvalues[param]
- }
- print(f"Study 1 Anger - Blame: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
- return coefficients
- # Extract coefficients from your Study 1 models
- study1_anger_coefs = extract_study1_coefficients(model_anger)
- study1_fear_coefs = extract_study1_coefficients(model_fear) #
- # Create the updated plot with Study 1 coefficients
- fig = go.Figure()
- # Define conditions and colors
- conditions = ['Threat', 'Blame']
- fear_color = '#56B4E9' # light blue
- anger_color = '#D55E00' # dark pink / orange-red
- # Add Fear data
- fig.add_trace(go.Scatter(
- name='Fear',
- x=conditions,
- y=[study1_fear_coefs['threat']['coef'], study1_fear_coefs['blame']['coef']],
- error_y=dict(
- type='data',
- array=[study1_fear_coefs['threat']['se'], study1_fear_coefs['blame']['se']],
- visible=True,
- thickness=3,
- width=6,
- color='black'
- ),
- mode='markers',
- marker=dict(
- size=16,
- color=fear_color,
- symbol='diamond',
- line=dict(width=3, color='black')
- ),
- hovertemplate='Fear - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
- ))
- # Add Anger data
- fig.add_trace(go.Scatter(
- name='Anger',
- x=conditions,
- y=[study1_anger_coefs['threat']['coef'], study1_anger_coefs['blame']['coef']],
- error_y=dict(
- type='data',
- array=[study1_anger_coefs['threat']['se'], study1_anger_coefs['blame']['se']],
- visible=True,
- thickness=3,
- width=6,
- color='black' #
- ),
- mode='markers',
- marker=dict(
- size=16,
- color=anger_color,
- symbol='diamond',
- line=dict(width=3, color='black')
- ),
- hovertemplate='Anger - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
- ))
- # Calculate significance stars based on p-values
- def get_significance_stars(p_value):
- if p_value < 0.001:
- return '***'
- elif p_value < 0.01:
- return '**'
- elif p_value < 0.05:
- return '*'
- else:
- return 'n.s.'
- # Add significance annotations
- y_offset_fear = max(study1_fear_coefs['threat']['coef'] + study1_fear_coefs['threat']['se'],
- study1_fear_coefs['blame']['coef'] + study1_fear_coefs['blame']['se']) * 0.2
- y_offset_anger = max(study1_anger_coefs['threat']['coef'] + study1_anger_coefs['threat']['se'],
- study1_anger_coefs['blame']['coef'] + study1_anger_coefs['blame']['se']) * 0.2
- # Fear significance annotations
- fig.add_annotation(
- x='Threat',
- y=study1_fear_coefs['threat']['coef'] + study1_fear_coefs['threat']['se'] + y_offset_fear,
- text=get_significance_stars(study1_fear_coefs['threat']['p_value']),
- showarrow=False,
- font=dict(
- size=22 if get_significance_stars(study1_fear_coefs['threat']['p_value']) != 'n.s.' else 16,
- color='black',
- family='Arial Black'
- ),
- yshift=10
- )
- fig.add_annotation(
- x='Blame',
- y=study1_fear_coefs['blame']['coef'] + study1_fear_coefs['blame']['se'] + y_offset_fear,
- text=get_significance_stars(study1_fear_coefs['blame']['p_value']),
- showarrow=False,
- font=dict(
- size=22 if get_significance_stars(study1_fear_coefs['blame']['p_value']) != 'n.s.' else 16,
- color='black',
- family='Arial Black'
- ),
- yshift=10
- )
- # Anger significance annotations
- fig.add_annotation(
- x='Threat',
- y=study1_anger_coefs['threat']['coef'] + study1_anger_coefs['threat']['se'] + y_offset_anger,
- text=get_significance_stars(study1_anger_coefs['threat']['p_value']),
- showarrow=False,
- font=dict(
- size=22 if get_significance_stars(study1_anger_coefs['threat']['p_value']) != 'n.s.' else 16,
- color='black',
- family='Arial Black'
- ),
- yshift=10
- )
- fig.add_annotation(
- x='Blame',
- y=study1_anger_coefs['blame']['coef'] + study1_anger_coefs['blame']['se'] + y_offset_anger,
- text=get_significance_stars(study1_anger_coefs['blame']['p_value']),
- showarrow=False,
- font=dict(
- size=22 if get_significance_stars(study1_anger_coefs['blame']['p_value']) != 'n.s.' else 16,
- color='black',
- family='Arial Black'
- ),
- yshift=10
- )
- # Customize layout
- fig.update_layout(
- title=dict(
- text="<b>(b.) Study 1</b>",
- x=0.05,
- xanchor='left',
- font=dict(size=20, family='Arial Black')
- ),
- xaxis=dict(
- title="",
- tickmode='array',
- tickvals=conditions,
- ticktext=conditions,
- tickfont=dict(size=16, family='Arial'),
- showgrid=False,
- range=[-0.3, 1.3]
- ),
- yaxis=dict(
- title="Simplification frame impact<br>on negative arousal (a.u.)",
- title_font=dict(size=18, family='Arial'),
- tickfont=dict(size=14, family='Arial'),
- gridcolor='lightgray',
- gridwidth=1,
- zeroline=True,
- zerolinewidth=2,
- zerolinecolor='black'
- ),
- showlegend=True,
- legend=dict(
- orientation="h",
- yanchor="bottom",
- y=1.02,
- xanchor="center",
- x=0.5,
- font=dict(size=14, family='Arial'),
- bgcolor='rgba(255,255,255,0.8)'
- ),
- template="plotly_white",
- width=400,
- height=400,
- margin=dict(l=80, r=40, t=80, b=60)
- )
- # Add a subtle background
- fig.update_layout(
- plot_bgcolor='white',
- paper_bgcolor='white'
- )
- fig.show()
- # %%
- # --- Model 4A : PVV attitudes ---
- model_pvv = smf.ols(
- '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',
- data=df_exp1
- ).fit()
- print(model_pvv.summary())
- # --- Model 4B : GL attitudes ---
- model_gl = smf.ols(
- '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',
- data=df_exp1
- ).fit()
- print(model_gl.summary())
- # %%
- import plotly.graph_objects as go
- from plotly.subplots import make_subplots
- import numpy as np
- # Create subplot figure with 1 row and 2 columns
- fig = make_subplots(rows=1, cols=2,
- subplot_titles=('A) GL Attitudes by Negative Affect and Topic',
- 'B) PVV Attitudes by Negative Affect and Topic'),
- shared_yaxes=False)
- # Define topics and colors
- topics = ['immigration', 'environment', 'healthcare']
- colors = ['red', 'green', 'blue'] # red for immigration, green for environment, blue for healthcare
- # negative affect values for prediction
- neg_affect_range = np.linspace(df_exp1['NEG_AFFECT'].min(), df_exp1['NEG_AFFECT'].max(), 50)
- # --- LEFT PLOT: GL Attitudes by Negative Affect ---
- for i, topic in enumerate(topics):
- # Calculate predicted GL attitudes for this topic across negative affect range
- if topic == 'immigration':
- # Reference category for topic
- topic_effect = 0
- topic_affect_interaction = 0
- elif topic == 'environment':
- topic_effect = -0.6007 # C(topic)[T.environment]
- topic_affect_interaction = 0.2263 # C(topic)[T.environment]:NEG_AFFECT
- else: # healthcare
- topic_effect = -0.3747 # C(topic)[T.healthcare]
- topic_affect_interaction = 0.1127 # C(topic)[T.healthcare]:NEG_AFFECT
- predicted_gl = (11.1089 + # Intercept
- topic_effect +
- neg_affect_range * 0.0335 + # NEG_AFFECT main effect
- neg_affect_range * topic_affect_interaction)
- fig.add_trace(
- go.Scatter(x=neg_affect_range, y=predicted_gl,
- mode='lines',
- name=f'{topic}',
- line=dict(color=colors[i], width=3),
- showlegend=True),
- row=1, col=1
- )
- # --- RIGHT PLOT: PVV Attitudes by Negative Affect ---
- for i, topic in enumerate(topics):
- # Calculate predicted PVV attitudes for this topic across negative affect range
- if topic == 'immigration':
- # Reference category for topic
- topic_effect = 0
- topic_affect_interaction = 0
- elif topic == 'environment':
- topic_effect = 1.4084 # C(topic)[T.environment]
- topic_affect_interaction = -0.2823 # C(topic)[T.environment]:NEG_AFFECT
- else: # healthcare
- topic_effect = 0.9733 # C(topic)[T.healthcare]
- topic_affect_interaction = -0.3822 # C(topic)[T.healthcare]:NEG_AFFECT
- predicted_pvv = (5.2341 + # Intercept
- topic_effect +
- neg_affect_range * 0.3433 + # NEG_AFFECT main effect
- neg_affect_range * topic_affect_interaction)
- fig.add_trace(
- go.Scatter(x=neg_affect_range, y=predicted_pvv,
- mode='lines',
- name=f'PVV - {topic}',
- line=dict(color=colors[i], width=3, dash='dash'),
- showlegend=False),
- row=1, col=2
- )
- # Update layout
- fig.update_layout(
- title_text="Party Attitudes by Negative Affect and Video Topic",
- height=500,
- width=1000,
- template="plotly_white"
- )
- # Update axes labels
- fig.update_xaxes(title_text="Negative Affect", row=1, col=1)
- fig.update_xaxes(title_text="Negative Affect", row=1, col=2)
- fig.update_yaxes(title_text="GL Attitudes", row=1, col=1)
- fig.update_yaxes(title_text="PVV Attitudes", row=1, col=2)
- fig.show()
- # %%
- # --- Model 4A :Agreement ---
- model_agreement = smf.ols(
- '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 ',
- data=df_exp1
- ).fit()
- print(model_agreement.summary())
- # %%
- # Create overall importance as mean of the three indices
- df_exp1['ISSUE_IMPORTANCE'] = df_exp1[['INDEX_CLIMATE', 'INDEX_HEALTH', 'INDEX_CYNICISM']].mean(axis=1)
- # Run the model with overall importance
- model_importance= smf.ols(
- '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',
- data=df_exp1
- ).fit()
- print(model_importance.summary())
- # %%
- ## Model 5 - Sharing
- model_sharing = smf.ols(
- '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 ',
- data=df_exp1
- ).fit()
- print(model_sharing.summary())
- # %%
- import pandas as pd
- import plotly.graph_objects as go
- import numpy as np
- # Extract coefficients from Study 1 anger model
- def extract_study1_coefficients(model):
- """Extract threat and blame coefficients from Study 1 model"""
- coefficients = {}
- for param in model.params.index:
- if 'frame_reference' in param:
- if '[T.threat]' in param:
- coefficients['threat'] = {
- 'coef': model.params[param],
- 'se': model.bse[param],
- 'p_value': model.pvalues[param]
- }
- print(f"Study 1 Anger - Threat: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
- elif '[T.blame]' in param:
- coefficients['blame'] = {
- 'coef': model.params[param],
- 'se': model.bse[param],
- 'p_value': model.pvalues[param]
- }
- print(f"Study 1 Anger - Blame: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
- return coefficients
- # Extract coefficients from your Study 1 models
- sharing_coefs = extract_study1_coefficients(model_sharing)
- importance_coefs = extract_study1_coefficients(model_importance)
- # Create the plot with the specified color scheme and formatting
- fig = go.Figure()
- # Define conditions and colors from your specification
- conditions = ['Threat', 'Blame']
- sharing_color = '#2C4A52' # Dark teal (agreement/upper diamond)
- importance_color = '#E6C87F' # Muted yellow-beige (importance/lower diamond)
- error_color = '#222222' # Black (slightly gray)
- grid_color = '#D3D3D3' # Light gray
- # Adjust coefficients by moving everything 0.5 down on y-axis
- adjusted_sharing_threat = sharing_coefs['threat']['coef'] - 0.5
- adjusted_sharing_blame = sharing_coefs['blame']['coef'] - 0.5
- adjusted_importance_threat = importance_coefs['threat']['coef'] - 0.5
- adjusted_importance_blame = importance_coefs['blame']['coef'] - 0.5
- # Add Video Sharing data with diamond markers (NO connecting lines)
- fig.add_trace(go.Scatter(
- name='Video Sharing',
- x=conditions,
- y=[adjusted_sharing_threat, adjusted_sharing_blame],
- error_y=dict(
- type='data',
- array=[sharing_coefs['threat']['se'], sharing_coefs['blame']['se']],
- visible=True,
- thickness=3,
- width=6,
- color=error_color
- ),
- mode='markers',
- marker=dict(
- size=20,
- color=sharing_color,
- symbol='diamond',
- line=dict(width=3, color=error_color)
- ),
- hovertemplate='Video Sharing - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
- ))
- # Add Issue Importance data with diamond markers
- fig.add_trace(go.Scatter(
- name='Issue Importance',
- x=conditions,
- y=[adjusted_importance_threat, adjusted_importance_blame],
- error_y=dict(
- type='data',
- array=[importance_coefs['threat']['se'], importance_coefs['blame']['se']],
- visible=True,
- thickness=3,
- width=6,
- color=error_color
- ),
- mode='markers',
- marker=dict(
- size=20,
- color=importance_color,
- symbol='diamond',
- line=dict(width=3, color=error_color)
- ),
- hovertemplate='Issue Importance - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
- ))
- # Calculate significance stars based on p-values
- def get_significance_stars(p_value):
- if p_value < 0.001:
- return '***'
- elif p_value < 0.01:
- return '**'
- elif p_value < 0.05:
- return '*'
- else:
- return 'n.s.'
- # Add significance annotations
- y_offset = 0.3
- # Video Sharing significance annotations
- fig.add_annotation(
- x='Threat',
- y=adjusted_sharing_threat + sharing_coefs['threat']['se'] + y_offset,
- text=get_significance_stars(sharing_coefs['threat']['p_value']),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(sharing_coefs['threat']['p_value']) != 'n.s.' else 14, # LESS BOLD sizing
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- fig.add_annotation(
- x='Blame',
- y=adjusted_sharing_blame + sharing_coefs['blame']['se'] + y_offset,
- text=get_significance_stars(sharing_coefs['blame']['p_value']),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(sharing_coefs['blame']['p_value']) != 'n.s.' else 14, # LESS BOLD sizing
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- # Issue Importance significance annotations
- fig.add_annotation(
- x='Threat',
- y=adjusted_importance_threat + importance_coefs['threat']['se'] + y_offset,
- text=get_significance_stars(importance_coefs['threat']['p_value']),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(importance_coefs['threat']['p_value']) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- fig.add_annotation(
- x='Blame',
- y=adjusted_importance_blame + importance_coefs['blame']['se'] + y_offset,
- text=get_significance_stars(importance_coefs['blame']['p_value']),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(importance_coefs['blame']['p_value']) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- # Customize layout
- fig.update_layout(
- title=dict(
- text="(a.) Study 1",
- x=0.05,
- xanchor='left',
- font=dict(size=22, family='Arial')
- ),
- xaxis=dict(
- title="",
- tickmode='array',
- tickvals=conditions,
- ticktext=conditions,
- tickfont=dict(size=18, family='Arial'),
- showgrid=False,
- range=[-0.3, 1.3]
- ),
- yaxis=dict(
- title="Simplification frame impact<br>on political behaviour (a.u.)",
- title_font=dict(size=20, family='Arial'),
- tickfont=dict(size=16, family='Arial'),
- gridcolor=grid_color,
- gridwidth=1,
- zeroline=True,
- zerolinewidth=2,
- zerolinecolor='black'
- ),
- showlegend=True,
- legend=dict(
- orientation="h",
- yanchor="bottom",
- y=1.02,
- xanchor="center",
- x=0.5,
- font=dict(size=16, family='Arial'),
- bgcolor='rgba(255,255,255,0.8)'
- ),
- template="plotly_white",
- width=400,
- height=500,
- margin=dict(l=80, r=40, t=80, b=60)
- )
- fig.update_layout(
- plot_bgcolor='white',
- paper_bgcolor='white'
- )
- fig.show()
- # %%
- ##Print all model outputs
- # Create a function to extract model results
- def extract_regression_results(model, model_name):
- """Extract regression coefficients and statistics from a statsmodels model."""
- # Get summary table
- results_summary = model.summary2().tables[1]
- # Reset index to get variable names as column
- results_df = results_summary.reset_index()
- # Rename columns
- results_df.columns = ['Variable', 'coef', 'std_err', 't', 'p_value', 'CI_lower', 'CI_upper']
- # Add significance stars
- def add_stars(p):
- if p < 0.001:
- return '***'
- elif p < 0.01:
- return '**'
- elif p < 0.05:
- return '*'
- elif p < 0.1:
- return '.'
- else:
- return ''
- results_df['sig'] = results_df['p_value'].apply(add_stars)
- # Format coefficient with standard error and significance
- results_df['coef_se'] = results_df.apply(
- lambda x: f"{x['coef']:.3f}{x['sig']}\n({x['std_err']:.3f})", axis=1
- )
- # Add model name
- results_df['Model'] = model_name
- # Add model statistics
- n_obs = model.nobs
- r_squared = model.rsquared
- adj_r_squared = model.rsquared_adj
- f_stat = model.fvalue
- f_pvalue = model.f_pvalue
- stats_row = pd.DataFrame({
- 'Variable': ['N', 'R²', 'Adj. R²', 'F-statistic', 'F p-value'],
- 'coef_se': [f"{n_obs:.0f}", f"{r_squared:.3f}", f"{adj_r_squared:.3f}",
- f"{f_stat:.3f}", f"{f_pvalue:.3f}"],
- 'Model': [model_name] * 5
- })
- return pd.concat([results_df[['Variable', 'coef_se', 'Model']], stats_row], ignore_index=True)
- # Extract results from each model
- pvv_results = extract_regression_results(model_pvv, "PVV attitudes")
- gl_results = extract_regression_results(model_gl, "GL attitudes")
- video_agreement_results = extract_regression_results(model_agreement, "Video agreement")
- issue_importance_results = extract_regression_results(model_importance, "Issue importance")
- # Combine all results
- all_results = pd.concat([pvv_results, gl_results, video_agreement_results, issue_importance_results],
- ignore_index=True)
- # Pivot to wide format for the table
- table_s1_3 = all_results.pivot_table(
- index='Variable',
- columns='Model',
- values='coef_se',
- aggfunc='first'
- )
- # Reorder columns if needed
- desired_order = ['PVV attitudes', 'GL attitudes', 'Video agreement', 'Issue importance']
- table_s1_3 = table_s1_3[desired_order]
- # Sort variables in a logical order (intercept first, then main predictors, then covariates)
- def sort_key(var):
- if var == 'const' or var == 'Intercept':
- return (0, var)
- elif 'frame' in var.lower() or 'video' in var.lower():
- return (1, var)
- elif 'topic' in var.lower():
- return (2, var)
- elif 'gender' in var.lower() or 'age' in var.lower():
- return (3, var)
- elif 'BIG5' in var or 'personality' in var.lower():
- return (4, var)
- elif 'INDEX' in var:
- return (5, var)
- elif 'affect' in var.lower() or 'FEAR' in var or 'ANGER' in var:
- return (6, var)
- elif 'interest' in var.lower() or 'read' in var.lower():
- return (7, var)
- else:
- return (8, var)
- table_s1_3 = table_s1_3.reindex(sorted(table_s1_3.index, key=sort_key))
- # Add descriptive header
- print("=" * 100)
- print("Supplementary Table S1.3")
- print("Full Regression Models from Study 1 with All Covariates Included")
- print("=" * 100)
- print("\nNote: Coefficients shown with standard errors in parentheses.")
- print("Significance levels: *p < .05, **p < .01, ***p < .001\n")
- # Display the table
- with pd.option_context('display.max_rows', None, 'display.width', 1000):
- print(table_s1_3)
- # Save to CSV
- table_s1_3.to_csv('Supplementary_Table_S1_3_Full_Regression_Models.csv')
- print("\nTable saved to 'Supplementary_Table_S1_3_Full_Regression_Models.csv'")
- # %% [markdown]
- # Study 2
- # %%
- import pandas as pd
- import numpy as np
- import statsmodels.api as sm
- import statsmodels.formula.api as smf
- from scipy import stats
- from statsmodels.stats.anova import anova_lm
- from sklearn.preprocessing import StandardScaler
- # Factors - Convert to categorical with labels
- data_exp2['subj_idx'] = data_exp2['subj_idx'].astype('category')
- # Create factors
- data_exp2['Topic'] = data_exp2['Topic'].replace({1: 'immigration', 2: 'climate change', 3: 'healthcare'}).astype('category')
- data_exp2['Appraisal'] = data_exp2['Appraisal '].replace({1: 'uncertain', 2: 'threat', 3: 'blame'}).astype('category')
- # Merge demographics from demo_exp2
- expected_demo_cols = ['sj_nr', 'gender2', 'age', 'education', 'pol_autho', 'pol_scep',
- 'issue_positions_immigration', 'issue_positions_climate', 'issue_positions_health']
- # Find which expected columns actually exist
- available_demo_cols = [col for col in expected_demo_cols if col in demo_exp2.columns]
- print(f"Available demographic columns for merging: {available_demo_cols}")
- # Merge demographics
- data_exp2 = data_exp2.merge(
- demo_exp2[available_demo_cols],
- left_on='subj_idx',
- right_on='sj_nr',
- how='left'
- )
- print(f"Study 2 data shape after merge: {data_exp2.shape}")
- # Rename columns
- rename_mapping = {}
- if 'gender2' in data_exp2.columns:
- rename_mapping['gender2'] = 'gender'
- if 'education' in data_exp2.columns:
- rename_mapping['education'] = 'edu'
- if 'issue_positions_immigration' in data_exp2.columns:
- rename_mapping['issue_positions_immigration'] = 'PositionImm'
- if 'issue_positions_climate' in data_exp2.columns:
- rename_mapping['issue_positions_climate'] = 'PositionCl'
- if 'issue_positions_health' in data_exp2.columns:
- rename_mapping['issue_positions_health'] = 'PositionHe'
- data_exp2 = data_exp2.rename(columns=rename_mapping)
- # Set 'uncertain' as reference category using pandas Categorical
- data_exp2['frame_reference'] = pd.Categorical(data_exp2['Appraisal'], categories=['uncertain', 'threat', 'blame'])
- # For clarity
- data_exp2['video_sharing'] = data_exp2['clap']
- # %%
- 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))"
- print(report_text)
- # %%
- # Create descriptive statistics
- desc_stats = data_exp2.describe(include='all').T
- # Select relevant statistics
- desc_stats = desc_stats[['mean', 'std', 'min', 'max']]
- # Calculate mean + 3*std
- desc_stats['mean_plus_3std'] = desc_stats['mean'] + (3 * desc_stats['std'])
- # Round to 2 decimal places for better readability
- desc_stats = desc_stats.round(2)
- # Reorder columns
- desc_stats = desc_stats[['mean', 'std', 'min', 'max', 'mean_plus_3std']]
- print("\n" + "=" * 80)
- print("Descriptive Statistics for Numeric Variables Only:")
- print("=" * 80)
- # Get numeric columns
- numeric_cols = data_exp2.select_dtypes(include=[np.number]).columns
- numeric_stats = data_exp2[numeric_cols].describe().T
- numeric_stats['mean_plus_3std'] = numeric_stats['mean'] + (3 * numeric_stats['std'])
- numeric_stats = numeric_stats[['mean', 'std', 'min', 'max', 'mean_plus_3std']].round(2)
- print(numeric_stats)
- # If you want to save to CSV
- numeric_stats.to_csv('descriptive_statistics_study2.csv')
- # %%
- import pandas as pd
- import numpy as np
- import statsmodels.api as sm
- from statsmodels.formula.api import ols
- import matplotlib.pyplot as plt
- import seaborn as sns
- # Negative Affect model
- negative_appraisal = ols('NEG_AFFECT ~ C(frame_reference) + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
- print("=== NEGATIVE AFFECT MODEL ===")
- print(negative_appraisal.summary())
- print("\n")
- # Fear model
- fear_appraisal = ols('FEAR ~ C(frame_reference) + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
- print("=== FEAR MODEL ===")
- print(fear_appraisal.summary())
- print("\n")
- # Anger model
- anger_appraisal = ols('ANGER ~ C(frame_reference) + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
- print("=== ANGER MODEL ===")
- print(anger_appraisal.summary())
- print("\n")
- # PART 2: BEHAVIOURAL EFFECTS
- # Importance model
- imp_appraisal = ols('importance ~ C(frame_reference) + NEG_AFFECT + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
- print("=== IMPORTANCE MODEL ===")
- print(imp_appraisal.summary())
- print("\n")
- # Clap model
- clap_appraisal = ols('video_sharing ~ C(frame_reference) + NEG_AFFECT + age + gender + edu + pol_autho + pol_scep + PositionImm + PositionCl + PositionHe', data=data_exp2).fit()
- print("=== CLAP MODEL ===")
- print(clap_appraisal.summary())
- print("\n")
- # %%
- import pandas as pd
- import plotly.graph_objects as go
- import numpy as np
- # Create the plot
- fig = go.Figure()
- # Extract coefficients from Study 1 anger model
- def extract_study2_coefficients(model):
- """Extract threat and blame coefficients from Study 2 model"""
- coefficients = {}
- for param in model.params.index:
- if 'frame_reference' in param:
- if '[T.threat]' in param:
- coefficients['threat'] = {
- 'coef': model.params[param],
- 'se': model.bse[param],
- 'p_value': model.pvalues[param]
- }
- print(f"Study 2 Anger - Threat: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
- elif '[T.blame]' in param:
- coefficients['blame'] = {
- 'coef': model.params[param],
- 'se': model.bse[param],
- 'p_value': model.pvalues[param]
- }
- print(f"Study 2 Anger - Blame: {model.params[param]:.4f} (SE: {model.bse[param]:.4f}, p: {model.pvalues[param]:.4f})")
- return coefficients
- fear_coefs = extract_study2_coefficients(fear_appraisal)
- anger_coefs = extract_study2_coefficients(anger_appraisal)
- clap_coefs = extract_study2_coefficients(clap_appraisal)
- imp_coefs = extract_study2_coefficients(imp_appraisal)
- # Define conditions and colors
- conditions = ['Threat', 'Blame']
- fear_color = '#56B4E9' # light blue
- anger_color = '#D55E00' # dark pink / orange-red
- error_color = '#222222' # Black error bars
- grid_color = '#D3D3D3' # Light gray
- # Add Fear data
- fig.add_trace(go.Scatter(
- name='Fear',
- x=conditions,
- y=[fear_coefs['threat']['coef'], fear_coefs['blame']['coef']],
- error_y=dict(
- type='data',
- array=[fear_coefs['threat']['se'], fear_coefs['blame']['se']],
- visible=True,
- thickness=3,
- width=6,
- color=error_color
- ),
- mode='markers',
- marker=dict(
- size=20,
- color=fear_color,
- symbol='diamond',
- line=dict(width=3, color=error_color)
- ),
- hovertemplate='Fear - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
- ))
- # Add Anger data
- fig.add_trace(go.Scatter(
- name='Anger',
- x=conditions,
- y=[anger_coefs['threat']['coef'], anger_coefs['blame']['coef']],
- error_y=dict(
- type='data',
- array=[anger_coefs['threat']['se'], anger_coefs['blame']['se']],
- visible=True,
- thickness=3,
- width=6,
- color=error_color
- ),
- mode='markers',
- marker=dict(
- size=20,
- color=anger_color,
- symbol='diamond',
- line=dict(width=3, color=error_color)
- ),
- hovertemplate='Anger - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
- ))
- # Calculate significance
- def get_significance_stars(p_value):
- if p_value < 0.001:
- return '***'
- elif p_value < 0.01:
- return '**'
- elif p_value < 0.05:
- return '*'
- else:
- return 'n.s.'
- # Add significance annotations
- y_offset = 1.5
- # Fear significance
- fig.add_annotation(
- x='Threat',
- y=fear_coefs['threat']['coef'] + fear_coefs['threat']['se'] + y_offset,
- text=get_significance_stars(fear_coefs['threat']['p_value']),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(fear_coefs['threat']['p_value']) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- fig.add_annotation(
- x='Blame',
- y=fear_coefs['blame']['coef'] + fear_coefs['blame']['se'] + y_offset,
- text=get_significance_stars(fear_coefs['blame']['p_value']),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(fear_coefs['blame']['p_value']) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- # Anger significance
- fig.add_annotation(
- x='Threat',
- y=anger_coefs['threat']['coef'] + anger_coefs['threat']['se'] + y_offset,
- text=get_significance_stars(anger_coefs['threat']['p_value']),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(anger_coefs['threat']['p_value']) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- fig.add_annotation(
- x='Blame',
- y=anger_coefs['blame']['coef'] + anger_coefs['blame']['se'] + y_offset,
- text=get_significance_stars(anger_coefs['blame']['p_value']),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(anger_coefs['blame']['p_value']) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- # Customize layout
- fig.update_layout(
- title=dict(
- x=0.05,
- xanchor='left',
- font=dict(size=22, family='Arial')
- ),
- xaxis=dict(
- title="",
- tickmode='array',
- tickvals=conditions,
- ticktext=conditions,
- tickfont=dict(size=18, family='Arial'),
- showgrid=False,
- range=[-0.3, 1.3]
- ),
- yaxis=dict(
- title="Simplification frame impact<br>on negative arousal (a.u.)",
- title_font=dict(size=20, family='Arial'),
- tickfont=dict(size=16, family='Arial'),
- gridcolor=grid_color,
- gridwidth=1,
- zeroline=True,
- zerolinewidth=2,
- zerolinecolor='black'
- ),
- showlegend=True,
- legend=dict(
- orientation="h",
- yanchor="bottom",
- y=1.02,
- xanchor="center",
- x=0.5,
- font=dict(size=16, family='Arial'),
- bgcolor='rgba(255,255,255,0.8)'
- ),
- template="plotly_white",
- width=400,
- height=400,
- margin=dict(l=80, r=40, t=80, b=60)
- )
- fig.update_layout(
- plot_bgcolor='white',
- paper_bgcolor='white'
- )
- fig.show()
- # %%
- import pandas as pd
- import plotly.graph_objects as go
- import numpy as np
- # Create the plot
- fig = go.Figure()
- # Define conditions a
- conditions = ['Threat', 'Blame']
- sharing_color = '#2C4A52' # Dark teal (agreement/upper diamond)
- importance_color = '#E6C87F' # Muted yellow-beige (importance/lower diamond)
- error_color = '#222222' # Black (slightly gray)
- grid_color = '#D3D3D3' # Light gray
- # Extract coefficients
- threat_imp_coef = 2.7773
- threat_imp_se = 2.660
- blame_imp_coef = 5.2652
- blame_imp_se = 2.505
- threat_clap_coef = -1.2202
- threat_clap_se = 0.827
- blame_clap_coef = -2.1850
- blame_clap_se = 0.779
- # Adjust coefficients
- adjusted_threat_imp = threat_imp_coef - 0.5
- adjusted_blame_imp = blame_imp_coef - 0.5
- adjusted_threat_clap = threat_clap_coef - 0.5
- adjusted_blame_clap = blame_clap_coef - 0.5
- # Add Video Sharing data
- fig.add_trace(go.Scatter(
- name='Video Sharing',
- x=conditions,
- y=[adjusted_threat_clap, adjusted_blame_clap],
- error_y=dict(
- type='data',
- array=[threat_clap_se, blame_clap_se],
- visible=True,
- thickness=3,
- width=6,
- color=error_color
- ),
- mode='markers',
- marker=dict(
- size=20,
- color=sharing_color,
- symbol='diamond',
- line=dict(width=3, color=error_color)
- ),
- hovertemplate='Video Sharing - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
- ))
- # Add Issue Importance
- fig.add_trace(go.Scatter(
- name='Issue Importance',
- x=conditions,
- y=[adjusted_threat_imp, adjusted_blame_imp],
- error_y=dict(
- type='data',
- array=[threat_imp_se, blame_imp_se],
- visible=True,
- thickness=3,
- width=6,
- color=error_color
- ),
- mode='markers',
- marker=dict(
- size=20,
- color=importance_color,
- symbol='diamond',
- line=dict(width=3, color=error_color)
- ),
- hovertemplate='Issue Importance - %{x}: %{y:.2f} ± %{error_y.array:.2f}<extra></extra>'
- ))
- # Calculate significance
- def get_significance_stars(p_value):
- if p_value < 0.001:
- return '***'
- elif p_value < 0.01:
- return '**'
- elif p_value < 0.05:
- return '*'
- else:
- return 'n.s.'
- # Add significance annotations
- y_offset = 1.0
- # Video Sharing
- fig.add_annotation(
- x='Threat',
- y=adjusted_threat_clap + threat_clap_se + y_offset,
- text=get_significance_stars(0.140),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(0.140) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- fig.add_annotation(
- x='Blame',
- y=adjusted_blame_clap + blame_clap_se + y_offset,
- text=get_significance_stars(0.005),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(0.005) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- # Issue Importance
- fig.add_annotation(
- x='Threat',
- y=adjusted_threat_imp + threat_imp_se + y_offset,
- text=get_significance_stars(0.297),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(0.297) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- fig.add_annotation(
- x='Blame',
- y=adjusted_blame_imp + blame_imp_se + y_offset,
- text=get_significance_stars(0.036),
- showarrow=False,
- font=dict(
- size=20 if get_significance_stars(0.036) != 'n.s.' else 14,
- color='black',
- family='Arial'
- ),
- yshift=10
- )
- # Customize layout
- fig.update_layout(
- title=dict(
- text="(b.) Study 2",
- x=0.05,
- xanchor='left',
- font=dict(size=22, family='Arial')
- ),
- xaxis=dict(
- title="",
- tickmode='array',
- tickvals=conditions,
- ticktext=conditions,
- tickfont=dict(size=18, family='Arial'),
- showgrid=False,
- range=[-0.3, 1.3]
- ),
- yaxis=dict(
- title="Simplification frame impact<br>on political behaviour (a.u.)",
- title_font=dict(size=20, family='Arial'),
- tickfont=dict(size=16, family='Arial'),
- gridcolor=grid_color,
- gridwidth=1,
- zeroline=True,
- zerolinewidth=2,
- zerolinecolor='black'
- ),
- showlegend=True,
- legend=dict(
- orientation="h",
- yanchor="bottom",
- y=1.02,
- xanchor="center",
- x=0.5,
- font=dict(size=16, family='Arial'),
- bgcolor='rgba(255,255,255,0.8)'
- ),
- template="plotly_white",
- width=400,
- height=500,
- margin=dict(l=80, r=40, t=80, b=60)
- )
- fig.update_layout(
- plot_bgcolor='white',
- paper_bgcolor='white'
- )
- fig.show()
- # %%
- ##Print all model outputs
- # Create a function to extract model results
- def extract_regression_results(model, model_name):
- """Extract regression coefficients and statistics from a statsmodels model."""
- # Get summary table
- results_summary = model.summary2().tables[1]
- # Reset index to get variable names as column
- results_df = results_summary.reset_index()
- # Rename columns
- results_df.columns = ['Variable', 'coef', 'std_err', 't', 'p_value', 'CI_lower', 'CI_upper']
- # Add significance stars
- def add_stars(p):
- if p < 0.001:
- return '***'
- elif p < 0.01:
- return '**'
- elif p < 0.05:
- return '*'
- elif p < 0.1:
- return '.'
- else:
- return ''
- results_df['sig'] = results_df['p_value'].apply(add_stars)
- # Format coefficient with standard error and significance
- results_df['coef_se'] = results_df.apply(
- lambda x: f"{x['coef']:.3f}{x['sig']}\n({x['std_err']:.3f})", axis=1
- )
- # Add model name
- results_df['Model'] = model_name
- # Add model statistics
- n_obs = model.nobs
- r_squared = model.rsquared
- adj_r_squared = model.rsquared_adj
- f_stat = model.fvalue
- f_pvalue = model.f_pvalue
- stats_row = pd.DataFrame({
- 'Variable': ['N', 'R²', 'Adj. R²', 'F-statistic', 'F p-value'],
- 'coef_se': [f"{n_obs:.0f}", f"{r_squared:.3f}", f"{adj_r_squared:.3f}",
- f"{f_stat:.3f}", f"{f_pvalue:.3f}"],
- 'Model': [model_name] * 5
- })
- return pd.concat([results_df[['Variable', 'coef_se', 'Model']], stats_row], ignore_index=True)
- # Extract results from each model
- video_agreement_results = extract_regression_results(clap_appraisal, "Video agreement")
- issue_importance_results = extract_regression_results(imp_appraisal, "Issue importance")
- # Combine all results
- all_results = pd.concat([pvv_results, gl_results, video_agreement_results, issue_importance_results],
- ignore_index=True)
- # Pivot to wide format for the table
- table_s1_3 = all_results.pivot_table(
- index='Variable',
- columns='Model',
- values='coef_se',
- aggfunc='first'
- )
- # Reorder columns if needed
- desired_order = ['PVV attitudes', 'GL attitudes', 'Video agreement', 'Issue importance']
- table_s1_3 = table_s1_3[desired_order]
- # Sort variables in a logical order (intercept first, then main predictors, then covariates)
- def sort_key(var):
- if var == 'const' or var == 'Intercept':
- return (0, var)
- elif 'frame' in var.lower() or 'video' in var.lower():
- return (1, var)
- elif 'topic' in var.lower():
- return (2, var)
- elif 'gender' in var.lower() or 'age' in var.lower():
- return (3, var)
- elif 'BIG5' in var or 'personality' in var.lower():
- return (4, var)
- elif 'INDEX' in var:
- return (5, var)
- elif 'affect' in var.lower() or 'FEAR' in var or 'ANGER' in var:
- return (6, var)
- elif 'interest' in var.lower() or 'read' in var.lower():
- return (7, var)
- else:
- return (8, var)
- table_s1_3 = table_s1_3.reindex(sorted(table_s1_3.index, key=sort_key))
- # Add descriptive header
- print("=" * 100)
- print("Supplementary Table S1.3")
- print("Full Regression Models from Study 2 with All Covariates Included")
- print("=" * 100)
- print("\nNote: Coefficients shown with standard errors in parentheses.")
- print("Significance levels: *p < .05, **p < .01, ***p < .001\n")
- # Display the table
- with pd.option_context('display.max_rows', None, 'display.width', 1000):
- print(table_s1_3)
- # Save to CSV
- table_s1_3.to_csv('Supplementary_Table_S2_4_Full_Regression_Models.csv')
- print("\nTable saved to 'Supplementary_Table_S2_4_Full_Regression_Models.csv'")
- # %%
Analysis_Script.ipynb at commit 973cb78, no license · at the source
Overview
- Wellcome Centre for Human Neuroimaging, University College London, London, WC1N 3BG UK
- Donders Institute for Brain, Cognition and Behavior, Radboud University Nijmegen, Nijmegen, 6525 EN The Netherlands
- Max-Planck Institute for Psycholinguistics, Nijmegen, 6525 XD The Netherlands
- Department of Public Administration, University of Twente Enschede, Enschede, 7522 NB The Netherlands
- Hugo Sinzheimer Instituut, Amsterdam Institute for Advanced Labour Studies, University of Amsterdam, Amsterdam, The Netherlands
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/
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
973cb783661be11970214f98f36958505803bb4d, 11 August 2026Availability: 1 check, the latest on 30 September 2026: the link answers
- 30 September 2026: the link answers
3 files
- Analysis_Script.ipynb, Jupyter, 1,476 lines, 3 matches
- analyses/
fMRI_behaveffects.R , R, 138 lines, 1 match - README.md, Text, 25 lines
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://
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://
BibTeX
@article{vanderplas2026t
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/
url = {https://
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/
VL - 16
IS - 1
SP - 14304
SN - 2045-2322
PB - Nature Publishing Group
DO - 10.1038/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1038/
"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":
"volume": "16",
"issue": "1",
"page": "14304",
"DOI": "10.1038/
"PMID": "41862580",
"PMCID": "PMC13144340",
"ISSN": "2045-2322",
"publisher": "Nature Publishing Group",
"URL": "https://
"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 AmericaIn 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 generalizabilityJournal: 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 communicationsIn 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 reportsIn 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 biologyIn 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 scienceIn 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 AmericaIn 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 biologyIn 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 mappingIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 2 scripts, and 4 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:e4674c176c2c4211…
Add the badge to the code's README
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 -->
[](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:endHTML
<!-- 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.
