bunddev: Wie bund.dev in R nutzbar wird

Von der bund.dev-Idee über echte API-Hürden bis zur dreischichtigen R-Architektur mit tidy Outputs.

R
OpenData
APIs
DataScience
Autor:in

Michael Bücker

Veröffentlichungsdatum

15. Februar 2026

bund.dev ist eine zentrale Infrastruktur für die Auffindbarkeit und Dokumentation von APIs der Bundesverwaltung.1 bunddev überträgt dieses Prinzip in ein R-orientiertes Analysemodell: von der API-Discovery über standardisierte Requests bis zur direkten Weiterverarbeitung in reproduzierbaren Data-Science-Workflows.2

Kurze Geschichte von bund.dev

Die öffentlich verfügbaren Quellen zeigen eine klare Entwicklungslinie:

  • Im Juli 2021 wurde das Repo bundesAPI/sofortmassnahmen angelegt und als zivilgesellschaftliches Beteiligungsverfahren zum “Zweiten Open Data Gesetz” veröffentlicht.3
  • Der dort dokumentierte 5-Punkte-Plan setzte das Ziel, bis 2024 Datensätze und Verwaltungsverfahren der Bundesverwaltung per API zugänglich zu machen.4
  • bund.dev positioniert sich dabei als Entwicklungs- und Dokumentationsportal: APIs auffindbar machen, dokumentieren und für Wiederverwendung öffnen.5
  • Das Python-Paket bundesAPI/deutschland (seit 2021) zeigt, dass daraus früh ein praktisches Ökosystem entstanden ist und nicht nur eine reine Link-Sammlung.6
  • Entscheidend: Dieses Fundament wurde von Freiwilligen aufgebaut, obwohl API-Dokumentation institutionell getragen sein sollte. Die Professionalität des Projekts führte teilweise zur Wahrnehmung als offizielles Angebot.78
  • Damit wurde jedoch auch eine strukturelle Schwäche sichtbar: Eine zivilgesellschaftliche Initiative kompensierte eine staatliche Infrastruktur-Lücke.
  • Parallel traten Gegenreaktionen auf. Öffentlich wurde beschrieben, dass einzelne Behörden dokumentierte Schnittstellen stärker absichern wollten; dies zeigt sich heute unter anderem im Umfeld der Bundesagentur für Arbeit.9

Der zentrale Punkt bleibt: Open Data ist nur dann wirksam, wenn Daten unter realen technischen Bedingungen tatsächlich nutzbar sind, für Zivilgesellschaft, Wissenschaft und Produktentwicklung.

Warum ein R-Paket?

In R-basierten Data-Science-Projekten entsteht Wert vor allem dann, wenn öffentliche Daten ohne Medienbruch in eine konsistente Pipeline überführt werden:

  • abrufen
  • bereinigen
  • joinen
  • visualisieren
  • modellieren

Hier lag die Lücke: Die APIs auf bund.dev sind inhaltlich stark, aber heterogen hinsichtlich Struktur, Authentifizierung und Antwortformaten. Für R-Workflows fehlte eine einheitliche Zugriffsschicht.

Der Aufbau von bunddev: Drei Schichten

Daraus folgt ein dreischichtiger Architekturansatz:10

  1. Registry-Layer: APIs entdecken, filtern, inspizieren (bunddev_list(), bunddev_info()).
  2. OpenAPI-Core: Specs lesen und beliebige Endpunkte generisch ansprechen (bunddev_spec(), bunddev_call()).
  3. Adapter-Layer: sofort nutzbare, tidy Ausgaben als Tibble (smard_timeseries(), dwd_station_overview(), autobahn_roadworks(), …).

Der Unterschied zu vielen Wrappern liegt im Zielkriterium: nicht nur Konnektivität, sondern analytische Verwendbarkeit als Primäranforderung.

Was bei bunddev schwierig war

Die zentralen Herausforderungen lagen weniger in R selbst als in der operativen Realität heterogener API-Landschaften:

  • Uneinheitliche Formate: je nach API sehr unterschiedliche JSON-Strukturen, teils tief verschachtelt.
  • Instabile Endpunkte: einzelne APIs ändern Pfade, Antwortformate oder verschwinden.
  • Auth-Varianten: API Keys, OAuth2, Session-Logik, teils ohne konsistente öffentliche Doku.
  • Bot-/Edge-Schutz: einzelne Quellen blocken nicht-browserbasierte Clients.
  • Fehlerbilder im Betrieb: z. B. Cache-Key-Kollisionen oder HTML-Änderungen, die Scraper/Parser brechen.

Konkret bedeutet das für bunddev: Einige Schnittstellen sind derzeit pausiert, bis belastbare Erreichbarkeit und konsistente Responses wieder gegeben sind:11

  • interpol: Akamai-JS-Bot-Detection blockiert Non-Browser-Clients.
  • zoll: Endpunkte entfernt (Redesign) plus Radware-Bot-Protection.
  • berufssprachkurssuche: öffentliche OAuth2-Credentials von der BA entzogen.
  • coachingangebote: öffentliche OAuth2-Credentials von der BA entzogen.
  • entgeltatlas: laut BA keine offizielle öffentliche API, Credentials entzogen.
  • weiterbildungssuche: interner, undokumentierter BA-Endpunkt, Credentials entzogen.

Hinzu kommen operative Einschränkungen trotz aktiver Adapter. Beispiel: hochwasserzentralen liefert für lagepegel zeitweise leere Responses. Bei diga ist ein gültiger Bearer-Token erforderlich, wodurch der Zugriff nicht unmittelbar öffentlich reproduzierbar ist.

Praktische Nutzung

Die folgenden Beispiele folgen explizit der Architektur von bunddev: Registry → OpenAPI-Core → Adapter. Damit wird die konzeptionelle Schichtung direkt in einen praktischen Analyse-Workflow übersetzt.

1) Registry-Schicht: APIs entdecken und einordnen

Der erste Schritt ist methodisch bewusst vorgeschaltet: nicht unmittelbar Endpunkte aufrufen, sondern zunächst thematisch passende APIs identifizieren und im Registry-Modell einordnen.
bunddev_list(tag = "energy") liefert den Überblick. bunddev_info("smard") ergänzt daraufhin die relevanten Metadaten einer konkreten API, etwa Basis-URL, Tags und Dokumentationslink.

Der Einstieg wird damit reproduzierbar strukturiert: zuerst Discovery, anschließend gezielte Analyse.

library(bunddev)

# Welche APIs sind im Bereich Energie vorhanden?
bunddev_list(tag = "energy")
# A tibble: 3 × 8
  id              title        provider spec_url docs_url auth  rate_limit tags 
  <chr>           <chr>        <chr>    <chr>    <chr>    <chr> <chr>      <lis>
1 ladestationen   Ladesaeulen… Bundesn… https:/… https:/… none  <NA>       <chr>
2 marktstammdaten Marktdatens… Bundesn… https:/… https:/… none  <NA>       <chr>
3 smard           SMARD API    Bundesn… https:/… https:/… none  Mehr als … <chr>
# Details zu einer konkreten API
bunddev_info("smard")
# A tibble: 1 × 8
  id    title     provider          spec_url     docs_url auth  rate_limit tags 
  <chr> <chr>     <chr>             <chr>        <chr>    <chr> <chr>      <lis>
1 smard SMARD API Bundesnetzagentur https://raw… https:/… none  Mehr als … <chr>

2) OpenAPI-Core-Schicht: Spezifikation in robuste Calls übersetzen

Der zweite Schritt adressiert Robustheit: Viele Fehler resultieren aus falsch angenommenen Parametern, etwa IDs, Wertebereichen oder Pfadvariablen.
bunddev_parameters("smard") listet die in der Spezifikation definierten Parameter. bunddev_parameter_values(smard_timeseries, "filter") liefert anschließend die zulässigen Filterwerte für exakt die Funktion, die später verwendet wird.

Das reduziert Trial-and-Error und erhöht die Stabilität von Analyse-Notebooks.

# Welche Parameter bietet die SMARD-API laut Spezifikation?
bunddev_parameters("smard")
# A tibble: 14 × 8
   method path             name  location required description schema_type enum 
   <chr>  <chr>            <chr> <chr>    <lgl>    <chr>       <chr>       <lis>
 1 get    /chart_data/{fi… filt… path     TRUE     "Mögliche … integer     <int>
 2 get    /chart_data/{fi… regi… path     TRUE     "Land / Re… string      <chr>
 3 get    /chart_data/{fi… reso… path     TRUE     "Auflösung… string      <chr>
 4 get    /chart_data/{fi… filt… path     TRUE     "Mögliche … integer     <int>
 5 get    /chart_data/{fi… filt… path     TRUE     "Muss dem … integer     <int>
 6 get    /chart_data/{fi… regi… path     TRUE     "Land / Re… string      <chr>
 7 get    /chart_data/{fi… regi… path     TRUE     "Muss dem … string      <chr>
 8 get    /chart_data/{fi… reso… path     TRUE     "Auflösung… string      <chr>
 9 get    /chart_data/{fi… time… path     TRUE      <NA>       integer     <chr>
10 get    /table_data/{fi… filt… path     TRUE     "Mögliche … integer     <int>
11 get    /table_data/{fi… filt… path     TRUE     "Muss dem … integer     <int>
12 get    /table_data/{fi… regi… path     TRUE     "Land / Re… string      <chr>
13 get    /table_data/{fi… regi… path     TRUE     "Muss dem … string      <chr>
14 get    /table_data/{fi… time… path     TRUE      <NA>       integer     <chr>
# Mögliche Werte für einen Parameter
bunddev_parameter_values(smard_timeseries, "filter")
 [1] "1223"        "1224"        "1225"        "1226"        "1227"       
 [6] "1228"        "4066"        "4067"        "4068"        "4069"       
[11] "4070"        "4071"        "410"         "4359"        "4387"       
[16] "4169"        "5078"        "4996"        "4997"        "4170"       
[21] "252"         "253"         "254"         "255"         "256"        
[26] "257"         "258"         "259"         "260"         "261"        
[31] "262"         "3791"        "123"         "126"         "715"        
[36] "5097"        "122"         "DE"          "AT"          "LU"         
[41] "DE-LU"       "DE-AT-LU"    "50Hertz"     "Amprion"     "TenneT"     
[46] "TransnetBW"  "APG"         "Creos"       "hour"        "quarterhour"
[51] "day"         "week"        "month"       "year"       

3) Adapter-Schicht: Tidy Daten direkt für Analyse und Visualisierung

Das dritte Beispiel bleibt vollständig innerhalb von bunddev und nutzt die Wind-Onshore-Erzeugung (filter = 4067) für die vier deutschen TSO-Regionen:

  • 50Hertz
  • Amprion
  • TenneT
  • TransnetBW

Damit entstehen zwei komplementäre Perspektiven:

  1. Zeitreihe über ein komplettes Jahr (Wochenwerte je Region).
  2. Interaktive Kartenansicht mit regionalem Mittelwert auf Basis der ÜNB-Regelzonen (GeoJSON).
Code
library(bunddev)
library(dplyr)
library(ggplot2)
library(leaflet)

# Zieljahr: letztes vollstaendiges Kalenderjahr
analysis_year <- as.integer(format(Sys.Date(), "%Y")) - 1
anchor_year <- analysis_year - 1

regions <- c("50Hertz", "Amprion", "TenneT", "TransnetBW")

wind_year <- lapply(regions, function(region_id) {
  idx <- smard_indices(4067, region = region_id, resolution = "week") |>
    mutate(anchor_time = bunddev_ms_to_posix(timestamp))

  anchor_ts <- idx |>
    filter(format(anchor_time, "%Y") == as.character(anchor_year)) |>
    summarise(ts = max(timestamp, na.rm = TRUE)) |>
    pull(ts)

  smard_timeseries(
    4067,
    region = region_id,
    resolution = "week",
    timestamp = anchor_ts
  ) |>
    filter(format(time, "%Y") == as.character(analysis_year)) |>
    transmute(region = region_id, week = time, wind_mw = value)
}) |>
  bind_rows()

# Ergebnisvorschau
wind_year |>
  group_by(region) |>
  summarise(
    wochen = n(),
    von = min(week),
    bis = max(week),
    mittelwert_mw = round(mean(wind_mw, na.rm = TRUE), 0),
    .groups = "drop"
  )
# A tibble: 4 × 5
  region     wochen von                 bis                 mittelwert_mw
  <chr>       <int> <dttm>              <dttm>                      <dbl>
1 50Hertz        51 2025-01-06 00:00:00 2025-12-22 00:00:00        628616
2 Amprion        51 2025-01-06 00:00:00 2025-12-22 00:00:00        428725
3 TenneT         51 2025-01-06 00:00:00 2025-12-22 00:00:00        890504
4 TransnetBW     51 2025-01-06 00:00:00 2025-12-22 00:00:00         55504
Code
# 1) Zeitreihe pro TSO-Region
ggplot(wind_year, aes(week, wind_mw, color = region)) +
  geom_line(linewidth = 0.9) +
  labs(
    title = paste0("Wind-Onshore-Erzeugung je TSO-Region (", analysis_year, ")"),
    subtitle = "SMARD Wochenwerte (Filter 4067), aufbereitet mit bunddev",
    y = "MW",
    x = NULL,
    color = NULL
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "top")

Code
# 2) Interaktive Polygonkarte der UeNB-Regelzonen
region_stats <- wind_year |>
  group_by(region) |>
  summarise(mittelwert_mw = mean(wind_mw, na.rm = TRUE), .groups = "drop")

geo_path <- "../../regelzonen_4ueNB.geojson"
if (!file.exists(geo_path)) {
  stop("GeoJSON nicht gefunden: ", geo_path)
}
if (!requireNamespace("sf", quietly = TRUE)) {
  stop("Paket 'sf' wird fuer die Karte benoetigt.")
}

normalize_tso <- function(x) {
  x_low <- tolower(x)
  dplyr::case_when(
    grepl("50hertz", x_low) ~ "50Hertz",
    grepl("amprion", x_low) ~ "Amprion",
    grepl("tennet", x_low) ~ "TenneT",
    grepl("transnet", x_low) ~ "TransnetBW",
    TRUE ~ NA_character_
  )
}

zones <- sf::st_read(geo_path, quiet = TRUE)
possible_cols <- c("TSO", "tso", "region", "REGION", "name", "NAME")
tso_col <- intersect(possible_cols, names(zones))[1]
if (is.na(tso_col)) {
  stop("Keine passende TSO-Spalte in der GeoJSON gefunden.")
}

zones <- zones |>
  mutate(region = normalize_tso(as.character(.data[[tso_col]]))) |>
  filter(!is.na(region)) |>
  left_join(region_stats, by = "region")

pal <- colorNumeric("viridis", domain = zones$mittelwert_mw, na.color = "#cccccc")

leaflet(zones) |>
  addProviderTiles(providers$CartoDB.Positron) |>
  addPolygons(
    fillColor = ~pal(mittelwert_mw),
    color = "white",
    weight = 1,
    fillOpacity = 0.85,
    label = ~paste0(region, ": ", format(round(mittelwert_mw, 0), big.mark = "."), " MW"),
    popup = ~paste0(
      "<b>", region, "</b><br/>",
      "Mittlere Windleistung ", analysis_year, ": ",
      format(round(mittelwert_mw, 0), big.mark = "."), " MW"
    ),
    highlightOptions = highlightOptions(weight = 2, color = "#333333", bringToFront = TRUE)
  ) |>
  addLegend(
    "bottomright",
    pal = pal,
    values = ~mittelwert_mw,
    title = "Mittlere Windleistung (MW)",
    opacity = 0.9
  )

Fazit

bunddev ist primär ein Produktivitätswerkzeug für Open-Data-Analysen in R: weniger API-Friktion, mehr Fokus auf die fachliche Fragestellung. Die Kombination aus Registry, generischem OpenAPI-Core und tidy Adaptern macht Datenquellen aus der Bundesverwaltung nicht nur erreichbar, sondern schnell analytisch verwertbar.

Fehlende Adapter oder instabile Endpunkte können jederzeit über Issues und PRs eingebracht werden.

Quellen