3  Results

The following section loads the R libraries, and executes the parsing of the raw Linked Open Data (RDF) provided by the Chamber of Deputies.

Code
# --- 1. LIBRARIES ---
library(tidyverse)
library(stringr)
library(lubridate)
library(ggplot2)
library(scales)

# --- 2. PARSING FUNCTION ---
read_camera_rdf <- function(file_path) {
  raw_lines <- read_lines(file_path)
  tibble(raw = raw_lines) |>
    filter(raw != "") |> 
    extract(raw, into = c("soggetto_url", "predicato_url", "oggetto_raw"),
            regex = "^<([^>]+)>\\s+<([^>]+)>\\s+(.+)$", remove = FALSE) |>
    mutate(
      id = str_extract(soggetto_url, "[^/]+$"),
      variable = str_extract(predicato_url, "[^/#]+$"),
      value = oggetto_raw |> str_remove("\\^\\^.*$") |> str_remove(" \\.$") |> str_remove_all("\"") |> str_trim()
    ) |>
    select(id, variable, value)
}

# --- 3. LOAD RAW DATASETS ---

# A. VOTING RECORDS (Votazioni)
df_votazioni <- read_camera_rdf("data/votazione-19.rdf") |>
  distinct(id, variable, .keep_all = TRUE) |>
  pivot_wider(names_from = variable, values_from = value) |>
  mutate(across(c(votanti, favorevoli, contrari, astenuti), as.numeric)) |>
  mutate(date = ymd(date))

# B. SESSION CALENDAR (Sedute)
df_sedute <- read_camera_rdf("data/seduta-19.rdf") |>
  distinct(id, variable, .keep_all = TRUE) |>
  pivot_wider(names_from = variable, values_from = value) |>
  mutate(date = ymd(date)) |> 
  select(id, date, label) |> rename(id_seduta = id)

# C. DEPUTIES REGISTRY (Deputati)
df_deputati <- read_camera_rdf("data/deputato-19.rdf") |>
  distinct(id, variable, .keep_all = TRUE) |>
  pivot_wider(names_from = variable, values_from = value) |>
  mutate(id_numerico = str_extract(id, "\\d{5,}"))

# D. ELECTION RESULTS (Elezioni)
df_elezioni <- read_camera_rdf("data/elezione-19.rdf") |>
  distinct(id, variable, .keep_all = TRUE) |>
  pivot_wider(names_from = variable, values_from = value) |>
  mutate(id_numerico = str_extract(id, "\\d{5,}"))

# E. PARLIAMENTARY GROUPS (Dictionary)
df_groups_dict <- read_camera_rdf("data/gruppoParlamentare-19.rdf") |>
  distinct(id, variable, .keep_all = TRUE) |>
  pivot_wider(names_from = variable, values_from = value) |>
  select(id, label) |>
  mutate(group_code = str_remove(id, "^gr")) |>
  rename(group_name = label)

# --- 4. MASTER DATA PREPARATION (Hybrid Method) ---
# Step 1: Election Source (Primary)
df_source_election <- df_elezioni |>
  filter(!is.na(lista)) |>
  select(id_numerico, raw_party_name = lista) |>
  mutate(source = "Election")

# Step 2: Office Source (Fallback)
df_source_office <- df_deputati |>
  select(id_numerico, rif_ufficioParlamentare) |>
  filter(!is.na(rif_ufficioParlamentare)) |>
  mutate(group_code_extracted = str_extract(rif_ufficioParlamentare, "_(\\d{4})_") |> str_remove_all("_")) |>
  left_join(df_groups_dict, by = c("group_code_extracted" = "group_code")) |>
  filter(!is.na(group_name)) |>
  distinct(id_numerico, .keep_all = TRUE) |>
  select(id_numerico, raw_party_name = group_name) |>
  mutate(source = "Office_Fallback")

# Step 3: Merge & Clean
df_deputati_final <- df_deputati |>
  distinct(id_numerico, .keep_all = TRUE) |>
  select(id, id_numerico) |>
  left_join(df_source_election, by = "id_numerico") |>
  rename(party_election = raw_party_name) |>
  left_join(df_source_office, by = "id_numerico") |>
  rename(party_office = raw_party_name) |>
  mutate(final_party_raw = coalesce(party_election, party_office)) |>
  mutate(
    party_acronym = case_when(
      is.na(final_party_raw) ~ "First-past-the-post Candidates",
      str_detect(final_party_raw, regex("FRATELLI D'ITALIA", ignore_case = TRUE)) ~ "FdI",
      str_detect(final_party_raw, regex("DEMOCRATICO", ignore_case = TRUE)) ~ "PD",
      str_detect(final_party_raw, regex("LEGA", ignore_case = TRUE)) ~ "Lega",
      str_detect(final_party_raw, regex("MOVIMENTO", ignore_case = TRUE)) ~ "M5S",
      str_detect(final_party_raw, regex("FORZA ITALIA", ignore_case = TRUE)) ~ "FI",
      str_detect(final_party_raw, regex("AZIONE|CALENDA|VIVA", ignore_case = TRUE)) ~ "Az-IV",
      str_detect(final_party_raw, regex("VERDI|SINISTRA", ignore_case = TRUE)) ~ "AVS",
      str_detect(final_party_raw, regex("MODERATI|NOI", ignore_case = TRUE)) ~ "NM",
      str_detect(final_party_raw, regex("MAIE", ignore_case = TRUE)) ~ "MAIE",
      str_detect(final_party_raw, regex("SVP|PATT|SÜDTIROLER|MINORANZE", ignore_case = TRUE)) ~ "SVP",
      TRUE ~ "Other"
    )
  )

# --- 5. COLOR PALETTE ---
party_colors <- c(
  "FdI" = "#003366",  # Dark Blue
  "PD" = "#EF3E36",   # Red
  "Lega" = "#008000", # Green
  "M5S" = "#F4C430",  # Yellow
  "FI" = "#2F81C3",   # Light Blue
  "Az-IV" = "#C71585",# Magenta
  "AVS" = "#568203",  # Avocado Green
  "NM" = "#333333",   # Dark Grey
  "SVP" = "#000000",  # Black
  "MAIE" = "#ADD8E6", # Sky Blue
  "First-past-the-post Candidates" = "#CCCCCC"
)

3.1 The Players (Who Represents Us?)

The XIX Legislature is the first to operate under the new constitutional rules approved in the 2020 referendum, which cut the Chamber of Deputies from 630 to 400. Now each vote matters more, and each seat is harder to win, so we think the bar for getting elected should be higher.

First we must understand who they are. Are we looking at the same pool of career politicians who have cycled through Italian institutions for decades, or did the smaller chamber attract representative with diverse background? Who really are these deputies, and how do they compare to what we might expect from a representative assembly?

3.1.1 The Numbers of Power

The snap elections of September 25, 2022 produced a clear result that Italy has not seen in a while: the Center-Right bloc won a comfortable majority, largely thanks to the rise of Fratelli d’Italia (FdI), which went from being a minor party to the largest one in Parliament in a short time.

This outcome was strenghtened by the “Rosatellum” electoral law: out of 400 seats, 148 deputies were elected via the uninominal colleges (First Past the Post method). The governing coalition, composed of Fratelli d’Italia, Lega and Forza Italia (FI), dominated this contest, sweeping 123 of the 148 single-member districts. These candidates were supported by the entire coalition, and are often grouped by their broader bloc, while the remaining were assigned via proportional plurinominal lists.

But does the electoral victory actually translate into control of the Chamber? The numbers suggest Yes.

Code
df_deputati_final |>
  # 1. Count the number of deputies for each party
  count(party_acronym, sort = TRUE) |>
  
  # 2. Initialize the plot
  ggplot(aes(x = reorder(party_acronym, n), y = n, fill = party_acronym)) +
  
  # 3. Create the bars
  geom_col(width = 0.7) +
  
  # 4. Add the exact count labels next to the bars
  geom_text(aes(label = n), hjust = -0.2, fontface = "bold") +
  
  # 5. Flip coordinates to make horizontal bars (better for reading names)
  coord_flip() +
  
  # 6. Apply the custom color palette defined earlier
  scale_fill_manual(values = party_colors) +
  
  # 7. Add extra space on the right (top when flipped) for the labels
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  
  # 8. Clean theme adjustments
  theme_minimal() +
  theme(
    legend.position = "none",
    panel.grid.major.y = element_blank(),
    axis.text.y = element_text(size = 11, face = "bold")
  ) +
  
  # 9. Final Titles and Labels
  labs(
    title = "Chamber Composition",
    subtitle = "Number of Deputies per Political Group",
    x = "",
    y = "Count"
  )

3.1.2 Gender Balance: the Meloni Paradox

There’s a tension in Italian politics right now when it comes to gender representation. The country’s first female Prime Minister came from the Right, not the Left: Giorgia Meloni broke through a “Glass ceiling” for females in the political world that many progressive parties have tried for decades but didn’t succeed.

Is this a true paradox? The data below provides a answer: No.

While the Right produced the national figurehead, Left and Center-Left parties elect and maintain a significantly higher percentage of women. Meloni may be the most visible woman in Italian politics, but her own coalition is still predominantly male.

Code
# 1. Prepare Data with Overall Benchmark
df_demographics_full <- df_deputati_final |>
  left_join(df_deputati |> select(id_numerico, gender, description), by = "id_numerico")

# 2. Create Plot Dataset (Parties + Overall)
df_gender_plot <- bind_rows(

  df_demographics_full |> 
    select(party_acronym, gender),

  df_demographics_full |> 
    mutate(party_acronym = "OVERALL") |> 
    select(party_acronym, gender)
) |>
  count(party_acronym, gender) |> 
  group_by(party_acronym) |> 
  mutate(pct = n / sum(n)) |> 
  ungroup() |>
  mutate(
    plot_pct = ifelse(gender == "male", -pct, pct), 
    label_pct = scales::percent(pct, accuracy = 1)
  )

# 3. Sorting Logic
# A. Calculate Female % for ALL parties (0% for all-male groups)
df_female_pct <- df_gender_plot |>
  filter(gender == "female") |>
  select(party_acronym, female_pct = pct)

# B. Get list of ALL parties (including those with 0% female)
all_parties_list <- unique(df_gender_plot$party_acronym)

# C. Join and fill NAs with 0
df_sort_base <- tibble(party_acronym = all_parties_list) |>
  left_join(df_female_pct, by = "party_acronym") |>
  mutate(female_pct = replace_na(female_pct, 0))

# D. Create the final order vector (sort by the calculated female_pct)
order_gender <- df_sort_base |>
  arrange(female_pct) |> 
  pull(party_acronym)

# Apply factor order
df_gender_plot$party_acronym <- factor(df_gender_plot$party_acronym, levels = order_gender)

# 4. Plotting
ggplot(df_gender_plot, aes(x = party_acronym, y = plot_pct, fill = gender)) +
  geom_col(width = 0.7) +

  geom_text(aes(label = label_pct), 
            hjust = ifelse(df_gender_plot$plot_pct > 0, 1.2, -0.2), 
            color = "white", fontface = "bold", size = 3) +
  
  coord_flip() +

  scale_y_continuous(labels = function(x) scales::percent(abs(x))) +
  scale_fill_manual(values = c("female" = "#D55E00", "male" = "#0072B2")) +
  geom_hline(yintercept = 0, color = "white", linewidth = 1) +
  theme_minimal() + 
  theme(
    legend.position = "top", 
    panel.grid.major.y = element_blank(),
    axis.text.y = element_text(face = "bold", size = 10)
  ) +
  
  labs(
    title = "Gender Balance by Group",
    subtitle = "Diverging Chart: Left (Male) vs Right (Female)",
    x = "", 
    y = "Percentage", 
    fill = ""
  )

3.1.3 Education Gap: Women Need a Higher Bar to Enter Parliament

While women are underrepresented in Parliament, they tend to be more educated than the men. This suggests that women they need stronger credentials just to be considered viable candidates in the first place.

Code
# 1. DATA PREPARATION
df_education_gender <- df_deputati_final |>
  left_join(df_deputati |> select(id_numerico, gender, description), by = "id_numerico") |>
  filter(party_acronym != "Unknown/Misto") |>
  
  # --- FILTERING OUT MINOR PARTIES ---
  filter(!party_acronym %in% c("SVP", "First-past-the-post Candidates")) |>
  
  mutate(
    gender_label = ifelse(gender == "female", "Female", "Male"),
    
    # 1. CLASSIFY EDUCATION (HIERARCHICALLY)
    education_level_raw = case_when(
      str_detect(description, regex("Dottorato|PhD", ignore_case = TRUE)) ~ "PhD / Doctorate",
      str_detect(description, regex("Laurea|Master|Specialistica|Università", ignore_case = TRUE)) ~ "Graduate Degree",
      str_detect(description, regex("Diploma|Maturità|Liceo|Istituto Tecnico", ignore_case = TRUE)) ~ "High School Diploma",
      TRUE ~ "Other / Not Declared"
    ),
    education_level = factor(education_level_raw, 
                             levels = c("Other / Not Declared", "High School Diploma", "Graduate Degree", "PhD / Doctorate"))
  )

# 2. PLOTTING (FACETED GRID - 2 COLONNE)
ggplot(df_education_gender, aes(x = party_acronym, fill = education_level)) +
  geom_bar(position = position_fill(reverse = TRUE), width = 0.7) +
  facet_grid(party_acronym ~ gender_label, scales = "free_y", switch = "y") +
  coord_flip() +
  scale_fill_manual(values = c(
    "Other / Not Declared" = "#D9E3D9",
    "High School Diploma" = "#A8D19F",
    "Graduate Degree" = "#589C4C",
    "PhD / Doctorate" = "#2D5A20"
  )) +
  
  scale_y_continuous(labels = scales::percent) + 
  
  theme_minimal() +
  theme(
    panel.grid.major.y = element_blank(), 
    panel.grid.minor = element_blank(),
    strip.placement = "outside", 
    axis.text.y = element_blank(),
    strip.text.x = element_text(face = "bold", size = 10),
    strip.text.y.left = element_text(face = "bold", size = 10, angle = 0, hjust = 1) 
  ) +
  
  labs(
    title = "Education Gap by Gender & Party (Horizontal)",
    subtitle = "Educational attainment of MPs: Separated by Gender and Party Group",
    x = "",
    y = "% of Group",
    fill = "Education Level"
  )

3.1.4 Professional Background: The Rise of Career Politicians

One question worth asking is whether Parliament actually reflects the people it represents in terms of professional experience. Some European parliaments have many former civil servants but Italy’s does not.

The data highlights a dominance of Lawyers and Professional Politicians, often categorized under “Other” since they lack an alternative career. Meanwhile, people who work in healthcare, education, or the private sector are underrepresented.

Code
# 1. Enrich Data with Biographical Description
df_professions <- df_deputati_final |>
  left_join(df_deputati |> select(id_numerico, description), by = "id_numerico") |>
  filter(!is.na(description)) |>
  
  # 2. Text Mining to Classify Jobs
  mutate(
    job_category = case_when(
      str_detect(description, regex("Avvocato|Legale|Giurista", ignore_case = TRUE)) ~ "Lawyer",
      str_detect(description, regex("Professore|Docente|Insegnante|Ricercatore", ignore_case = TRUE)) ~ "Teacher/Prof",
      str_detect(description, regex("Imprenditore|Azienda|Manager", ignore_case = TRUE)) ~ "Entrepreneur",
      str_detect(description, regex("Giornalista|Pubblicista", ignore_case = TRUE)) ~ "Journalist",
      str_detect(description, regex("Medico|Chirurgo|Sanit", ignore_case = TRUE)) ~ "Doctor/Health",
      str_detect(description, regex("Funzionario|Impiegato|Dirigente", ignore_case = TRUE)) ~ "Civil Servant",
      TRUE ~ "Other/Politician"
    )
  )

# 3. Plotting
df_professions |> 
  count(job_category, sort = TRUE) |> 
  mutate(pct = n/sum(n)) |>
  
  ggplot(aes(x = reorder(job_category, n), y = n)) +
  geom_col(fill = "#4682B4", width = 0.7) +
  geom_text(aes(label = paste0(n, " (", scales::percent(pct, accuracy=1), ")")), 
            hjust = -0.1, fontface = "bold") +
  coord_flip() + 
  scale_y_continuous(expand = expansion(mult = c(0, 0.2))) +
  
  theme_minimal() + 
  theme(
    panel.grid.major.y = element_blank(),
    axis.text.y = element_text(size = 11, face = "bold")
  ) + 
  
  labs(
    title = "Professional Background", 
    subtitle = "Self-reported profession in biographical data",
    x = "", 
    y = "Count"
  )

3.1.5 Political Geography: A Nation of Many Italies

In Italy, politics is as regional as the cuisine. The country is like a mosaic of distinct socio-economic identities, and the voting data reflects this distinction.

While Fratelli d’Italia (FdI) is a national “homogenizer”, other forces are deeply based in specific areas:

  • The North: This is the stronghold of Lega (born as a northern autonomist party) and SVP (representing linguistic minorities in South Tyrol).
  • The Center-North: The traditional “Red Belt” that represents the Democratic Party (PD).
  • The South: The Five Star Movement (M5S) dominates here, where people are often discontent because of the economical disadvantages of this region.
Code
# 1. Enrich Data with Geography
df_geo_analysis <- df_deputati_final |>
  left_join(df_elezioni |> select(id_numerico, coverage), by = "id_numerico") |>
  filter(!is.na(coverage), party_acronym != "Unknown/Misto") |>
  mutate(
    region_raw = str_extract(coverage, "^[A-Z\\s']+") |> str_trim(),
    macro_area = case_when(
      region_raw %in% c("PIEMONTE", "VALLE D'AOSTA", "LOMBARDIA", "TRENTINO", "VENETO", "FRIULI", "LIGURIA", "EMILIA") ~ "North",
      region_raw %in% c("TOSCANA", "UMBRIA", "MARCHE", "LAZIO") ~ "Center",
      region_raw %in% c("ABRUZZO", "MOLISE", "CAMPANIA", "PUGLIA", "BASILICATA", "CALABRIA") ~ "South",
      region_raw %in% c("SICILIA", "SARDEGNA") ~ "Islands",
      TRUE ~ "Abroad" # Fallback
    )
  )

# 2. Set Factor Order for Legend
df_geo_analysis$macro_area <- factor(df_geo_analysis$macro_area, 
                                     levels = c("Abroad", "Islands", "South", "Center", "North"))

# 3. Calculate Sorting Order
north_order <- df_geo_analysis |> 
  count(party_acronym, macro_area) |> 
  group_by(party_acronym) |> 
  mutate(pct = n/sum(n)) |> 
  filter(macro_area == "North") |> 
  arrange(pct) |> 
  pull(party_acronym)
final_order <- c(setdiff(unique(df_geo_analysis$party_acronym), north_order), north_order)

df_geo_analysis$party_acronym <- factor(df_geo_analysis$party_acronym, levels = final_order)

# 4. Plotting
ggplot(df_geo_analysis, aes(x = party_acronym, fill = macro_area)) +
  geom_bar(position = "fill", width = 0.7) +
  coord_flip() +
  scale_y_continuous(labels = scales::percent) +
  scale_fill_manual(values = c(
    "North" = "#004B87",   # Deep Blue
    "Center" = "#C0392B",  # Brick Red
    "South" = "#F39C12",   # Orange/Gold
    "Islands" = "#1ABC9C", # Turquoise
    "Abroad" = "#95A5A6"   # Grey
  )) +

  theme_minimal() + 
  theme(legend.position = "top") +
  
  labs(
    title = "Geographic Distribution", 
    subtitle = "Sorted by presence in the North (Blue segments)", 
    x = "", 
    y = "%", 
    fill = ""
  )

3.2 The “Short Week” Reality

There is a popular joke in Rome: “Parliament opens on Tuesday and closes on Thursday.” The stereotype depicts deputies as commuters who arrive in the capital on Monday night and rush to the airports by Thursday afternoon.

Is this just a myth? The data suggests no.

3.2.1 The Weekly Rhythm

The analysis of voting timestamps confirms the “Short Week” is not a myth. The legislative machine runs at full speed only on Tuesdays and Wednesdays. Mondays and Fridays are phantom days, with low voting activity. The data validates the stereotype: for the average deputy, the parliamentary week effectively lasts 48 to 72 hours.

Code
# 1. Data Preparation: Extracting Time Components
df_activity <- df_votazioni |>
  mutate(date = ymd(date)) |>
  filter(!is.na(date)) |>
  mutate(weekday = wday(date, label = TRUE, abbr = FALSE, week_start = 1))

# 2. Visualization (Styled to match Seasonality Chart)
df_activity |> 
  count(weekday) |>
  ggplot(aes(x = weekday, y = n, group = 1)) +
  geom_area(fill = "#E6E6FA", alpha = 0.5) +
  geom_line(color = "#6A5ACD", size = 1.2) +
  geom_point(color = "#483D8B", size = 3) +

  theme_minimal() + 
  theme(
    panel.grid.major.x = element_blank(), 
    axis.text.x = element_text(size = 11, face = "bold"),
    plot.margin = margin(t = 10, r = 10, b = 10, l = 10) 
  ) +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +

  labs(
    title = "Weekly Work Rhythm", 
    subtitle = "Volume of votes by day of the week: The Tuesday-Wednesday Peak", 
    x = "", 
    y = "Total Votes Cast"
  )

3.2.2 Summer Vacation

Similarly, parliamentary activity is strictly dictated by the calendar of holidays.

August is a vacation month. The Chamber shuts down for summer vacation and voting activity drops drastically. But the month before that, in July, there’s a noticeable spike as deputies rush to do as much as possible before the break. You see a similar pattern around Easter in April, with a smaller peak in March as work ramps up beforehand.

Another pattern is observed in December, which is intense in workload because of the annual Budget Law deadline.

The result is a legislature that operates in bursts: long stretches of low activity followed by frantic voting as a deadline or a holiday approaches.

Code
# 1. Data Preparation
df_votazioni |>
  mutate(date = ymd(date)) |> 
  filter(!is.na(date)) |> 
  mutate(month_label = month(date, label = TRUE, abbr = FALSE)) |>
  
  # 2. Aggregation
  count(month_label) |>
  
  # 3. Visualization
  ggplot(aes(x = month_label, y = n, group = 1)) +
  geom_area(fill = "#E6E6FA", alpha = 0.5) + 
  geom_line(color = "#6A5ACD", size = 1.2) + 
  geom_point(color = "#483D8B", size = 3) +

  theme_minimal() + 
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1)
  ) +

  labs(
    title = "Parliamentary Seasonality", 
    subtitle = "Total votes cast by Month: The July Sprint vs. The August Void", 
    x = "", 
    y = "Volume of Votes"
  )

4 The Geometry of Consensus

Coalition partners are supposed to vote together, and the opposition is supposed to oppose. But how clean is the reality? Do parties actually vote as blocs, or is there more crossover than the political rhetoric suggests?

We wanted to see who breaks ranks, and when. Is Parliament as polarized as it looks on TV, or is there cooperation happening?

4.0.1 The Stability Test

Does the majority on paper translate into actual voting power? To check whether the Center-Right coalition can pass legislation, we tracked approved motions over time, looking specifically at votes where the government got the outcome it wanted.

The results are fairly stable. There’s a slight decline over time, which is expected as government loses slight momentum after the initial period. Notably, the curve eventually dips slightly below the absolute majority threshold (201 seats).

However, this should not be mistaken for a crisis. In the daily reality of the Chamber, absenteeism lowers the bar: the Government does not always need 201 votes to pass ordinary legislation; it simply needs to outnumber the opposition present in the room.

Code
df_votazioni |>
  mutate(date = ymd(date)) |> 
  filter(!is.na(date), votanti > 100) |>
  filter(favorevoli > contrari) |>
  
  ggplot(aes(x = date, y = favorevoli)) +
  geom_point(alpha = 0.1, color = "grey60") +
  geom_smooth(method = "loess", color = "#003366", fill = "#ADD8E6", span = 0.3) +
  geom_hline(yintercept = 201, linetype = "dashed", color = "red", size = 1) +
 
  annotate("text", x = min(ymd(df_votazioni$date), na.rm=TRUE), y = 190, 
           label = "Majority Threshold (201)", color = "red", hjust = 0, vjust = 1, size = 3.5) +
  
  theme_minimal() +
  labs(
    title = "Government Strength Trend", 
    subtitle = "Number of 'Votes in Favor' for passed legislation (Trend vs Threshold)", 
    x = "", 
    y = "Votes in Favor"
  )

4.0.2 Polarization vs. Consensus

While talk shows suggest perpetual chaos, the data reveals a strict order. Legislative activity is far from random and follows a precise Geometry of Power. The data clusters into three logical zones:

  • The Green: A large number of votes pass with almost no opposition. These are mostly technical or routine matters where there’s no real political disagreement.
  • The Blue: The Government passes its laws but the opposition fights (voting against them), but the majority numbers still holds.
  • The Red:This area is “Nays”, which represents Minority proposals rejected by the Majority. It is where conflict happens to block opposition amendments.
Code
# 1. Data Classification
df_vote_analysis <- df_votazioni |> 
  filter(votanti > 50) |>
  mutate(
    outcome = case_when(
      contrari < 20 ~ "Unanimous / Technical",
      
      # Case B: Majority Approved (Govt Wins)
      favorevoli > contrari ~ "Majority Approved",
      
      # Case C: Minority Rejected (Govt Blocks)
      TRUE ~ "Minority Proposal Rejected"
    )
  )

# 2. Visualization
ggplot(df_vote_analysis, aes(x = contrari, y = favorevoli, color = outcome)) +
  geom_point(alpha = 0.6) +
  geom_abline(intercept = 0, slope = 1, linetype = "dashed", color = "grey50") +
  scale_color_manual(values = c(
    "Unanimous / Technical" = "#2E8B57",    # Green: Harmony
    "Majority Approved" = "#4682B4",        # Blue: Govt Constructive Power
    "Minority Proposal Rejected" = "#CD5C5C" # Red: Govt Blocking Power
  )) +
  
  theme_minimal() +

  labs(
    title = "The Geometry of Consensus & Conflict", 
    subtitle = "Analysis of Parliamentary dynamics: Approval, Rejection, and Unanimity", 
    x = "Votes Against (Nays)", 
    y = "Votes In Favor (Ayes)", 
    color = "Vote Dynamics"
  )

4.0.3 Strategic Abstention

When the opposition abstains together it’s a powerful political move: they are effectively not support this law, but also not stop it. This usually happens in two scenarios:

  • Tactical Agreements: The government accepts an opposition’s amendment in exchange for the opposition to step back instead of voting against.
  • Shared Responsibility: On sensitive topics (foreign policy, ethical issues), voting against might be unpopular, so they abstain instead.

The data reveals a distinct “Purple Zone”: votes where Abstentions outnumbered Nays. These are the moments where the conflict was suspended.

Code
# 1. Data Preparation: defining Voting Behavior
df_abstention <- df_votazioni |> 
  filter(!is.na(votanti)) |>
  
  mutate(
    behavior = case_when(
      # Case A: Unanimous / Technical
      contrari < 15 & astenuti < 15 ~ "Unanimous",
      
      # Case B: Strategic Abstention (The "Purple Zone")
      astenuti > contrari & astenuti > 20 ~ "Strategic Abstention",
      
      # Case C: Standard Conflict
      TRUE ~ "Conflict"
    )
  )

# 2. Visualization
ggplot(df_abstention, aes(x = contrari, y = astenuti, color = behavior)) +
  geom_jitter(alpha = 0.6, width = 1, height = 1) +
  geom_abline(intercept = 0, slope = 1, linetype = "dashed", color = "grey50") +
  scale_color_manual(values = c(
    "Conflict" = "#CCCCCC",             # Grey: Background noise (Normal politics)
    "Unanimous" = "#2E8B57",            # Green: Total Agreement
    "Strategic Abstention" = "#800080"  # Purple: The tactical area
  )) +
  
  theme_minimal() + 
  theme(legend.position = "top") +

  labs(
    title = "Strategic Abstention", 
    subtitle = "Purple dots indicate votes where the Opposition preferred Abstaining over voting 'No'", 
    x = "Votes Against (Nays)", 
    y = "Abstentions",
    color = "Vote Dynamics"
  )

4.0.4 Final Votes vs Amendments

Legislative activity is divided into two distinct phases: the discussion of Amendments (modifications to the text) and the Final Vote (passage of the law).

The visual comparison below shows voting patterns in those two phases:

  • Amendments (Grey): This phase perfectly mirrors the general “Geometry of Consensus”.
  • Final Votes (Red): The picture changes drastically. The previous red area of rejected proposals disappears. During the final vote, the Government does not take risks. The Opposition’s attempts are already dead, and almost all laws pass. The dots condense into two specific zones:
    1. Unanimity: Broad agreement (top-left corner).
    2. Majority Force: The coalition lines up to push political laws through (top-center).
Code
# 1. Data Preparation: Distinguish Final Laws from Amendments
df_final <- df_votazioni |> 
  filter(votanti > 50) |>
  
  mutate(
    vote_type = ifelse(
      replace_na(as.numeric(votazioneFinale), 0) == 1, 
      "Final Vote (Law Passage)", 
      "Amendment / Procedural"
    )
  )

# 2. Visualization
ggplot(df_final, aes(x = contrari, y = favorevoli, color = vote_type)) +
  geom_point(alpha = 0.5) + 
  facet_wrap(~ vote_type) +
  scale_color_manual(values = c(
    "Amendment / Procedural" = "#999999",     
    "Final Vote (Law Passage)" = "#D55E00"    
  )) +
  theme_minimal() + 
  theme(legend.position = "none") +

  labs(
    title = "Anatomy of Conflict", 
    subtitle = "Amendments (Chaos) vs Final Laws (Discipline)", 
    x = "Votes Against (Opposition)", 
    y = "Votes in Favor (Majority)"
  )