fix: correct axis 4 and 5 pole labels and flip orientation in SVD_THEMES

Axes 4 and 5 had inverted sign conventions relative to actual party votes.
Diagnostic confirmed SP/PvdD scored negative on axis 4 (free trade motions)
and FVD scored negative on axis 5 (secular motions), opposite to their
voting behaviour. Fix: swap positive_pole/negative_pole for both axes and
set correct flip direction so progressive parties appear on the left.
main
Sven Geboers 1 month ago
parent 361cf9fd35
commit 49a1f2f67d
  1. 82
      explorer.py

@ -251,21 +251,27 @@ def load_party_axis_scores(db_path: str) -> Dict[str, List[float]]:
def _render_party_axis_chart(
party_scores: Dict[str, List[float]], comp_sel: int
party_scores: Dict[str, List[float]], comp_sel: int, theme: dict
) -> None:
"""Render a 1D horizontal Plotly scatter of party positions on SVD axis `comp_sel`.
Each party is plotted at its score on a single horizontal axis (y=0).
When theme['flip'] is True the scores are negated so that the progressive/left
side always appears on the left of the chart.
"""
if not party_scores:
st.caption("_Partijdata niet beschikbaar voor deze as._")
return
axis_idx = comp_sel - 1 # 0-based index into the 50-dim vector
flip = theme.get("flip", False)
data: list[dict] = []
for party, vec in party_scores.items():
if axis_idx < len(vec):
data.append({"party": party, "score": vec[axis_idx]})
score = vec[axis_idx]
if flip:
score = -score
data.append({"party": party, "score": score})
if not data:
st.caption("_Geen partijscores voor deze as._")
@ -276,9 +282,17 @@ def _render_party_axis_chart(
colours = [PARTY_COLOURS.get(p, "#9E9E9E") for p in parties]
hover = [f"{p}: {s:.3f}" for p, s in zip(parties, scores)]
# Determine axis labels: left = progressive pole, right = conservative pole
pos_pole = theme.get("positive_pole", "")
neg_pole = theme.get("negative_pole", "")
left_label = pos_pole if flip else neg_pole
right_label = neg_pole if flip else pos_pole
fig = go.Figure()
# Baseline
x_min, x_max = min(scores) * 1.15, max(scores) * 1.15
if x_min == x_max:
x_min, x_max = x_min - 1, x_max + 1
fig.add_trace(
go.Scatter(
x=[x_min, x_max],
@ -297,7 +311,7 @@ def _render_party_axis_chart(
mode="markers+text",
text=parties,
textposition="top center",
marker={"size": 12, "color": colours},
marker={"size": 18, "color": colours},
hovertext=hover,
hoverinfo="text",
showlegend=False,
@ -307,12 +321,13 @@ def _render_party_axis_chart(
height=160,
margin={"l": 10, "r": 10, "t": 10, "b": 30},
xaxis={
"title": "Negatieve pool | Positieve pool",
"title": f"{left_label} | {right_label}",
"zeroline": True,
"zerolinecolor": "#aaaaaa",
},
yaxis={"visible": False, "range": [-1, 2]},
plot_bgcolor="white",
plot_bgcolor="rgba(0,0,0,0)",
paper_bgcolor="rgba(0,0,0,0)",
)
st.plotly_chart(fig, use_container_width=True)
@ -784,6 +799,7 @@ def build_svd_components_tab(db_path: str) -> None:
),
"positive_pole": "Breed coalitiebeleid: zorg, defensie, multilateralisme, inclusie",
"negative_pole": "Radicale PVV-eis tot onmiddellijke uitzetting migranten",
"flip": True,
},
2: {
"label": "Nationalistisch migratiebeleid versus progressief internationaal solidariteitsdenken",
@ -799,6 +815,7 @@ def build_svd_components_tab(db_path: str) -> None:
),
"positive_pole": "Asielbeperking, nationaal belang, restrictief migratiebeleid",
"negative_pole": "Pro-Palestina, progressieve zorgrechten, anti-discriminatie minderheden",
"flip": False,
},
3: {
"label": "Humanitaire solidariteit en inclusie versus nationalistische handhaving en deregulering",
@ -815,6 +832,7 @@ def build_svd_components_tab(db_path: str) -> None:
),
"positive_pole": "Internationale solidariteit, inclusie en pragmatische overheidsinterventie",
"negative_pole": "Strikte handhaving, deregulering en nationalistisch eigenbelang boven humanitaire verplichtingen",
"flip": True,
},
4: {
"label": "Publieke voorzieningen beschermen versus liberale marktwerking",
@ -830,8 +848,9 @@ def build_svd_components_tab(db_path: str) -> None:
"voorzieningen betaalbaar en toegankelijk te houden, of dat vrije markt en open handel "
"leidend moeten zijn."
),
"positive_pole": "Staatsbescherming van betaalbare publieke voorzieningen voor iedereen",
"negative_pole": "Vrije handel, open economie en marktgerichte arbeidsmigratie",
"positive_pole": "Vrije handel, open economie en marktgerichte arbeidsmigratie",
"negative_pole": "Staatsbescherming van betaalbare publieke voorzieningen voor iedereen",
"flip": False,
},
5: {
"label": "Christelijk-conservatief sociaal beleid versus seculier progressief",
@ -844,8 +863,9 @@ def build_svd_components_tab(db_path: str) -> None:
"consistent vanuit een christelijk-sociale visie stemmen tegenover partijen als D66, "
"GroenLinks-PvdA en SP die een seculier-progressief beleid voorstaan."
),
"positive_pole": "Christelijk-conservatief: gezin, kerk, leven, traditionele waarden",
"negative_pole": "Seculier-progressief: individuele autonomie, progressieve sociale rechten",
"positive_pole": "Seculier-progressief: individuele autonomie, progressieve sociale rechten",
"negative_pole": "Christelijk-conservatief: gezin, kerk, leven, traditionele waarden",
"flip": True,
},
6: {
"label": "Christelijk-sociaal beschermingsbeleid versus links-progressieve systeemkritiek",
@ -860,6 +880,7 @@ def build_svd_components_tab(db_path: str) -> None:
),
"positive_pole": "Christelijk-sociaal beschermingsbeleid voor pgb, kinderen en geloofsgroepen",
"negative_pole": "Links-progressieve systeemkritiek op zorg, arbeid en internationale solidariteit",
"flip": False,
},
7: {
"label": "Liberaal investeren en defensie versus linkse bescherming en controle",
@ -873,6 +894,7 @@ def build_svd_components_tab(db_path: str) -> None:
),
"positive_pole": "Gerichte liberale investeringen in sport, wetenschap en defensie",
"negative_pole": "Collectieve bescherming, parlementaire controle en anti-marktwerking in zorg",
"flip": False,
},
8: {
"label": "Confessioneel-sociaal coalitiebeleid versus procedurele blokkade en handhaving",
@ -888,6 +910,7 @@ def build_svd_components_tab(db_path: str) -> None:
),
"positive_pole": "Coalitie christelijk-sociaal beleid: defensie, stikstofmaatwerk, bouw en ethiek",
"negative_pole": "Procedurele blokkade coffeeshop, handhavingsdoelstelling en topsportderegulering",
"flip": False,
},
9: {
"label": "Brede coalitiemeerderheid versus links marktingrijpen zorg",
@ -903,6 +926,7 @@ def build_svd_components_tab(db_path: str) -> None:
),
"positive_pole": "Breed gedragen beleid door centrum-rechts meerderheidsstemmen",
"negative_pole": "Socialistische inkomensregulering en marktingrijpen in de zorg",
"flip": False,
},
10: {
"label": "Gereguleerde kennismigratie en natuur-landbouwtransitie versus institutionele veiligheid",
@ -918,6 +942,7 @@ def build_svd_components_tab(db_path: str) -> None:
),
"positive_pole": "Beperkte kennismigratie, natuur-landbouwtransitie en Gaza-humanitair",
"negative_pole": "Institutionele veiligheidssturing, economisch nationalisme en Woo-beperking",
"flip": True,
},
}
@ -985,7 +1010,7 @@ def build_svd_components_tab(db_path: str) -> None:
# Party axis chart
party_scores = load_party_axis_scores(db_path)
_render_party_axis_chart(party_scores, comp_sel)
_render_party_axis_chart(party_scores, comp_sel, theme)
# Batch-fetch motion details (title, date, policy_area, url, body_text, voting_results)
motion_ids = [m.get("motion_id") for m in motions if m.get("motion_id") is not None]
@ -1021,21 +1046,28 @@ def build_svd_components_tab(db_path: str) -> None:
pos_motions = [m for m in motions if float(m.get("score", 0.0)) >= 0]
neg_motions = [m for m in motions if float(m.get("score", 0.0)) < 0]
pos_pole = (
theme.get("positive_pole", "Positieve pool") if theme else "Positieve pool"
)
neg_pole = (
theme.get("negative_pole", "Negatieve pool") if theme else "Negatieve pool"
)
flip = theme.get("flip", False) if theme else False
pos_pole = theme.get("positive_pole", "") if theme else ""
neg_pole = theme.get("negative_pole", "") if theme else ""
# Determine which pole goes left (progressive) and which goes right
if flip:
left_pole, right_pole = pos_pole, neg_pole
left_motions, right_motions = pos_motions, neg_motions
left_arrow, right_arrow = "", ""
else:
left_pole, right_pole = neg_pole, pos_pole
left_motions, right_motions = neg_motions, pos_motions
left_arrow, right_arrow = "", ""
pcol, ncol = st.columns(2)
lcol, rcol = st.columns(2)
with pcol:
st.success(f"▲ **Positieve pool:** {pos_pole}")
for m in pos_motions:
with lcol:
st.markdown(f"**← {left_pole}**")
for m in left_motions:
mid = m.get("motion_id")
raw_title = m.get("title") or f"Motie #{mid}"
with st.expander(f" {raw_title[:80]}"):
with st.expander(f"{left_arrow} {raw_title[:80]}"):
row = motion_details.get(int(mid)) if mid is not None else None
if row:
try:
@ -1052,12 +1084,12 @@ def build_svd_components_tab(db_path: str) -> None:
else:
st.caption("_Geen metadata beschikbaar_")
with ncol:
st.error(f"▼ **Negatieve pool:** {neg_pole}")
for m in neg_motions:
with rcol:
st.markdown(f"**{right_pole} →**")
for m in right_motions:
mid = m.get("motion_id")
raw_title = m.get("title") or f"Motie #{mid}"
with st.expander(f" {raw_title[:80]}"):
with st.expander(f"{right_arrow} {raw_title[:80]}"):
row = motion_details.get(int(mid)) if mid is not None else None
if row:
try:

Loading…
Cancel
Save