Introducing {ParlAT}: An R Package to access Open Data of the Austrian Parliament
Austria
parliament
r-bloggers
{ParlAT} is an R package that wraps the Austrian Parliament’s Open Data API — making it straightforward to query data on MPs, legislative items, committees, plenary meetings, and more from R.
Author
Roland Schmidt
Published
14 Feb 2026
Modified
20 May 2026
Anyone who has followed this blog will know that I frequently rely on data published by the Austrian Parliament — whether it’s for analyzing speeches, tracing calls to order, or examining the prior experience of MPs. Until now, most of that work required custom scripts to query the Parliament’s API and wrangle the JSON responses into tidy data frames. The {ParlAT} package is an attempt to bundle these steps into a consistent, reusable interface. Overall, though, the package was developed with the intent to make the data of the Parliament more accessible to anyone interested in the Parliament’s affairs, be it researchers, students, journalists etc, and working in R.
At this point, special thanks to Simon Hofer, Open Data evangelist at the Austrian Parliament, who was super helpful when diving into the details of the API. However, please note that neither the package nor its author is affiliated with the Austrian Parliament.
The package is still in development, but most of the functionality (and quite a bit more) is in place. Below I showcase only some examples.
For the full function reference and a more detailed introductory vignette, see the documentation site.
1 What data is available?
The Austrian Parliament publishes 25 open datasets — with some exceptions — under a CC BY 4.0 license. These cover a broad range of parliamentary activity: MP records going back to 1918, committee compositions and memberships, plenary meeting protocols, citizen initiatives (Bürgerinitiativen), government bills (Regierungsvorlagen), written questions (Schriftliche Anfragen), EU submissions, and more.
{ParlAT} mirrors the API’s structure. Most datasets map directly to a dedicated function; some, like the many types of parliamentary items (Verhandlungsgegenstände), are accessed through get_items() with different item parameters. Here’s a condensed overview of the main functions:
Function
Data
get_mps()
MPs since 1918, by period or date
get_mps_current()
Current composition of NR or BR
get_mps_details()
Individual MP speeches, activities, committee work
get_mandates()
All mandates of one or more individuals
get_items()
Parliamentary items: bills, motions, questions, etc.
get_committees()
Committees, optionally with member lists
get_plenary_meetings()
Plenary meeting metadata
get_transcripts()
Stenographic protocols with PDF export
get_events()
Parliamentary events and calendar
get_participation()
Public participation / consultation data
2 MPs: Current composition
A natural starting point is the composition of Parliament itself. get_mps_current() retrieves the members currently serving in the National Council (Nationalrat) or the Federal Council (Bundesrat):
For historical queries, get_mps() returns MPs active during a given legislative period or on a specific date. The function accepts both numeric and Roman numeral period identifiers (e.g., 27 or "XXVII"), as well as special codes like "PN" (Provisorische Nationalversammlung) and "KN" (Konstituierende Nationalversammlung).
# All MPs of the National Council during the 22nd legislative periodmps_legis22 <-get_mps(legis_period =22, institution ="NR")## {"ATTR_JSON.mandate_detail.gremium_name":["Nationalrat"],"ATTR_JSON.mandate_detail.gp_text_full_short":["20.12.2002 - 29.10.2006: XXII. GP"]} ## https://www.parlament.gv.at/recherchieren/personen/parlamentarierinnen-ab-1848/parlamentarierinnen-ab-1918?PERSON_409ATTR_JSON.mandate_detail.gremium_name=Nationalrat&PERSON_409ATTR_JSON.mandate_detail.gp_text_full_short=20.12.2002%20-%2029.10.2006:%20XXII.%20GP## [1] 209
Each row corresponds to one MP per legislative period. A nested mp_details column captures mandate-level details — party affiliation, electoral list, position — which can change within a single period. This is where things get interesting: unnesting mp_details reveals party switches, changes in parliamentary group, or mandate interruptions within a period.
3.2 By date
When a specific date is more appropriate than a legislative period — for instance, when working with the Federal Council, which doesn’t follow legislative periods in the same way — get_mps() accepts a date parameter:
MPs by date (BR)
# Members of the Federal Council on 30 October 2013df_mps_br <-get_mps(date ="30.10.2013", institution ="BR")nrow(df_mps_br)## [1] 60
Next to the data retrieval functions, there are some auxiliary functions which hopefully come handy. E.g. to find the relevant dates for a specific legislative period, get_legis_periods() provides the start and end dates. If i have a date, it returns the pertaining legislative period.
get_mps() supports a number of additional filters. These can be combined:
Filtering MPs by gender, party, state
# Female SPÖ MPs in the current legislative periodfemale_spoe <-get_mps(gender ="female",party ="SPÖ",legis_period =28,institution ="NR")## {"GESCHL_CODE":["W"],"ATTR_JSON.mandate_detail.gremium_name":["Nationalrat"],"ATTR_JSON.mandate_detail.gp_text_full_short":["ab 24.10.2024: XXVIII. Gesetzgebungsperiode"],"ATTR_JSON.mandate_detail.wahlpartei_full_txt":["Sozialdemokratische Partei Österreichs (SPÖ)","Sozialistische Partei Österreichs (SPÖ)"]} ## https://www.parlament.gv.at/recherchieren/personen/parlamentarierinnen-ab-1848/parlamentarierinnen-ab-1918?PERSON_409GESCHL_CODE=W&PERSON_409ATTR_JSON.mandate_detail.gremium_name=Nationalrat&PERSON_409ATTR_JSON.mandate_detail.gp_text_full_short=ab%2024.10.2024:%20XXVIII.%20Gesetzgebungsperiode&PERSON_409ATTR_JSON.mandate_detail.wahlpartei_full_txt=Sozialdemokratische%20Partei%20%C3%96sterreichs%20(SP%C3%96)&PERSON_409ATTR_JSON.mandate_detail.wahlpartei_full_txt=Sozialistische%20Partei%20%C3%96sterreichs%20(SP%C3%96)## [1] 19female_spoe## # A tibble: 19 × 6## # Groups: pad_intern [19]## pad_intern legis_period name gender link mp_details## <int> <chr> <chr> <chr> <chr> <list> ## 1 145 XXVIII Doris Bures female /per… <tibble> ## 2 2189 XXVIII Elisabeth Feichtinger, BEd B… female /per… <tibble> ## 3 2309 XXVIII Eva-Maria Holzleitner, BSc female /per… <tibble> ## 4 2334 XXVIII Mag.<sup>a</sup> Verena Nuss… female /per… <tibble> ## 5 2337 XXVIII Sabine Schatz female /per… <tibble> ## 6 2339 XXVIII Mag. Selma Yildirim female /per… <tibble> ## 7 5639 XXVIII Julia Elisabeth Herr female /per… <tibble> ## 8 5643 XXVIII Mag.<sup>a</sup> Dr.<sup>in<… female /per… <tibble> ## 9 5647 XXVIII Petra Tanzler female /per… <tibble> ## 10 14835 XXVIII Petra Bayr, MA MLS female /per… <tibble> ## 11 25188 XXVIII MMag. Michaela Schmidt female /per… <tibble> ## 12 30653 XXVIII Melanie Erasim, MSc female /per… <tibble> ## 13 30688 XXVIII Mag. Katrin Auer female /per… <tibble> ## 14 30696 XXVIII Mag. Elke Hanel-Torsch female /per… <tibble> ## 15 30704 XXVIII Silvia Kumpan-Takacs, MSc BA female /per… <tibble> ## 16 30708 XXVIII Barbara Teiber, MA female /per… <tibble> ## 17 30709 XXVIII MMag. Pia Maria Wieninger female /per… <tibble> ## 18 60446 XXVIII Mag. Muna Duzdar female /per… <tibble> ## 19 80479 XXVIII Mag. Karin Greiner female /per… <tibble># MPs from Vienna during the 27th periodvienna_mps <-get_mps(state ="Wien",legis_period =27,institution ="NR")## {"ATTR_JSON.mandate_detail.gremium_name":["Nationalrat"],"ATTR_JSON.mandate_detail.gp_text_full_short":["23.10.2019 - 23.10.2024: XXVII. GP"],"ATTR_JSON.mandate_detail.wahlkreis_bundesland":["Wien"]} ## https://www.parlament.gv.at/recherchieren/personen/parlamentarierinnen-ab-1848/parlamentarierinnen-ab-1918?PERSON_409ATTR_JSON.mandate_detail.gremium_name=Nationalrat&PERSON_409ATTR_JSON.mandate_detail.gp_text_full_short=23.10.2019%20-%2023.10.2024:%20XXVII.%20GP&PERSON_409ATTR_JSON.mandate_detail.wahlkreis_bundesland=Wien## [1] 32vienna_mps## # A tibble: 32 × 6## # Groups: pad_intern [32]## pad_intern legis_period name gender link mp_details## <int> <chr> <chr> <chr> <chr> <list> ## 1 145 XXVII Doris Bures female /per… <tibble> ## 2 1976 XXVII Mag. Martin Engelberg male /per… <tibble> ## 3 1986 XXVII Dr. Gudrun Kugler female /per… <tibble> ## 4 2006 XXVII Karl Mahrer male /per… <tibble> ## 5 2122 XXVII Nico Marchetti male /per… <tibble> ## 6 2242 XXVII Mag. Dr. Maria Theresia Niss… female /per… <tibble> ## 7 2344 XXVII Dr. Stephanie Krisper female /per… <tibble> ## 8 2834 XXVII Mag. Dr. Martin Graf male /per… <tibble> ## 9 3133 XXVII MMst. Mag. (FH) Maria Neumann female /per… <tibble> ## 10 5649 XXVII Mag. Eva Blimlinger female /per… <tibble> ## # ℹ 22 more rows
4 Mandates: Tracing careers across Parliament
get_mandates() returns all current and past mandates for one or more individuals. It accepts either names or unique pad_intern identifiers. Mandates are not limited to Parliament — they also cover executive positions (e.g., Chancellor, Minister), presidencies, and other functions.
The returned data includes position_code (e.g., “NR”, “BK” for Bundeskanzler, “BM” for Bundesminister), start and end dates, party affiliation, and a flag for whether the mandate is currently active. This makes it straightforward to calculate the total time an MP has served:
Service duration calculation
# How long have current NR members served in the National Council?df_mandates <-get_mandates(pad_intern = df_current$pad_intern, institution ="NR")duration <- df_mandates |># For active mandates, use today as end datemutate(position_date_end =case_when(is.na(position_date_end) & position_active ==TRUE~today(),.default = position_date_end )) |>mutate(days =as.numeric(difftime(position_date_end, position_date_start, units ="days")) ) |>summarise(total_days =sum(days), .by =c(pad_intern, name)) |>arrange(desc(total_days))# Top 5 longest-serving current NR membersduration |>slice_head(n =5)## # A tibble: 5 × 3## pad_intern name total_days## <chr> <chr> <dbl>## 1 145 Doris Bures 14670## 2 2834 Mag. Dr. Martin Graf 13717## 3 35514 Wolfgang Zanger 13168## 4 12741 Peter Haubner 9507## 5 83129 MMMag. Dr. Axel Kassegger 8576
get_mandates() also accepts a date argument to filter for mandates active at a specific point in time, and an institution argument to restrict to a single chamber.
5 Identifying MPs: get_pad_intern() and get_names()
Two further examples of auxiliary functions are get_pat
The unique identifier for each person in the Parliament’s database is pad_intern. Many functions accept it as input. To look it up by name:
Look up pad_intern ID
get_pad_intern("Strache")## # A tibble: 3 × 2## pad_intern names_variants ## <chr> <chr> ## 1 1905 Max Strache ## 2 35518 Heinz-Christian Strache ## 3 44127 Pia Philippa Beck, Pia Philippa Strache
get_names() retrieves all name variants an MP has used over time — relevant when names change due to marriage, divorce, or other reasons. A good example is Pia Philippa Beck (formerly Strache):
Name variants over time
# All name variantsget_names(pad_intern =44127)## index pad_intern name date_start date_end## 1 1 44127 Pia Philippa Beck 2023-06-28 <NA>## 2 2 44127 Pia Philippa Strache <NA> 2023-06-27## name_clean name_family name_given## 1 Pia Philippa Beck Beck Pia Philippa ## 2 Pia Philippa Strache Strache Pia Philippa ## note## 1 <NA>## 2 (bis 27.6.2023: Pia Philippa Strache)
6 Parliamentary items (Verhandlungsgegenstände)
get_items() is arguably the most versatile function in the package. It covers a broad spectrum of parliamentary business — motions (Anträge), committee reports (Ausschussberichte), government bills (Regierungsvorlagen), written questions (Anfragen), EU submissions, citizen initiatives (Bürgerinitiativen), popular initiatives (Volksbegehren), and more — all controlled through the item parameter.
# Written and oral questions from the 20th to 27th periodquestions <-get_items(item ="J_JPR_M", legis_period =seq(20, 27))## {"GP_CODE":["XX","XXI","XXII","XXIII","XXIV","XXV","XXVI","XXVII"],"VHG":["J_JPR_M"]} ## https://www.parlament.gv.at/recherchieren/gegenstaende/index.html?FP_001GP_CODE=XX&FP_001GP_CODE=XXI&FP_001GP_CODE=XXII&FP_001GP_CODE=XXIII&FP_001GP_CODE=XXIV&FP_001GP_CODE=XXV&FP_001GP_CODE=XXVI&FP_001GP_CODE=XXVII&FP_001VHG=J_JPR_M## [1] 77208nrow(questions)## [1] 77208
The returned data includes metadata such as topic classification (topics is a list-column), sponsoring parliamentary group(s), and document type. This makes it straightforward to aggregate and visualise parliamentary activity — for example, counting questions by party and legislative period.
Visualise questions by party across periods
party_colors <-c("FPÖ"="#0056A2","BZÖ"="#F5831F","GRÜNE"="#78A22F","JETZT/Pilz"="#1C3A5E","NEOS"="#E84188","ÖVP"="#62C2CE","SPÖ"="#E2001A","Stronach"="#888888")questions |> tidyr::unnest(parl_group) |>mutate(party =case_match( parl_group,c("F", "F-BZÖ", "FPÖ") ~"FPÖ","BZÖ"~"BZÖ","GRÜNE"~"GRÜNE",c("JETZT", "PILZ") ~"JETZT/Pilz",c("L", "NEOS-LIF", "NEOS") ~"NEOS","ÖVP"~"ÖVP","SPÖ"~"SPÖ","STRONACH"~"Stronach",.default =NA_character_ ),period_int =as.integer(as.roman(legis_period)) ) |>filter(!is.na(party), institution =="NR") |>count(period_int, party) |>ggplot(aes(x = period_int, y = n, colour = party)) +geom_line(linewidth =0.8) +geom_point(size =2.5) +scale_colour_manual(values = party_colors, name =NULL) +scale_x_continuous(breaks =20:27) +labs(x ="Legislative period",y ="Number of questions",title ="Parliamentary questions by party, periods 20–27",subtitle ="National Council — written and oral questions (J, JPR, M)" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot",legend.position ="right",panel.grid.minor =element_blank() )
Figure 1: Written and oral questions (J_JPR_M) filed per parliamentary group across legislative periods 20–27, National Council.
6.3 Filtering by topic and parliamentary group
Items can be filtered by topic (Thema), parliamentary group, EuroVoc terms, and more. The parl_group_names_standard argument expands party abbreviations to their historical variants — so searching for “FPÖ” also captures items filed under “F” or “F-BZÖ”:
Filter by topic and party
# SPÖ motions on health topics during the 27th periodspoe_health <-get_items(item ="ANTR",topic ="Gesundheit und Ernährung",parl_group ="SPÖ",legis_period =27,institution ="NR",parl_group_names_standard =TRUE)## {"THEMEN":["Gesundheit und Ernährung"],"NRBR":["NR"],"GP_CODE":["XXVII"],"VHG":["ANTR"],"FRAK_CODE":["SPÖ"]} ## https://www.parlament.gv.at/recherchieren/gegenstaende/index.html?FP_001THEMEN=Gesundheit%20und%20Ern%C3%A4hrung&FP_001NRBR=NR&FP_001GP_CODE=XXVII&FP_001VHG=ANTR&FP_001FRAK_CODE=SP%C3%96## [1] 601
7 An MP’s work in detail
get_mps_details() provides granular data on individual MPs’ plenary speeches, committee memberships, and parliamentary activities. It takes a pad_intern and a detail_type (“plenary”, “committees”, or “activities”).
7.1 Plenary speeches
Plenary speeches
# All plenary speeches by Stephanie Krisper in the National Councilget_pad_intern("Krisper")## # A tibble: 1 × 2## pad_intern names_variants ## <chr> <chr> ## 1 2344 Stephanie Krisperdf_plenary_krisper <-get_mps_details(pad_intern =2344,detail_type ="plenary",institution ="NR",echo=T)## {"PAD_INTERN":[2344],"GREMIUM":["N"]} ## https://www.parlament.gv.at/person/2344?BIO_250PAD_INTERN=2344&BIO_250GREMIUM=N&selectedtab=PLENUM## [1] 135df_plenary_krisper |>unnest(position_name) |>count(position_name, sort =TRUE)## # A tibble: 1 × 2## position_name n## <chr> <int>## 1 Abgeordnete zum Nationalrat 135
get_mps_details() returns all speeches regardless of the person’s role at the time. The position_name list-column reveals which position(s) the person held when the speech was given.
A natural follow-up is to visualise when those speeches happened. The date column maps directly onto a GitHub-style calendar heatmap:
Calendar heatmap of Krisper’s speeches
# GitHub's five-level contribution palettegh_pal <-c("0"="#ebedf0","1"="#9be9a8","2–3"="#40c463","4–6"="#30a14e","7+"="#216e39")# Full date spine — silent days render as lightest tile, not blank spacedate_min <- lubridate::floor_date(min(df_plenary_krisper$date), "year")date_max <- lubridate::ceiling_date(max(df_plenary_krisper$date), "year") -1df_cal <-tibble(date =seq.Date(date_min, date_max, by ="day")) |>mutate(iso_year = lubridate::isoyear(date), # keeps Dec/Jan ISO-week edge days in the right facetweek_no = lubridate::isoweek(date),dow = lubridate::wday(date, label =TRUE, abbr =TRUE, week_start =1) ) |>left_join(count(df_plenary_krisper, date, name ="n"), by ="date") |>replace_na(list(n =0)) |>mutate(n_cat =cut(n,breaks =c(-Inf, 0, 1, 3, 6, Inf),labels =names(gh_pal),right =TRUE) )ggplot(df_cal, aes(x = week_no, y =fct_rev(dow), fill = n_cat)) +geom_tile(width =0.85, height =0.85, colour =NA) +facet_wrap(~ iso_year, ncol =1, strip.position ="left") +scale_fill_manual(values = gh_pal, name ="Speeches", drop =FALSE) +scale_x_continuous(breaks =c(1, 5, 9, 14, 18, 22, 27, 31, 35, 40, 44, 48),labels = month.abb,expand =c(0.01, 0) ) +scale_y_discrete(breaks =c("Mon", "Wed", "Fri"), # GitHub only labels alternating daysexpand =c(0, 0.5) ) +coord_fixed() +labs(x =NULL, y =NULL,title ="Stephanie Krisper — plenary speeches",subtitle ="National Council" ) +theme_minimal(base_size =11) +theme(plot.title.position ="plot",panel.grid =element_blank(),axis.ticks =element_blank(),strip.text.y.left =element_text(angle =0, size =9, hjust =1),legend.position ="bottom",legend.key.size =unit(0.55, "cm"),legend.direction ="horizontal",panel.spacing.y =unit(0.6, "cm"),plot.background =element_rect(fill ="white", colour =NA) )
Figure 2: GitHub-style calendar heatmap of Stephanie Krisper’s plenary speeches. Each cell is one day; colour depth encodes speech count using GitHub’s five-level contribution palette.
To get a sense of what Krisper was actually speaking about, we can trace the exact items she debated. The meeting_url in the plenary data and url_meeting in get_plenary_meetings() refer to the same sessions but use different URL schemes (/sitzung/ vs /gegenstand/), so a small normalisation step aligns them. That gives us url_item for each item in her sessions. We then call get_item_details() per item, which returns a speeches nested column listing every speaker per stage. Filtering to Krisper’s name leaves only items she personally contributed to, each carrying topics, headwords, and eurovoc.
Topics of items Krisper actually spoke on
# Legislative periods as integerskrisper_periods <- df_plenary_krisper |>distinct(legis_period) |>mutate(p =as.integer(as.roman(legis_period))) |>pull(p)# Step 1: Items on the agenda in all sessions where Krisper spokedf_meeting_acts <-map( krisper_periods, \(p) get_plenary_meetings(institution ="NR", legis_period = p,meeting_and_activities ="activities")) |>list_rbind()# meeting_url uses /sitzung/XXVI/NRSITZ/7?selectedStage=111# url_meeting uses /gegenstand/XXVI/NRSITZ/7 — same meeting, different URL scheme# normalise: swap path prefix and strip query string before joiningkrisper_joined <- df_plenary_krisper |>distinct(meeting_url) |>mutate(url_meeting_norm = meeting_url |>str_replace("/sitzung/", "/gegenstand/") |>str_remove("\\?.*") ) |>left_join(df_meeting_acts, by =c("url_meeting_norm"="url_meeting"))unmatched_k <- krisper_joined |>filter(is.na(url_item)) |>pull(meeting_url)if (length(unmatched_k) >0)message(sprintf("%d meeting(s) unmatched in activities data:\n%s",length(unmatched_k), paste(unmatched_k, collapse ="\n")))krisper_item_urls <- krisper_joined |>filter(!is.na(url_item)) |>distinct(url_item)# Step 2: Scrape item details — one request per item; cache: true avoids re-runningsafe_results_k <- krisper_item_urls$url_item |>map(safely(get_item_details))failed_k <- krisper_item_urls$url_item[map_lgl(safe_results_k, \(x) !is.null(x$error))]if (length(failed_k) >0)message(sprintf("%d item(s) failed to parse:\n%s", length(failed_k), paste(failed_k, collapse ="\n")))df_item_details <- safe_results_k |>map("result") |>compact() |>list_rbind()# Step 3: Find items where Krisper appears as a speaker in any stagespoke_items <- df_item_details |>filter(!map_lgl(speeches, is.null)) |>unnest(speeches) |>filter(str_detect(speaker, "Krisper")) |>distinct(item_url)# Step 4: One row per item (topics are item-level, replicated across stage rows)df_krisper_spoke <- df_item_details |>semi_join(spoke_items, by ="item_url") |>distinct(item_url, .keep_all =TRUE)# Visualise topicsdf_krisper_spoke |>unnest(topics) |>count(topics, sort =TRUE) |>slice_head(n =15) |>mutate(topics =fct_reorder(topics, n)) |>ggplot(aes(x = n, y = topics)) +geom_col(fill ="#216e39") +labs(x ="Number of items",y =NULL,title ="What did Krisper actually speak about?",subtitle ="Topics of parliamentary items where she personally contributed" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot",panel.grid.major.y =element_blank() )
Figure 3: Topics of parliamentary items where Stephanie Krisper personally gave a speech.
The same analysis applies to any MP. Here is Ernst Gödl (ÖVP), offering a useful contrast to Krisper’s profile:
Gödl plenary speeches
get_pad_intern("Gödl")## # A tibble: 1 × 2## pad_intern names_variants## <chr> <chr> ## 1 83409 Ernst Gödldf_plenary_goedl <-get_mps_details(pad_intern =83409,detail_type ="plenary",institution ="NR")## {"PAD_INTERN":[83409],"GREMIUM":["N"]} ## https://www.parlament.gv.at/person/83409?BIO_250PAD_INTERN=83409&BIO_250GREMIUM=N&selectedtab=PLENUM## [1] 127df_plenary_goedl |>unnest(position_name) |>count(position_name, sort =TRUE)## # A tibble: 2 × 2## position_name n## <chr> <int>## 1 Abgeordneter zum Nationalrat 127## 2 Ordner des Nationalrates 26
Figure 5: Topics of parliamentary items where Ernst Gödl personally gave a speech.
And finally August Wöginger (ÖVP), the long-serving parliamentary club chairman — his profile as a senior government-side MP should look quite different again:
Wöginger plenary speeches
get_pad_intern("Wöginger")## # A tibble: 2 × 2## pad_intern names_variants ## <chr> <chr> ## 1 14795 August Wöginger## 2 1525 Josef Wögingerdf_plenary_woeginger <-get_mps_details(pad_intern =14795,detail_type ="plenary",institution ="NR")## {"PAD_INTERN":[14795],"GREMIUM":["N"]} ## https://www.parlament.gv.at/person/14795?BIO_250PAD_INTERN=14795&BIO_250GREMIUM=N&selectedtab=PLENUM## [1] 434df_plenary_woeginger |>unnest(position_name) |>count(position_name, sort =TRUE)## # A tibble: 1 × 2## position_name n## <chr> <int>## 1 Abgeordneter zum Nationalrat 434
Figure 7: Topics of parliamentary items where August Wöginger personally gave a speech.
With data on all three MPs in hand, we can compare their topical focus side by side. The chart below shows the relative composition of topics for items each MP personally spoke on, with the top 4 topics per MP labelled and everything else grouped as “Other”.
Comparative topic composition across MPs
# Combine topic counts for all three MPsdf_all_topics <-bind_rows( df_krisper_spoke |>unnest(topics) |>count(topics) |>mutate(mp ="Krisper"), df_goedl_spoke |>unnest(topics) |>count(topics) |>mutate(mp ="Gödl"), df_woeginger_spoke |>unnest(topics) |>count(topics) |>mutate(mp ="Wöginger"))# Per-MP top 4 topicstop4_per_mp <- df_all_topics |>group_by(mp) |>slice_max(n, n =4, with_ties =FALSE) |>ungroup() |>distinct(mp, topics)# Relabel non-top-4 as "Other" and aggregatedf_plot <- df_all_topics |>left_join(top4_per_mp |>mutate(keep =TRUE), by =c("mp", "topics")) |>mutate(topic_label =if_else(is.na(keep), "Other", topics)) |>summarise(n =sum(n), .by =c(mp, topic_label)) |>mutate(pct = n /sum(n), .by = mp)# Order named topics by combined frequency; "Other" always lasttopic_order <- df_plot |>filter(topic_label !="Other") |>summarise(total =sum(n), .by = topic_label) |>arrange(desc(total)) |>pull(topic_label)df_plot <- df_plot |>mutate(topic_label =factor(topic_label, levels =c(topic_order, "Other")),mp =factor(mp, levels =c("Wöginger", "Gödl", "Krisper")) )# Colour palette — interpolated Set2 for named topics, grey for Othern_named <-length(topic_order)topic_colors <-setNames(c(colorRampPalette(RColorBrewer::brewer.pal(8, "Set2"))(n_named), "#cccccc"),c(topic_order, "Other"))ggplot(df_plot, aes(x = mp, y = pct, fill = topic_label)) +geom_col(position =position_stack(reverse =TRUE), width =0.6) +geom_text(aes(label =if_else(pct >=0.05, scales::percent(pct, accuracy =1), "")),position =position_stack(vjust =0.5, reverse =TRUE),size =3, colour ="white", fontface ="bold" ) +scale_fill_manual(values = topic_colors, name =NULL) +scale_y_continuous(labels = scales::percent_format(accuracy =1)) +coord_flip() +labs(x =NULL, y =NULL,title ="Topical focus of plenary speeches",subtitle ="Top 4 topics per MP; remaining topics grouped as 'Other'" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot",legend.position ="bottom",legend.key.size =unit(0.45, "cm"),panel.grid.major.y =element_blank() ) +guides(fill =guide_legend(nrow =2))
Figure 8: Relative topical composition of parliamentary items each MP personally spoke on. Top 4 topics per MP shown individually; all others grouped as ‘Other’.
df_yearly <- df_all_items |>mutate(year = lubridate::year(date)) |>unnest(topics) |>count(year, topics)top_topics <- df_yearly |>summarise(total =sum(n), .by = topics) |>slice_max(total, n =6, with_ties =FALSE) |>pull(topics)df_plot_yearly <- df_yearly |>mutate(topic_label =if_else(topics %in% top_topics, topics, "Other")) |>summarise(n =sum(n), .by =c(year, topic_label))topic_order <- df_plot_yearly |>filter(topic_label !="Other") |>summarise(total =sum(n), .by = topic_label) |>arrange(desc(total)) |>pull(topic_label)df_plot_yearly <- df_plot_yearly |>mutate(topic_label =factor(topic_label, levels =c(topic_order, "Other")))n_named <-length(topic_order)topic_colors <-setNames(c(colorRampPalette(RColorBrewer::brewer.pal(8, "Set2"))(n_named), "#cccccc"),c(topic_order, "Other"))ggplot(df_plot_yearly, aes(x = year, y = n, fill = topic_label)) +geom_col(position =position_stack(reverse =TRUE), width =0.75) +scale_fill_manual(values = topic_colors, name =NULL) +scale_x_continuous(breaks = scales::pretty_breaks()) +scale_y_continuous(expand =expansion(mult =c(0, 0.05))) +labs(x =NULL, y ="Number of items",title ="How the topical focus of parliamentary work changed over time",subtitle ="All item types, National Council, periods XX–XXVII; top 6 topics shown" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot",legend.position ="bottom",legend.key.size =unit(0.45, "cm"),panel.grid.major.x =element_blank() ) +guides(fill =guide_legend(nrow =2))
Figure 9: Topical composition of all National Council items by year
Code
df_yearly_weighted <- df_all_item_details |>mutate(year = lubridate::year(date_introduced),n_speeches =map_int(speeches, \(s) if (is.null(s)) 0L elsenrow(s)) ) |>filter(n_speeches >0) |>unnest(topics) |>group_by(year, topics) |>summarise(n =sum(n_speeches), .groups ="drop")df_plot_yearly_weighted <- df_yearly_weighted |>mutate(topic_label =if_else(topics %in% top_topics, topics, "Other")) |>summarise(n =sum(n), .by =c(year, topic_label)) |>mutate(topic_label =factor(topic_label, levels =c(topic_order, "Other")))ggplot(df_plot_yearly_weighted, aes(x = year, y = n, fill = topic_label)) +geom_col(position =position_stack(reverse =TRUE), width =0.75) +scale_fill_manual(values = topic_colors, name =NULL) +scale_x_continuous(breaks = scales::pretty_breaks()) +scale_y_continuous(expand =expansion(mult =c(0, 0.05))) +labs(x =NULL, y ="Number of speeches",title ="How the intensity of parliamentary debate changed over time",subtitle ="Items weighted by speeches held; all item types, National Council, periods XX–XXVII" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot",legend.position ="bottom",legend.key.size =unit(0.45, "cm"),panel.grid.major.x =element_blank() ) +guides(fill =guide_legend(nrow =2))
Figure 10: Topical composition of National Council speeches by year (speech-weighted)
8 Speech-level analysis
The charts above treat parliamentary items as the unit of analysis. But get_plenary_meeting_details() lets us go one level deeper — to individual speeches — and links each speech to the speaker, duration, and debate context. Combining this with get_mps() for gender and party affiliation produces a speech-level dataset that opens up a different class of questions: not just what was debated, but who spoke, how long, and on what topic.
The dataset covers all 90,610 speeches delivered in the National Council across legislative periods XX–XXVII (1996–2024). Each row records the speaker’s name, gender, party, the date and duration of the speech (in seconds), the debate type, and — where a formal resolution was adopted — the item-level topic classifications (topics, EuroVoc descriptors, keywords).
8.1 Do women and men speak for different lengths of time by topic?
A first cut: average speech duration by gender and topic, limited to speeches with matched topic data.
Figure 11: Mean speech duration by gender and topic
The raw picture shows women speaking longer on Frauen und Gleichbehandlung (+29 s), Innovation (+23 s), and Familie (+10 s), while men speak longer on Landesverteidigung (−18 s), Europäische Union (−18 s), and Budget (−13 s). But raw means conflate individual speaking style with topic effects — a handful of prolific male MPs dominating certain debates could drive the whole pattern.
8.2 Controlling for individual speaking style: a mixed effects model
To isolate the topic-specific gender effect we fit a log-linear mixed effects model with a random intercept per MP (absorbing individual baseline speaking style), fixed effects for party, legislative period, debate type, and formal time limit:
# tt_gender is pre-extracted in the cached speech-model-gender chunk# Overall gender effect (on reference topic)main_gender <- tt_gender["gendermale", ]cat(sprintf("Overall gender effect: β = %.3f, SE = %.3f, t = %.2f, p = %.3f\n", main_gender$Value, main_gender$`Std.Error`, main_gender$`t-value`, main_gender$`p-value`))## Overall gender effect: β = -0.037, SE = 0.018, t = -2.00, p = 0.046# Gender:topic interactionsgi <- tt_gender[grep("gendermale:topics", tt_gender$term), ]gi$topic <-sub("gendermale:topics", "", gi$term)gi$total_effect <- main_gender$Value + gi$Value # total gender gap on this topicgi$topic <-fct_reorder(gi$topic, gi$total_effect)ggplot(gi, aes(x = total_effect, y = topic)) +geom_vline(xintercept =0, linewidth =0.4) +geom_vline(xintercept = main_gender$Value, linetype ="dashed",colour ="grey50", linewidth =0.4) +geom_pointrange(aes(xmin = total_effect -1.96*`Std.Error`,xmax = total_effect +1.96*`Std.Error`,colour =`p-value`<0.05),size =0.4 ) +scale_colour_manual(values =c("TRUE"="#E41A1C", "FALSE"="grey40"),name ="p < 0.05") +scale_x_continuous(labels = scales::percent_format(accuracy =1)) +labs(x ="Total gender effect on log(duration) — male vs female",y =NULL,title ="After controlling for MP style, topic-specific gender gaps vanish",subtitle ="Dashed line = overall gender effect; red = significant at 5%" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot", panel.grid.major.y =element_blank())
Figure 12: Gender × topic interactions from mixed effects model
After accounting for individual speaking style, the apparent topic-level gender differences largely disappear. The overall effect is small but significant (β = −0.037, p = 0.046): on average women speak ~3.7% longer than men after controls. Only Budget und Finanzen shows a significant interaction (p = 0.037), where the gender gap narrows almost to zero — men speak nearly as long as women on budget topics, unlike most other areas where women are slightly longer.
8.3 Does party predict how long MPs speak by topic?
Replacing gender with party affiliation tests whether ideological positioning shapes topic-specific speaking time. We restrict to the six parties with sufficient observations (ÖVP, SPÖ, FPÖ, GRÜNE, NEOS, BZÖ) and use ÖVP as the reference category.
# tt_party is pre-extracted in the cached speech-model-party chunkmain_party <- tt_party[grepl("^party[A-Z]", tt_party$term) &!grepl(":", tt_party$term), ]main_party$party <-str_remove(main_party$term, "party")main_party$party <-fct_reorder(main_party$party, main_party$Value)ggplot(main_party, aes(x = Value, y = party)) +geom_vline(xintercept =0, linewidth =0.4) +geom_pointrange(aes(xmin = Value -1.96*`Std.Error`,xmax = Value +1.96*`Std.Error`,colour =`p-value`<0.05),size =0.5 ) +scale_colour_manual(values =c("TRUE"="#E41A1C", "FALSE"="grey40"),name ="p < 0.05") +scale_x_continuous(labels = scales::percent_format(accuracy =1)) +labs(x ="Effect on log(duration) vs ÖVP",y =NULL,title ="SPÖ speaks significantly shorter than ÖVP across all topics",subtitle ="Mixed effects model controlling for MP style, period, debate type, time limit" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot", panel.grid.major.y =element_blank())
Figure 13: Main party effects on speech duration (vs ÖVP)
SPÖ is the only party that speaks significantly shorter than ÖVP (β = −0.056, p = 0.014, ~5.4% on the log scale). No other party differs significantly from ÖVP, and — crucially — none of the 110 party × topic interaction terms reach significance. The formal Redezeit allocation system, which distributes floor time proportionally to party size, appears to dominate over any ideological differences in how parties allocate their speaking time across topics.
8.4 Do female MPs speak at their representation rate?
The gender gap in speech duration is one question; a different question is whether female MPs speak at all at the rate their numbers would predict. A party where 30% of MPs are women should, all else equal, see roughly 30% of its plenary speeches delivered by women. If the observed share is lower, female MPs are speaking below their representation rate — a signal of internal party culture, not just headcount.
The analysis pairs the MP roster from get_mps() with the speech dataset to compute, for each party × legislative period, the ratio of speeches-per-female-MP to speeches-per-male-MP. A ratio of 1 means parity; below 1 means female MPs speak less per capita than their male colleagues.
Figure 14: Female MP share vs female speech share by party and legislative period
Code
df_ratio |>mutate(party =factor(party, levels = PARTIES_6)) |>ggplot(aes(x = period_int, y = activity_ratio, colour = party)) +geom_hline(yintercept =1, linetype ="dashed", colour ="grey50", linewidth =0.5) +geom_line(linewidth =0.8, alpha =0.8) +geom_point(size =2.5) +scale_x_continuous(breaks =20:27,labels =paste0("GP ", as.roman(20:27)) ) +scale_y_continuous(labels = scales::label_number(suffix ="×")) +scale_colour_manual(values = party_colors_6, name =NULL) +labs(x =NULL,y ="Speeches per female MP / speeches per male MP",title ="FPÖ women are the least vocal relative to their numbers; GRÜNE women the most",subtitle ="Ratio = 1 means parity; above 1 means female MPs speak more per capita than male MPs" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot",axis.text.x =element_text(size =8),legend.position ="bottom" )
Figure 15: Female-to-male speech activity ratio by party across legislative periods
The pattern is consistent across periods. GRÜNE women track close to or above parity in every period — the party’s internal culture appears to actively support female participation. ÖVP women also sit close to the diagonal, though slightly below. SPÖ shows a modest but persistent gap that has widened in recent periods.
FPÖ is the outlier in the other direction. In every period with sufficient female MPs, FPÖ women speak at well below half the rate of their male colleagues. Period XXII is extreme: the post-BZÖ split left a small rump group, and the female MPs in it appear to have delivered no speeches at all. NEOS entered parliament in period XXV with a marked gap that has since narrowed — plausible for a new party still establishing internal speaking norms.
The parity line on the scatter makes the point directly: nearly all SPÖ, ÖVP, and GRÜNE points cluster around the diagonal; FPÖ points sit well below it regardless of how many women the party has in parliament.
8.5 Parliamentary activities
Parliamentary activities
# Written questions submitted by an MPget_pad_intern("Pilz")## # A tibble: 1 × 2## pad_intern names_variants## <chr> <chr> ## 1 1210 Peter Pilzdf_activities <-get_mps_details(pad_intern =1210,detail_type ="activities",institution ="NR",item ="JMIN"# Written questions to the Federal Government)## {"PAD_INTERN":[1210],"gremium":["N"],"vhg4":["JMIN"]} ## https://www.parlament.gv.at/person/1210?PERS_AKTIVIT_025PAD_INTERN=1210&PERS_AKTIVIT_025gremium=N&PERS_AKTIVIT_025vhg4=JMIN&selectedtab=AKT## [1] 459nrow(df_activities)## [1] 459glimpse(df_activities)## Rows: 459## Columns: 10## $ pad_intern <chr> "1210", "1210", "1210", "1210", "1210", "1210", "1210",…## $ legis_period <chr> "XXVI", "XXVI", "XXVI", "XXVI", "XXVI", "XXVI", "XXVI",…## $ institution <chr> "NR", "NR", "NR", "NR", "NR", "NR", "NR", "NR", "NR", "…## $ item_number <chr> "4165/J", "4166/J", "4167/J", "4113/J", "4164/J", "4093…## $ item_type <chr> "JMIN", "JMIN", "JMIN", "JMIN", "JMIN", "JMIN", "JMIN",…## $ title <chr> "Rechtmäßige Unterstützung der Gemeinde Erl für die Tir…## $ date_updated <chr> "15.11.2019", "14.11.2019", "06.11.2019", "30.10.2019",…## $ item_url <chr> "/gegenstand/XXVI/J/4165", "/gegenstand/XXVI/J/4166", "…## $ status_numeric <chr> "5", "5", "5", "5", "5", "5", "5", "5", "5", "5", "5", …## $ status_text <chr> "Schriftliche Beantwortung am 15.11.2019 (4142/AB)", "S…
9 Committees (Ausschüsse)
get_committees() retrieves committee information for a given legislative period and institution. It can also extract membership lists when details_type = "members" is set.
9.1 Listing committees
List committees
# All committees of the National Council in the 27th periodcommittees_nr27 <-get_committees(legis_period =27, institution ="NR")committees_nr27## # A tibble: 43 × 5## legis_period committee citation id_number url_committee## <chr> <chr> <chr> <int> <chr> ## 1 XXVII Ausschuss für Arbeit und Sozia… A-AS/1 883 https://www.…## 2 XXVII Außenpolitischer Ausschuss A-AU/1 884 https://www.…## 3 XXVII Ausschuss für Bauten und Wohnen A-BA/1 885 https://www.…## 4 XXVII Budgetausschuss A-BU/1 867 https://www.…## 5 XXVII Ständiger Unterausschuss des B… SA-BU/1 874 https://www.…## 6 XXVII Ständiger Unterausschuss in ES… SA-ESM/1 875 https://www.…## 7 XXVII Ausschuss für Familie und Juge… A-FA/1 886 https://www.…## 8 XXVII Finanzausschuss A-FI/1 887 https://www.…## 9 XXVII Ausschuss für Forschung, Innov… A-FO/1 888 https://www.…## 10 XXVII Geschäftsordnungsausschuss A-GO/1 873 https://www.…## # ℹ 33 more rows
9.2 Committees of inquiry (Untersuchungsausschüsse)
The citation parameter allows filtering by committee type code. Committees of inquiry use the code “USA”:
Committees of inquiry
# How many committees of inquiry per legislative period?inquiry_committees <-map(seq(20, 28), \(x) get_committees(legis_period = x, institution ="NR", citation ="USA")) |>list_rbind()inquiry_committees |>mutate(legis_period =factor(legis_period, levels =as.character(as.roman(20:28)))) |>count(legis_period, .drop =FALSE)## # A tibble: 9 × 2## legis_period n## <fct> <int>## 1 XX 0## 2 XXI 1## 3 XXII 0## 4 XXIII 3## 5 XXIV 2## 6 XXV 2## 7 XXVI 2## 8 XXVII 4## 9 XXVIII 1
10 Plenary meetings
get_plenary_meetings() retrieves information about plenary meetings from the Austrian Parliament’s API. Data is available from the 20th legislative period onward, and covers the National Council (NR), the Federal Council (BR), and the Federal Assembly (BV).
The meeting_and_activities parameter controls what is returned:
"meetings": Meeting metadata — dates, meeting numbers, agenda URLs, and meeting type
"activities": Items that occurred during meetings — motions, questions, declarations, etc.
Additional filters include session_type ("N" for ordinary, "A" for extraordinary sessions) and meeting_type ("S", "SO", "ZU", "N").
The Federal Assembly (Bundesversammlung), which convenes for presidential inaugurations and similar joint meetings, can be queried by setting institution = "BV" and legis_period = NULL.
11 Transcripts (Stenographische Protokolle)
get_transcripts() provides access to stenographic protocols. Results include links to both HTML and PDF versions of the transcripts. For bulk downloads, the function supports PDF export
Search transcripts
# Transcripts mentioning "Gesundheit" in the 28th periodtranscripts <-get_transcripts(search_string ="gesundheit",legis_period =28,session_type ="NRSITZ",echo =TRUE)glimpse(transcripts)
When defining a destination folder, the function provides for the download of the transcript files.
Download transcript PDFs
# Download PDFs to a local folderget_transcripts(legis_period =28,session_type ="NRSITZ",export ="pdf",export_destination ="my_transcripts")
12 The echo argument
Most functions accept an echo argument. When set to TRUE, the function prints the search parameters and a direct URL linking to the same results on the Parliament’s website:
I find this useful for verification, for quickly sharing a specific query with someone who prefers the web interface, and for sanity-checking that the API query is doing what I expect. It’s a small feature but it’s saved me more than once.
13 FPÖ: A party in transformation
Few parties in post-war Austrian history have undergone as dramatic a trajectory as the FPÖ. From a minor liberal-nationalist party in the 1950s, it was reshaped into a populist right-wing force under Jörg Haider in the 1980s and 1990s, entered government twice (2000–2005 and 2017–2019), survived an internal split that spawned the BZÖ, and re-emerged as the dominant opposition force after the 2019 Ibiza affair ended its second coalition. The ParlAT data — spanning legislative periods XX–XXVII (2002–2024) — captures the arc from post-Haider peak through opposition years, the Türkis-Blau government, and back into opposition.
The party group name itself changed several times: “Klub der Freiheitlichen Partei Österreichs” in periods XX–XXI, “Freiheitlicher Parlamentsklub - BZÖ” in the first half of period XXII (before the April 2005 split), then “Freiheitlicher Parlamentsklub” from the split onwards through period XXVII. The analysis below treats all of these as “FPÖ”, excluding the BZÖ breakaway faction which formed its own parliamentary group.
df_fpoe |>filter(is_fpoe, !is.na(year)) |>count(year) |>ggplot(aes(x = year, y = n)) +annotate("rect",xmin = govt_spans$xmin, xmax = govt_spans$xmax,ymin =-Inf, ymax =Inf,fill ="#2171b5", alpha =0.10 ) +annotate("text",x = (govt_spans$xmin + govt_spans$xmax) /2,y =c(2700, 2700),label = govt_spans$label,size =3, colour ="#2171b5", lineheight =0.9 ) +geom_vline(xintercept =2019.35, linetype ="dashed", colour ="#cb181d", linewidth =0.6) +annotate("text", x =2019.55, y =2600, label ="Ibiza\nscandal",hjust =0, size =3, colour ="#cb181d", lineheight =0.9) +geom_col(fill ="#2171b5", alpha =0.7, width =0.75) +scale_x_continuous(breaks =seq(2002, 2024, 2)) +scale_y_continuous(expand =expansion(mult =c(0, 0.05))) +labs(x =NULL, y ="Speeches",title ="FPÖ speaks less when in government — opposition is its natural habitat",subtitle ="Annual number of FPÖ plenary speeches, National Council, 2002–2024" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot", panel.grid.major.x =element_blank())
Figure 16: FPÖ parliamentary speech volume by year, 2002–2024
When FPÖ is in government, its speech volume drops sharply — ministers speak for the government, and coalition discipline constrains backbencher activity. The pattern repeats across both coalition periods. The post-Ibiza years saw a rebound as the party returned to full-throated opposition.
ggplot(df_fpoe_pct,aes(x =factor(period_int), y = pct, fill = topic_label)) +geom_col(position =position_stack(reverse =TRUE), width =0.8) +scale_x_discrete(labels = period_x_labels) +scale_y_continuous(labels = scales::percent_format(accuracy =1),expand =expansion(mult =c(0, 0.02))) +scale_fill_manual(values = fpoe_colors, name =NULL) +labs(x =NULL, y ="Share of matched speeches",title ="How FPÖ's topical focus shifted across legislative periods",subtitle ="Share of FPÖ plenary speeches per topic group; top 7 topics shown" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot",legend.position ="bottom",legend.key.size =unit(0.45, "cm"),panel.grid.major.x =element_blank() ) +guides(fill =guide_legend(nrow =2))
Figure 17: FPÖ topic composition by legislative period
Code
# For each topic: FPÖ share of all speeches vs. overall FPÖ sharefpoe_share_overall <- df_fpoe |>filter(!map_lgl(topics, is.null)) |>unnest(topics) |>summarise(n_fpoe =sum(is_fpoe, na.rm =TRUE),n_total =n(),.by = topics ) |>mutate(fpoe_share = n_fpoe / n_total,overall_fpoe =sum(n_fpoe) /sum(n_total),over_index = fpoe_share / overall_fpoe ) |>filter(n_total >=100) |>arrange(desc(over_index))top_n_show <-18df_oi <-bind_rows(slice_max(fpoe_share_overall, over_index, n = top_n_show %/%2),slice_min(fpoe_share_overall, over_index, n = top_n_show %/%2)) |>distinct(topics, .keep_all =TRUE) |>mutate(dir =if_else(over_index >=1, "over", "under"),label = topics )ggplot(df_oi, aes(x = over_index -1, y =reorder(label, over_index),fill = dir)) +geom_col(width =0.7, alpha =0.85) +geom_vline(xintercept =0, linewidth =0.4) +scale_x_continuous(labels = scales::percent_format(accuracy =1)) +scale_fill_manual(values =c(over ="#cb181d", under ="#2171b5"), guide ="none") +labs(x ="Over-/under-representation vs. overall FPÖ speech share",y =NULL,title ="FPÖ dominates security, migration and EU topics; avoids social policy",subtitle ="Topics where FPÖ's share of all speeches deviates most from its overall share" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot", panel.grid.major.y =element_blank())
Figure 18: FPÖ topic over-representation relative to parliament average
Code
# Government periods: GP XXI (period_int 21) and GP XXVI (period_int 26)# Approximate: period 21 = FPÖ entered government 2000 but our data starts 2002;# period 26 = 2017-19 coalition; year-based: 2002-2005 and 2017-2019df_role <- df_fpoe |>filter(is_fpoe, !map_lgl(topics, is.null), !is.na(year)) |>mutate(role =case_when( year >=2002& year <=2005~"Government (2002–05)", year >=2017& year <=2019~"Government (2017–19)",TRUE~"Opposition" )) |>unnest(topics) |>count(role, topics) |>group_by(role) |>mutate(pct = n /sum(n)) |>ungroup()top_topics_role <- df_role |>summarise(total =sum(n), .by = topics) |>slice_max(total, n =10, with_ties =FALSE) |>pull(topics)df_role_top <- df_role |>filter(topics %in% top_topics_role) |>pivot_wider(id_cols = topics, names_from = role, values_from = pct, values_fill =0) |>mutate(oppo_minus_govt = Opposition -rowMeans(pick(starts_with("Government")))) |>arrange(desc(abs(oppo_minus_govt)))df_role_plot <- df_role |>filter(topics %in% top_topics_role) |>mutate(role_group =if_else(str_starts(role, "Government"), "Government", "Opposition"),topics =factor(topics, levels = df_role_top$topics) ) |>summarise(pct =mean(pct), .by =c(topics, role_group))ggplot(df_role_plot, aes(x = pct, y = topics, fill = role_group)) +geom_col(position =position_dodge(width =0.7), width =0.6, alpha =0.85) +scale_x_continuous(labels = scales::percent_format(accuracy =0.1)) +scale_fill_manual(values =c("Government"="#2171b5", "Opposition"="#cb181d"),name =NULL ) +labs(x ="Share of FPÖ speeches on this topic",y =NULL,title ="In opposition, FPÖ talks more about migration and security",subtitle ="Top-10 topics by speech count; government vs. opposition periods averaged" ) +theme_minimal(base_size =12) +theme(plot.title.position ="plot",panel.grid.major.y =element_blank(),legend.position ="top" )
Figure 19: FPÖ topic profile: government vs. opposition periods
The contrast is stark. In opposition, FPÖ concentrates its speaking time on the topics most resonant with its voter base — migration, internal security, and European Union affairs. When in government, ministerial duties and coalition constraints push the party toward the full spectrum of policy, diluting the signature topics that define its brand. The data reflects a deliberate rhetorical strategy: maximise issue ownership pressure in opposition, manage policy breadth in government.
14 Party names across history
Anyone working with Austrian parliamentary data spanning multiple decades will run into the naming problem. The FPÖ has been listed under various abbreviations over the years — “FPÖ”, “F”, “F-BZÖ” — depending on the period and context. The same applies to other parties.
The helper function aux_parl_group_names_standard() expands party abbreviations to include their historical variants. Several get_*() functions expose this via a parl_group_names_standard argument, which ensures that a search for “FPÖ” also captures “F” and “F-BZÖ”, “NEOS” includes “NEOS-LIF”, and so on. This matters more than you might think when aggregating data across legislative periods.
15 Getting started
The Get Started vignette walks through several worked examples — from counting questions by party across eight legislative periods to visualising government bills by topic, tracing MP careers through mandates, and exploring committee structures. The full function reference is available on the documentation site.
{ParlAT} is still a work in progress — the API surface of the Austrian Parliament is broad, documentation is sometimes sparse, and edge cases keep surfacing. That said, I hope the package is already a useful tool for anyone in the #rstats community working with Austrian parliamentary data, whether for research, journalism, teaching, or just curiosity about what happens in the Hohe Haus.
Feedback of any kind is very welcome: bug reports, questions, suggestions for new functions or better defaults, and pointers to use cases I haven’t considered. The best place to reach me is the GitHub issue tracker — opening an issue is the most reliable way to make sure nothing gets los - or contact me via direct message on Bluesky.
@online{schmidt2026,
author = {Schmidt, Roland and Schmidt, Roland},
title = {Introducing {\{ParlAT\}:} {An} {R} {Package} to Access {Open}
{Data} of the {Austrian} {Parliament}},
date = {2026-02-14},
url = {https://werk.statt.codes/posts/2026-02-11-ParlAT-release/},
langid = {en}
}