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.
This commit is contained in:
+57
-25
@@ -251,21 +251,27 @@ def load_party_axis_scores(db_path: str) -> Dict[str, List[float]]:
|
|||||||
|
|
||||||
|
|
||||||
def _render_party_axis_chart(
|
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:
|
) -> None:
|
||||||
"""Render a 1D horizontal Plotly scatter of party positions on SVD axis `comp_sel`.
|
"""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).
|
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:
|
if not party_scores:
|
||||||
st.caption("_Partijdata niet beschikbaar voor deze as._")
|
st.caption("_Partijdata niet beschikbaar voor deze as._")
|
||||||
return
|
return
|
||||||
|
|
||||||
axis_idx = comp_sel - 1 # 0-based index into the 50-dim vector
|
axis_idx = comp_sel - 1 # 0-based index into the 50-dim vector
|
||||||
|
flip = theme.get("flip", False)
|
||||||
data: list[dict] = []
|
data: list[dict] = []
|
||||||
for party, vec in party_scores.items():
|
for party, vec in party_scores.items():
|
||||||
if axis_idx < len(vec):
|
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:
|
if not data:
|
||||||
st.caption("_Geen partijscores voor deze as._")
|
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]
|
colours = [PARTY_COLOURS.get(p, "#9E9E9E") for p in parties]
|
||||||
hover = [f"{p}: {s:.3f}" for p, s in zip(parties, scores)]
|
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()
|
fig = go.Figure()
|
||||||
# Baseline
|
# Baseline
|
||||||
x_min, x_max = min(scores) * 1.15, max(scores) * 1.15
|
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(
|
fig.add_trace(
|
||||||
go.Scatter(
|
go.Scatter(
|
||||||
x=[x_min, x_max],
|
x=[x_min, x_max],
|
||||||
@@ -297,7 +311,7 @@ def _render_party_axis_chart(
|
|||||||
mode="markers+text",
|
mode="markers+text",
|
||||||
text=parties,
|
text=parties,
|
||||||
textposition="top center",
|
textposition="top center",
|
||||||
marker={"size": 12, "color": colours},
|
marker={"size": 18, "color": colours},
|
||||||
hovertext=hover,
|
hovertext=hover,
|
||||||
hoverinfo="text",
|
hoverinfo="text",
|
||||||
showlegend=False,
|
showlegend=False,
|
||||||
@@ -307,12 +321,13 @@ def _render_party_axis_chart(
|
|||||||
height=160,
|
height=160,
|
||||||
margin={"l": 10, "r": 10, "t": 10, "b": 30},
|
margin={"l": 10, "r": 10, "t": 10, "b": 30},
|
||||||
xaxis={
|
xaxis={
|
||||||
"title": "← Negatieve pool | Positieve pool →",
|
"title": f"← {left_label} | {right_label} →",
|
||||||
"zeroline": True,
|
"zeroline": True,
|
||||||
"zerolinecolor": "#aaaaaa",
|
"zerolinecolor": "#aaaaaa",
|
||||||
},
|
},
|
||||||
yaxis={"visible": False, "range": [-1, 2]},
|
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)
|
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",
|
"positive_pole": "Breed coalitiebeleid: zorg, defensie, multilateralisme, inclusie",
|
||||||
"negative_pole": "Radicale PVV-eis tot onmiddellijke uitzetting migranten",
|
"negative_pole": "Radicale PVV-eis tot onmiddellijke uitzetting migranten",
|
||||||
|
"flip": True,
|
||||||
},
|
},
|
||||||
2: {
|
2: {
|
||||||
"label": "Nationalistisch migratiebeleid versus progressief internationaal solidariteitsdenken",
|
"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",
|
"positive_pole": "Asielbeperking, nationaal belang, restrictief migratiebeleid",
|
||||||
"negative_pole": "Pro-Palestina, progressieve zorgrechten, anti-discriminatie minderheden",
|
"negative_pole": "Pro-Palestina, progressieve zorgrechten, anti-discriminatie minderheden",
|
||||||
|
"flip": False,
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
"label": "Humanitaire solidariteit en inclusie versus nationalistische handhaving en deregulering",
|
"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",
|
"positive_pole": "Internationale solidariteit, inclusie en pragmatische overheidsinterventie",
|
||||||
"negative_pole": "Strikte handhaving, deregulering en nationalistisch eigenbelang boven humanitaire verplichtingen",
|
"negative_pole": "Strikte handhaving, deregulering en nationalistisch eigenbelang boven humanitaire verplichtingen",
|
||||||
|
"flip": True,
|
||||||
},
|
},
|
||||||
4: {
|
4: {
|
||||||
"label": "Publieke voorzieningen beschermen versus liberale marktwerking",
|
"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 "
|
"voorzieningen betaalbaar en toegankelijk te houden, of dat vrije markt en open handel "
|
||||||
"leidend moeten zijn."
|
"leidend moeten zijn."
|
||||||
),
|
),
|
||||||
"positive_pole": "Staatsbescherming van betaalbare publieke voorzieningen voor iedereen",
|
"positive_pole": "Vrije handel, open economie en marktgerichte arbeidsmigratie",
|
||||||
"negative_pole": "Vrije handel, open economie en marktgerichte arbeidsmigratie",
|
"negative_pole": "Staatsbescherming van betaalbare publieke voorzieningen voor iedereen",
|
||||||
|
"flip": False,
|
||||||
},
|
},
|
||||||
5: {
|
5: {
|
||||||
"label": "Christelijk-conservatief sociaal beleid versus seculier progressief",
|
"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, "
|
"consistent vanuit een christelijk-sociale visie stemmen tegenover partijen als D66, "
|
||||||
"GroenLinks-PvdA en SP die een seculier-progressief beleid voorstaan."
|
"GroenLinks-PvdA en SP die een seculier-progressief beleid voorstaan."
|
||||||
),
|
),
|
||||||
"positive_pole": "Christelijk-conservatief: gezin, kerk, leven, traditionele waarden",
|
"positive_pole": "Seculier-progressief: individuele autonomie, progressieve sociale rechten",
|
||||||
"negative_pole": "Seculier-progressief: individuele autonomie, progressieve sociale rechten",
|
"negative_pole": "Christelijk-conservatief: gezin, kerk, leven, traditionele waarden",
|
||||||
|
"flip": True,
|
||||||
},
|
},
|
||||||
6: {
|
6: {
|
||||||
"label": "Christelijk-sociaal beschermingsbeleid versus links-progressieve systeemkritiek",
|
"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",
|
"positive_pole": "Christelijk-sociaal beschermingsbeleid voor pgb, kinderen en geloofsgroepen",
|
||||||
"negative_pole": "Links-progressieve systeemkritiek op zorg, arbeid en internationale solidariteit",
|
"negative_pole": "Links-progressieve systeemkritiek op zorg, arbeid en internationale solidariteit",
|
||||||
|
"flip": False,
|
||||||
},
|
},
|
||||||
7: {
|
7: {
|
||||||
"label": "Liberaal investeren en defensie versus linkse bescherming en controle",
|
"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",
|
"positive_pole": "Gerichte liberale investeringen in sport, wetenschap en defensie",
|
||||||
"negative_pole": "Collectieve bescherming, parlementaire controle en anti-marktwerking in zorg",
|
"negative_pole": "Collectieve bescherming, parlementaire controle en anti-marktwerking in zorg",
|
||||||
|
"flip": False,
|
||||||
},
|
},
|
||||||
8: {
|
8: {
|
||||||
"label": "Confessioneel-sociaal coalitiebeleid versus procedurele blokkade en handhaving",
|
"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",
|
"positive_pole": "Coalitie christelijk-sociaal beleid: defensie, stikstofmaatwerk, bouw en ethiek",
|
||||||
"negative_pole": "Procedurele blokkade coffeeshop, handhavingsdoelstelling en topsportderegulering",
|
"negative_pole": "Procedurele blokkade coffeeshop, handhavingsdoelstelling en topsportderegulering",
|
||||||
|
"flip": False,
|
||||||
},
|
},
|
||||||
9: {
|
9: {
|
||||||
"label": "Brede coalitiemeerderheid versus links marktingrijpen zorg",
|
"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",
|
"positive_pole": "Breed gedragen beleid door centrum-rechts meerderheidsstemmen",
|
||||||
"negative_pole": "Socialistische inkomensregulering en marktingrijpen in de zorg",
|
"negative_pole": "Socialistische inkomensregulering en marktingrijpen in de zorg",
|
||||||
|
"flip": False,
|
||||||
},
|
},
|
||||||
10: {
|
10: {
|
||||||
"label": "Gereguleerde kennismigratie en natuur-landbouwtransitie versus institutionele veiligheid",
|
"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",
|
"positive_pole": "Beperkte kennismigratie, natuur-landbouwtransitie en Gaza-humanitair",
|
||||||
"negative_pole": "Institutionele veiligheidssturing, economisch nationalisme en Woo-beperking",
|
"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 axis chart
|
||||||
party_scores = load_party_axis_scores(db_path)
|
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)
|
# 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]
|
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]
|
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]
|
neg_motions = [m for m in motions if float(m.get("score", 0.0)) < 0]
|
||||||
|
|
||||||
pos_pole = (
|
flip = theme.get("flip", False) if theme else False
|
||||||
theme.get("positive_pole", "Positieve pool") if theme else "Positieve pool"
|
pos_pole = theme.get("positive_pole", "") if theme else ""
|
||||||
)
|
neg_pole = theme.get("negative_pole", "") if theme else ""
|
||||||
neg_pole = (
|
|
||||||
theme.get("negative_pole", "Negatieve pool") if theme else "Negatieve pool"
|
|
||||||
)
|
|
||||||
|
|
||||||
pcol, ncol = st.columns(2)
|
# 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 = "▼", "▲"
|
||||||
|
|
||||||
with pcol:
|
lcol, rcol = st.columns(2)
|
||||||
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")
|
mid = m.get("motion_id")
|
||||||
raw_title = m.get("title") or f"Motie #{mid}"
|
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
|
row = motion_details.get(int(mid)) if mid is not None else None
|
||||||
if row:
|
if row:
|
||||||
try:
|
try:
|
||||||
@@ -1052,12 +1084,12 @@ def build_svd_components_tab(db_path: str) -> None:
|
|||||||
else:
|
else:
|
||||||
st.caption("_Geen metadata beschikbaar_")
|
st.caption("_Geen metadata beschikbaar_")
|
||||||
|
|
||||||
with ncol:
|
with rcol:
|
||||||
st.error(f"▼ **Negatieve pool:** {neg_pole}")
|
st.markdown(f"**{right_pole} →**")
|
||||||
for m in neg_motions:
|
for m in right_motions:
|
||||||
mid = m.get("motion_id")
|
mid = m.get("motion_id")
|
||||||
raw_title = m.get("title") or f"Motie #{mid}"
|
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
|
row = motion_details.get(int(mid)) if mid is not None else None
|
||||||
if row:
|
if row:
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user