@ -100,7 +100,8 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
r . motion_id ,
r . motion_id ,
r . year ,
r . year ,
r . title ,
r . title ,
r . centrist_support ,
r . centrist_support_strict ,
r . center_right_support ,
r . right_support ,
r . right_support ,
r . left_opposition ,
r . left_opposition ,
r . category ,
r . category ,
@ -118,7 +119,8 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
yearly : dict [ int , dict [ str , Any ] ] = { }
yearly : dict [ int , dict [ str , Any ] ] = { }
for year in range ( YEAR_MIN , YEAR_MAX + 1 ) :
for year in range ( YEAR_MIN , YEAR_MAX + 1 ) :
yearly [ year ] = {
yearly [ year ] = {
" centrist_support " : [ ] ,
" centrist_support_strict " : [ ] ,
" center_right_support " : [ ] ,
" right_support " : [ ] ,
" right_support " : [ ] ,
" left_opposition " : [ ] ,
" left_opposition " : [ ] ,
" extremity " : [ ] ,
" extremity " : [ ] ,
@ -128,10 +130,11 @@ def compute_yearly_rw_metrics(con: duckdb.DuckDBPyConnection) -> dict[int, dict]
" motion_ids " : [ ] ,
" motion_ids " : [ ] ,
}
}
for mid , year , title , cs , rs , lo , cat , ext , vr_json , wm in rows :
for mid , year , title , cst , crs , rs , lo , cat , ext , vr_json , wm in rows :
if year is None or year < YEAR_MIN or year > YEAR_MAX :
if year is None or year < YEAR_MIN or year > YEAR_MAX :
continue
continue
yearly [ year ] [ " centrist_support " ] . append ( cs if cs is not None else np . nan )
yearly [ year ] [ " centrist_support_strict " ] . append ( cst if cst is not None else np . nan )
yearly [ year ] [ " center_right_support " ] . append ( crs if crs is not None else np . nan )
yearly [ year ] [ " right_support " ] . append ( rs if rs is not None else np . nan )
yearly [ year ] [ " right_support " ] . append ( rs if rs is not None else np . nan )
yearly [ year ] [ " left_opposition " ] . append ( lo if lo is not None else np . nan )
yearly [ year ] [ " left_opposition " ] . append ( lo if lo is not None else np . nan )
yearly [ year ] [ " extremity " ] . append ( ext if ext is not None else np . nan )
yearly [ year ] [ " extremity " ] . append ( ext if ext is not None else np . nan )
@ -286,7 +289,7 @@ def compute_opposition_metrics(
opp : dict [ int , dict [ str , list ] ] = { }
opp : dict [ int , dict [ str , list ] ] = { }
for year in range ( YEAR_MIN , YEAR_MAX + 1 ) :
for year in range ( YEAR_MIN , YEAR_MAX + 1 ) :
opp [ year ] = {
opp [ year ] = {
" centrist_support " : [ ] ,
" centrist_support_strict " : [ ] ,
" extremity " : [ ] ,
" extremity " : [ ] ,
" passed " : [ ] ,
" passed " : [ ] ,
" n " : 0 ,
" n " : 0 ,
@ -310,7 +313,7 @@ def compute_opposition_metrics(
if submitter_party in coal :
if submitter_party in coal :
continue
continue
opp [ year ] [ " centrist_support " ] . append ( d [ " centrist_support " ] [ idx ] )
opp [ year ] [ " centrist_support_strict " ] . append ( d [ " centrist_support_stric t " ] [ idx ] )
opp [ year ] [ " extremity " ] . append ( d [ " extremity " ] [ idx ] )
opp [ year ] [ " extremity " ] . append ( d [ " extremity " ] [ idx ] )
opp [ year ] [ " passed " ] . append ( d [ " passed " ] [ idx ] )
opp [ year ] [ " passed " ] . append ( d [ " passed " ] [ idx ] )
opp [ year ] [ " n " ] + = 1
opp [ year ] [ " n " ] + = 1
@ -326,14 +329,14 @@ def compute_domain_metrics(
non_mig : dict [ int , dict [ str , list ] ] = { }
non_mig : dict [ int , dict [ str , list ] ] = { }
for year in range ( YEAR_MIN , YEAR_MAX + 1 ) :
for year in range ( YEAR_MIN , YEAR_MAX + 1 ) :
mig [ year ] = { " centrist_support " : [ ] , " extremity " : [ ] , " passed " : [ ] , " n " : 0 }
mig [ year ] = { " centrist_support_strict " : [ ] , " extremity " : [ ] , " passed " : [ ] , " n " : 0 }
non_mig [ year ] = { " centrist_support " : [ ] , " extremity " : [ ] , " passed " : [ ] , " n " : 0 }
non_mig [ year ] = { " centrist_support_strict " : [ ] , " extremity " : [ ] , " passed " : [ ] , " n " : 0 }
for year , d in yearly_raw . items ( ) :
for year , d in yearly_raw . items ( ) :
for idx in range ( len ( d [ " titles " ] ) ) :
for idx in range ( len ( d [ " titles " ] ) ) :
cat = d [ " categories " ] [ idx ]
cat = d [ " categories " ] [ idx ]
target = mig if cat == " asiel/vreemdelingen " else non_mig
target = mig if cat == " asiel/vreemdelingen " else non_mig
target [ year ] [ " centrist_support " ] . append ( d [ " centrist_support " ] [ idx ] )
target [ year ] [ " centrist_support_strict " ] . append ( d [ " centrist_support_stric t " ] [ idx ] )
target [ year ] [ " extremity " ] . append ( d [ " extremity " ] [ idx ] )
target [ year ] [ " extremity " ] . append ( d [ " extremity " ] [ idx ] )
target [ year ] [ " passed " ] . append ( d [ " passed " ] [ idx ] )
target [ year ] [ " passed " ] . append ( d [ " passed " ] [ idx ] )
target [ year ] [ " n " ] + = 1
target [ year ] [ " n " ] + = 1
@ -361,7 +364,7 @@ def compute_extremity_stratified(
period = " pre-2024 " if year < BREAK_YEAR else " post-2024 "
period = " pre-2024 " if year < BREAK_YEAR else " post-2024 "
for idx in range ( len ( d [ " titles " ] ) ) :
for idx in range ( len ( d [ " titles " ] ) ) :
ext = d [ " extremity " ] [ idx ]
ext = d [ " extremity " ] [ idx ]
cs = d [ " centrist_support " ] [ idx ]
cs = d [ " centrist_support_strict " ] [ idx ]
if np . isnan ( ext ) or cs is None or ( isinstance ( cs , float ) and np . isnan ( cs ) ) :
if np . isnan ( ext ) or cs is None or ( isinstance ( cs , float ) and np . isnan ( cs ) ) :
continue
continue
if ext < 2 :
if ext < 2 :
@ -377,17 +380,33 @@ def compute_extremity_stratified(
return pre_post
return pre_post
def compute_left_support_yearly ( con : duckdb . DuckDBPyConnection ) - > dict [ int , dict ] :
""" Query left_support_mp yearly averages from right_wing_motions. """
rows = con . execute ( """
SELECT year , AVG ( left_support_mp ) , COUNT ( * )
FROM right_wing_motions
WHERE classified = TRUE AND left_support_mp IS NOT NULL
GROUP BY year ORDER BY year
""" ).fetchall()
result : dict [ int , dict ] = { }
for year , avg , n in rows :
year = int ( year )
result [ year ] = { " mean_left_support " : avg , " n " : n }
return result
def yearly_summary ( yearly : dict [ int , dict ] ) - > dict [ int , dict ] :
def yearly_summary ( yearly : dict [ int , dict ] ) - > dict [ int , dict ] :
""" Compute mean values from raw lists. """
""" Compute mean values from raw lists. """
summary : dict [ int , dict ] = { }
summary : dict [ int , dict ] = { }
for year , d in yearly . items ( ) :
for year , d in yearly . items ( ) :
s : dict [ str , Any ] = { }
s : dict [ str , Any ] = { }
for key in [ " centrist_support " , " right_support " , " left_opposition " , " extremity " ] :
for key in [ " centrist_support_strict " , " center_right_support " , " right_support " , " left_opposition " , " extremity " ] :
vals = [ v for v in d . get ( key , [ ] ) if not ( isinstance ( v , float ) and np . isnan ( v ) ) ]
vals = [ v for v in d . get ( key , [ ] ) if not ( isinstance ( v , float ) and np . isnan ( v ) ) ]
s [ f " mean_ { key } " ] = np . mean ( vals ) if vals else float ( " nan " )
s [ f " mean_ { key } " ] = np . mean ( vals ) if vals else float ( " nan " )
passes = [ p for p in d . get ( " passed " , [ ] ) if p is not None ]
passes = [ p for p in d . get ( " passed " , [ ] ) if p is not None ]
s [ " pass_rate " ] = sum ( passes ) / len ( passes ) if passes else float ( " nan " )
s [ " pass_rate " ] = sum ( passes ) / len ( passes ) if passes else float ( " nan " )
s [ " n " ] = len ( d . get ( " motion_ids " , d . get ( " centrist_support " , [ ] ) ) )
s [ " n " ] = len ( d . get ( " motion_ids " , d . get ( " centrist_support_strict " , [ ] ) ) )
summary [ year ] = s
summary [ year ] = s
return summary
return summary
@ -486,16 +505,18 @@ def create_figure_1(
colour_non_mig = " #4CAF50 "
colour_non_mig = " #4CAF50 "
colour_baseline = " #9E9E9E "
colour_baseline = " #9E9E9E "
ax . plot ( years_arr , _vals ( yearly_sum , " mean_centrist_support " ) ,
ax . plot ( years_arr , _vals ( yearly_sum , " mean_centrist_support_strict " ) ,
marker = " o " , color = colour_rw , linewidth = 2 , label = " All right-wing " , zorder = 5 )
marker = " o " , color = colour_rw , linewidth = 2 , label = " All right-wing " , zorder = 5 )
ax . plot ( years_arr , _vals ( opp_sum , " mean_centrist_support " ) ,
ax . plot ( years_arr , _vals ( opp_sum , " mean_centrist_support_strict " ) ,
marker = " s " , color = colour_opp , linewidth = 1.5 , linestyle = " -- " , label = " Opposition-only " , zorder = 4 )
marker = " s " , color = colour_opp , linewidth = 1.5 , linestyle = " -- " , label = " Opposition-only " , zorder = 4 )
ax . plot ( years_arr , _vals ( mig_sum , " mean_centrist_support " ) ,
ax . plot ( years_arr , _vals ( mig_sum , " mean_centrist_support_strict " ) ,
marker = " ^ " , color = colour_mig , linewidth = 1.5 , linestyle = " : " , label = " Migration " , zorder = 3 )
marker = " ^ " , color = colour_mig , linewidth = 1.5 , linestyle = " : " , label = " Migration " , zorder = 3 )
ax . plot ( years_arr , _vals ( non_mig_sum , " mean_centrist_support " ) ,
ax . plot ( years_arr , _vals ( non_mig_sum , " mean_centrist_support_strict " ) ,
marker = " v " , color = colour_non_mig , linewidth = 1.5 , linestyle = " -. " , label = " Non-migration " , zorder = 2 )
marker = " v " , color = colour_non_mig , linewidth = 1.5 , linestyle = " -. " , label = " Non-migration " , zorder = 2 )
ax . plot ( years_arr , _vals ( baseline_sum , " mean_centrist_support " ) ,
ax . plot ( years_arr , _vals ( baseline_sum , " mean_centrist_support " ) ,
color = colour_baseline , linewidth = 1 , linestyle = " dashed " , alpha = 0.7 , zorder = 1 , label = " All motions (baseline) " )
color = colour_baseline , linewidth = 1 , linestyle = " dashed " , alpha = 0.7 , zorder = 1 , label = " All motions (baseline) " )
ax . plot ( years_arr , _vals ( yearly_sum , " mean_center_right_support " ) ,
marker = " D " , color = " #FF8F00 " , linewidth = 1.5 , linestyle = " -- " , label = " Center-right (VVD/BBB) " , zorder = 3 )
ax . axvline ( x = BREAK_YEAR - 0.5 , color = " black " , linestyle = " : " , alpha = 0.5 , linewidth = 1 )
ax . axvline ( x = BREAK_YEAR - 0.5 , color = " black " , linestyle = " : " , alpha = 0.5 , linewidth = 1 )
ax . annotate ( " 2024 " , xy = ( BREAK_YEAR - 0.3 , ax . get_ylim ( ) [ 1 ] * 0.95 if ax . get_ylim ( ) [ 1 ] > 0 else 0.95 ) ,
ax . annotate ( " 2024 " , xy = ( BREAK_YEAR - 0.3 , ax . get_ylim ( ) [ 1 ] * 0.95 if ax . get_ylim ( ) [ 1 ] > 0 else 0.95 ) ,
@ -506,8 +527,8 @@ def create_figure_1(
bbox = dict ( boxstyle = " round " , facecolor = " white " , alpha = 0.8 ) )
bbox = dict ( boxstyle = " round " , facecolor = " white " , alpha = 0.8 ) )
ax . set_xlabel ( " Year " )
ax . set_xlabel ( " Year " )
ax . set_ylabel ( " Centrist support (fraction of parties) " )
ax . set_ylabel ( " Centrist support (strict — fraction of parties) " )
ax . set_title ( " Centrist Support for Right-Wing Motions Over Time " , fontweight = " bold " )
ax . set_title ( " Centrist Support (Strict) for Right-Wing Motions Over Time " , fontweight = " bold " )
ax . legend ( loc = " lower right " , fontsize = 8 , ncol = 2 )
ax . legend ( loc = " lower right " , fontsize = 8 , ncol = 2 )
ax . set_ylim ( 0 , 1.05 )
ax . set_ylim ( 0 , 1.05 )
ax . grid ( True , alpha = 0.3 )
ax . grid ( True , alpha = 0.3 )
@ -594,10 +615,10 @@ def create_figure_2(
pre_means_a = np . array ( pre_means )
pre_means_a = np . array ( pre_means )
post_means_a = np . array ( post_means )
post_means_a = np . array ( post_means )
pre_lower = pre_means_a - np . array ( pre_p25s )
pre_lower = np . maximum ( pre_means_a - np . array ( pre_p25s ) , 0 )
pre_upper = np . array ( pre_p75s ) - pre_means_a
pre_upper = np . maximum ( np . array ( pre_p75s ) - pre_means_a , 0 )
post_lower = post_means_a - np . array ( post_p25s )
post_lower = np . maximum ( post_means_a - np . array ( post_p25s ) , 0 )
post_upper = np . array ( post_p75s ) - post_means_a
post_upper = np . maximum ( np . array ( post_p75s ) - post_means_a , 0 )
pre_yerr = np . vstack ( [ pre_lower , pre_upper ] )
pre_yerr = np . vstack ( [ pre_lower , pre_upper ] )
post_yerr = np . vstack ( [ post_lower , post_upper ] )
post_yerr = np . vstack ( [ post_lower , post_upper ] )
@ -616,7 +637,7 @@ def create_figure_2(
f " N= { n } " , ha = " center " , va = " bottom " , fontsize = 8 , fontweight = " bold " )
f " N= { n } " , ha = " center " , va = " bottom " , fontsize = 8 , fontweight = " bold " )
overall_cs_mean = np . average (
overall_cs_mean = np . average (
_vals ( yearly_sum , " mean_centrist_support " ) ,
_vals ( yearly_sum , " mean_centrist_support_strict " ) ,
weights = _vals ( yearly_sum , " n " ) ,
weights = _vals ( yearly_sum , " n " ) ,
)
)
ax2 . axhline ( y = overall_cs_mean , color = " grey " , linestyle = " -- " , alpha = 0.7 , linewidth = 1 ,
ax2 . axhline ( y = overall_cs_mean , color = " grey " , linestyle = " -- " , alpha = 0.7 , linewidth = 1 ,
@ -638,6 +659,47 @@ def create_figure_2(
return path
return path
def create_figure_3 (
left_yearly : dict [ int , dict ] ,
) - > str :
""" Figure 3: Left-party support for right-wing motions (bar chart). """
years = sorted ( left_yearly . keys ( ) )
years_arr = np . array ( years )
means = np . array ( [ left_yearly [ y ] [ " mean_left_support " ] for y in years ] )
ns = np . array ( [ left_yearly [ y ] [ " n " ] for y in years ] )
# Weighted all-years mean
overall_mean = np . average ( means , weights = ns ) if ns . sum ( ) > 0 else 0.0
fig , ax = plt . subplots ( figsize = ( 12 , 6 ) )
bars = ax . bar ( years_arr , means , color = " #1565C0 " , edgecolor = " white " , alpha = 0.9 )
for bar , n in zip ( bars , ns ) :
ax . text ( bar . get_x ( ) + bar . get_width ( ) / 2 , bar . get_height ( ) + 0.005 ,
f " N= { int ( n ) } " , ha = " center " , va = " bottom " , fontsize = 8 )
ax . axhline ( y = overall_mean , color = " #D32F2F " , linestyle = " -- " , alpha = 0.8 , linewidth = 1 ,
label = f " Weighted mean ( { overall_mean : .3f } ) " )
ax . axvline ( x = BREAK_YEAR - 0.5 , color = " black " , linestyle = " : " , alpha = 0.5 , linewidth = 1 )
ax . annotate ( " 2024 " , xy = ( BREAK_YEAR - 0.3 , ax . get_ylim ( ) [ 1 ] * 0.95 ) ,
fontsize = 9 , color = " black " , alpha = 0.7 )
ax . set_xlabel ( " Year " )
ax . set_ylabel ( " Mean left_support_mp " )
ax . set_title ( " Left-wing party support for right-wing motions " , fontweight = " bold " )
ax . legend ( fontsize = 9 )
ax . set_xticks ( years_arr )
ax . set_xticklabels ( [ str ( y ) for y in years ] , rotation = 45 )
ax . grid ( True , alpha = 0.3 , axis = " y " )
plt . tight_layout ( )
path = str ( REPORTS_DIR / " breakpoint_figure_3.png " )
fig . savefig ( path , dpi = 150 , bbox_inches = " tight " )
plt . close ( fig )
logger . info ( " Saved Figure 3 to %s " , path )
return path
def generate_report (
def generate_report (
yearly_sum : dict [ int , dict ] ,
yearly_sum : dict [ int , dict ] ,
opp_sum : dict [ int , dict ] ,
opp_sum : dict [ int , dict ] ,
@ -647,8 +709,10 @@ def generate_report(
ext_stratified : dict [ str , dict [ str , list ] ] ,
ext_stratified : dict [ str , dict [ str , list ] ] ,
yearly_raw : dict [ int , dict ] ,
yearly_raw : dict [ int , dict ] ,
opp_raw : dict [ int , dict ] ,
opp_raw : dict [ int , dict ] ,
left_yearly : dict [ int , dict ] ,
fig1_path : str ,
fig1_path : str ,
fig2_path : str ,
fig2_path : str ,
fig3_path : str ,
audit_sample : list [ dict ] ,
audit_sample : list [ dict ] ,
audit_notes : str = " " ,
audit_notes : str = " " ,
) - > str :
) - > str :
@ -674,8 +738,8 @@ def generate_report(
opp_post_ext = [ ]
opp_post_ext = [ ]
for y , d in yearly_raw . items ( ) :
for y , d in yearly_raw . items ( ) :
for idx in range ( len ( d . get ( " centrist_support " , [ ] ) ) ) :
for idx in range ( len ( d . get ( " centrist_support_strict " , [ ] ) ) ) :
cs = d [ " centrist_support " ] [ idx ]
cs = d [ " centrist_support_strict " ] [ idx ]
ext = d [ " extremity " ] [ idx ]
ext = d [ " extremity " ] [ idx ]
if not ( isinstance ( cs , float ) and np . isnan ( cs ) ) :
if not ( isinstance ( cs , float ) and np . isnan ( cs ) ) :
if y < BREAK_YEAR :
if y < BREAK_YEAR :
@ -689,8 +753,8 @@ def generate_report(
rw_post_ext . append ( ext )
rw_post_ext . append ( ext )
for y , d in opp_raw . items ( ) :
for y , d in opp_raw . items ( ) :
for idx in range ( len ( d . get ( " centrist_support " , [ ] ) ) ) :
for idx in range ( len ( d . get ( " centrist_support_strict " , [ ] ) ) ) :
cs = d [ " centrist_support " ] [ idx ]
cs = d [ " centrist_support_strict " ] [ idx ]
ext = d [ " extremity " ] [ idx ]
ext = d [ " extremity " ] [ idx ]
if not ( isinstance ( cs , float ) and np . isnan ( cs ) ) :
if not ( isinstance ( cs , float ) and np . isnan ( cs ) ) :
if y < BREAK_YEAR :
if y < BREAK_YEAR :
@ -710,11 +774,11 @@ def generate_report(
d_opp_ext = cohens_d ( np . array ( opp_pre_ext ) , np . array ( opp_post_ext ) ) if opp_pre_ext and opp_post_ext else float ( " nan " )
d_opp_ext = cohens_d ( np . array ( opp_pre_ext ) , np . array ( opp_post_ext ) ) if opp_pre_ext and opp_post_ext else float ( " nan " )
# Yearly summary table
# Yearly summary table
yearly_table = " | Year | N (RW) | Centrist Support | Extremity | Right Support | Left Opp. | \n "
yearly_table = " | Year | N (RW) | Centrist Support (Strict) | Extremity | Right Support | Left Opp. | \n "
yearly_table + = " |------|--------|-----------------|-----------|---------------|----------| \n "
yearly_table + = " |------|--------|--------------------------- |-----------|---------------|----------| \n "
for y in years :
for y in years :
n = _val ( yearly_sum , y , " n " )
n = _val ( yearly_sum , y , " n " )
cs = _val ( yearly_sum , y , " mean_centrist_support " )
cs = _val ( yearly_sum , y , " mean_centrist_support_strict " )
ext = _val ( yearly_sum , y , " mean_extremity " )
ext = _val ( yearly_sum , y , " mean_extremity " )
rs = _val ( yearly_sum , y , " mean_right_support " )
rs = _val ( yearly_sum , y , " mean_right_support " )
lo = _val ( yearly_sum , y , " mean_left_opposition " )
lo = _val ( yearly_sum , y , " mean_left_opposition " )
@ -817,8 +881,8 @@ def generate_report(
]
]
for domain_name , domain_sum in [ ( " Migration " , mig_sum ) , ( " Non-migration " , non_mig_sum ) ] :
for domain_name , domain_sum in [ ( " Migration " , mig_sum ) , ( " Non-migration " , non_mig_sum ) ] :
pre_cs = np . nanmean ( [ _val ( domain_sum , y , " mean_centrist_support " ) for y in pre_years ] )
pre_cs = np . nanmean ( [ _val ( domain_sum , y , " mean_centrist_support_strict " ) for y in pre_years ] )
post_cs = np . nanmean ( [ _val ( domain_sum , y , " mean_centrist_support " ) for y in post_years ] )
post_cs = np . nanmean ( [ _val ( domain_sum , y , " mean_centrist_support_strict " ) for y in post_years ] )
lines . append (
lines . append (
f " | { domain_name } | { pre_cs : .3f } | { post_cs : .3f } | { post_cs - pre_cs : +.3f } | "
f " | { domain_name } | { pre_cs : .3f } | { post_cs : .3f } | { post_cs - pre_cs : +.3f } | "
)
)
@ -835,19 +899,55 @@ def generate_report(
" If centrist support rose uniformly across all buckets, the shift is about volume " ,
" If centrist support rose uniformly across all buckets, the shift is about volume " ,
" (more motions) rather than tolerance. If only the 1-2 bucket rose, right-wing " ,
" (more motions) rather than tolerance. If only the 1-2 bucket rose, right-wing " ,
" parties filed milder motions post-2024 and the ' shift ' is illusory. " ,
" parties filed milder motions post-2024 and the ' shift ' is illusory. " ,
]
# Section 6: Left support for right-wing motions
left_years_sorted = sorted ( left_yearly . keys ( ) )
left_pre_years_list = [ y for y in pre_years if y in left_yearly ]
left_post_years_list = [ y for y in post_years if y in left_yearly ]
left_pre_vals = [ left_yearly [ y ] [ " mean_left_support " ] for y in left_pre_years_list ]
left_post_vals = [ left_yearly [ y ] [ " mean_left_support " ] for y in left_post_years_list ]
left_pre_mean = np . mean ( left_pre_vals ) if left_pre_vals else float ( " nan " )
left_post_mean = np . mean ( left_post_vals ) if left_post_vals else float ( " nan " )
left_delta = left_post_mean - left_pre_mean
left_table = " | Year | N | Mean left_support_mp | \n "
left_table + = " |------|---|---------------------| \n "
for y in left_years_sorted :
ls = left_yearly [ y ] [ " mean_left_support " ]
n = left_yearly [ y ] [ " n " ]
left_table + = f " | { y } | { int ( n ) } | { ls : .4f } | \n "
lines + = [
" " ,
" ## 6. Left-wing support for right-wing motions " ,
" " ,
left_table ,
" " ,
f " | Metric | Pre-2024 Mean | Post-2024 Mean | Δ | " ,
f " |--------|--------------|---------------|-----| " ,
f " | Left Support (MP) | { left_pre_mean : .4f } | { left_post_mean : .4f } | { left_delta : +.4f } | " ,
" " ,
" " ,
" ## 6. Manual Extremity Audit " ,
f " **Interpretation:** Left parties moved from { left_pre_mean : .1% } to { left_post_mean : .1% } "
f " support — a { abs ( left_delta ) : .1f } point shift. "
" Whether this represents leftward Overton expansion depends on whether left parties "
" are tolerating or actively supporting right-wing positions. " ,
" " ,
f "  . name } ) " ,
" " ,
" ## 7. Manual Extremity Audit " ,
" " ,
" " ,
audit_notes ,
audit_notes ,
" " ,
" " ,
audit_table ,
audit_table ,
" " ,
" " ,
" ## 7. Limitations " ,
" ## 8 . Limitations " ,
" " ,
" " ,
" - **Small-N time series:** 8 pre-2024 years and at most 3 post-2024 years (2026 is partial). " ,
" - **Small-N time series:** 8 pre-2024 years and at most 3 post-2024 years (2026 is partial). " ,
" Effect sizes are descriptive, not confirmatory. " ,
" Effect sizes are descriptive, not confirmatory. " ,
" - **LLM extremity scores:** Content-based, not independently validated beyond the " ,
" - **LLM extremity scores:** Content-based, not independently validated beyond the " ,
" manual audit above. See §6 for agreement rate and noted biases. " ,
" manual audit above. See §7 for agreement rate and noted biases. " ,
" - **Coalition composition:** Hardcoded per year. 2024 is ambiguous (Rutte IV until July, " ,
" - **Coalition composition:** Hardcoded per year. 2024 is ambiguous (Rutte IV until July, " ,
" Schoof thereafter). Early 2024 motions may be miscoded as Schoof-era. " ,
" Schoof thereafter). Early 2024 motions may be miscoded as Schoof-era. " ,
" - **Submitter party identification:** Parsed from motion title prefixes (e.g., " ,
" - **Submitter party identification:** Parsed from motion title prefixes (e.g., " ,
@ -856,12 +956,13 @@ def generate_report(
" - **Keyword penetration not analyzed:** The right-wing keyword set was derived " ,
" - **Keyword penetration not analyzed:** The right-wing keyword set was derived " ,
" differentially from right-wing motions, making it circular for adoption analysis. " ,
" differentially from right-wing motions, making it circular for adoption analysis. " ,
" " ,
" " ,
" ## 8 . Figures " ,
" ## 9 . Figures " ,
" " ,
" " ,
f "  . name } ) " ,
f "  . name } ) " ,
f "  . name } ) " ,
f "  . name } ) " ,
f "  . name } ) " ,
" " ,
" " ,
" ## 9 . Conclusion " ,
" ## 10 . Conclusion " ,
" " ,
" " ,
" *(Fill in after reviewing all indicators and audit results.)* " ,
" *(Fill in after reviewing all indicators and audit results.)* " ,
]
]
@ -895,6 +996,9 @@ def main() -> int:
logger . info ( " Computing extremity-stratified pass rates... " )
logger . info ( " Computing extremity-stratified pass rates... " )
ext_stratified = compute_extremity_stratified ( yearly_raw )
ext_stratified = compute_extremity_stratified ( yearly_raw )
logger . info ( " Computing left-support yearly averages... " )
left_yearly = compute_left_support_yearly ( con )
con . close ( )
con . close ( )
yearly_sum = yearly_summary ( yearly_raw )
yearly_sum = yearly_summary ( yearly_raw )
@ -909,6 +1013,9 @@ def main() -> int:
logger . info ( " Generating Figure 2... " )
logger . info ( " Generating Figure 2... " )
fig2_path = create_figure_2 ( yearly_sum , opp_sum , mig_sum , non_mig_sum , ext_stratified )
fig2_path = create_figure_2 ( yearly_sum , opp_sum , mig_sum , non_mig_sum , ext_stratified )
logger . info ( " Generating Figure 3... " )
fig3_path = create_figure_3 ( left_yearly )
logger . info ( " Sampling motions for manual audit... " )
logger . info ( " Sampling motions for manual audit... " )
audit_sample = sample_audit ( yearly_raw )
audit_sample = sample_audit ( yearly_raw )
print_audit ( audit_sample )
print_audit ( audit_sample )
@ -931,8 +1038,10 @@ def main() -> int:
ext_stratified = ext_stratified ,
ext_stratified = ext_stratified ,
yearly_raw = yearly_raw ,
yearly_raw = yearly_raw ,
opp_raw = opp_raw ,
opp_raw = opp_raw ,
left_yearly = left_yearly ,
fig1_path = fig1_path ,
fig1_path = fig1_path ,
fig2_path = fig2_path ,
fig2_path = fig2_path ,
fig3_path = fig3_path ,
audit_sample = audit_sample ,
audit_sample = audit_sample ,
audit_notes = audit_notes ,
audit_notes = audit_notes ,
)
)
@ -940,6 +1049,7 @@ def main() -> int:
print ( f " \n Report: { report_path } " )
print ( f " \n Report: { report_path } " )
print ( f " Figure 1: { fig1_path } " )
print ( f " Figure 1: { fig1_path } " )
print ( f " Figure 2: { fig2_path } " )
print ( f " Figure 2: { fig2_path } " )
print ( f " Figure 3: { fig3_path } " )
return 0
return 0