,

OpenData – Gasspeicher in Deutschland – Teil 2: 2026

2026 – Das Jahr mit wenig Gas. Wahrscheinlich.

Schlagzeilen wie „Gasspeicher so leer wie nie: Wird das Gas im Winter knapp? (Radio FFH)“ oder „Experte schlägt Alarm: Füllstand deutscher Gasspeicher auf „historischem Tiefstand“ (Berliner Morgenpost)“ konnte man im Spätsommer überall lesen. Wir aber brauchen keine Experten, wir haben die Daten selber.

Festzustellen, dass 2026 der niedrigste historische Füllstand ist, ist nicht schwierig. Man muss einfach in den Daten nachsehen, was der zweitniedrigste Wert war und man ist fertig:

import { readFile } from 'node:fs/promises';

async function main() {
    const data = JSON.parse(await readFile(new URL('history.json', import.meta.url)));

    // Nur Einträge behalten, deren Monat "08" (August) ist.
    // gasDayStart hat das Format "YYYY-MM-DD", slice(5, 7) liefert "MM".
    const augustEntries = data.filter((entry) => entry.gasDayStart.slice(5, 7) === '08');

    // Summe und Anzahl der "full"-Werte pro Jahr sammeln, um später
    // den Durchschnitt bilden zu können (Map: Jahr -> { sum, count }).
    const sumsByYear = new Map();
    for (const entry of augustEntries) {
        // Erste 4 Zeichen von gasDayStart ("YYYY-MM-DD") sind das Jahr.
        const year = entry.gasDayStart.slice(0, 4);
        // Bestehenden Zwischenstand holen oder mit 0/0 initialisieren.
        const stats = sumsByYear.get(year) ?? { sum: 0, count: 0 };
        stats.sum += entry.full;
        stats.count += 1;
        sumsByYear.set(year, stats);
    }

    // Aus den gesammelten Summen den Durchschnitt pro Jahr berechnen
    // und aufsteigend nach Durchschnitt sortieren (niedrigster zuerst).
    const averagesByYear = [...sumsByYear.entries()]
        .map(([year, { sum, count }]) => ({ year, average: sum / count }))
        .sort((a, b) => a.average - b.average);

    // Übersicht aller Jahre ausgeben, sortiert vom niedrigsten
    // zum höchsten August-Durchschnitt.
    for (const { year, average } of averagesByYear) {
        console.log(`${year}: ${average.toFixed(2)} %`);
    }

    // Die drei niedrigsten Jahre stehen nach der Sortierung ganz vorne
    // im Array, daher genügt Destrukturierung der ersten drei Elemente.
    const [lowest, secondLowest, thirdLowest] = averagesByYear;

    console.log(`\nNiedrigster:        ${lowest.year} mit ${lowest.average.toFixed(2)} %`);
    console.log(`Zweitniedrigster:   ${secondLowest.year} mit ${secondLowest.average.toFixed(2)} %`);
    console.log(`Drittniedrigster:   ${thirdLowest.year} mit ${thirdLowest.average.toFixed(2)} %`);

    // Differenzen zwischen den drei niedrigsten Jahren berechnen,
    // um zu zeigen, wie weit die Werte auseinanderliegen.
    console.log(`Unterschied 1-2:    ${(secondLowest.average - lowest.average).toFixed(2)} %`);
    console.log(`Unterschied 2-3:    ${(thirdLowest.average - secondLowest.average).toFixed(2)} %`);
    console.log(`Unterschied 1-3:    ${(thirdLowest.average - lowest.average).toFixed(2)} %`);
}

main();

Ausgabe:

Niedrigster:        2026 mit 48.55 %
Zweitniedrigster:   2021 mit 54.79 %
Drittniedrigster:   2015 mit 65.67 %
Unterschied 1-2:    6.24 %
Unterschied 2-3:    10.88 %
Unterschied 1-3:    17.12 %

Das war einfach. Weitergeben an die Redaktion und Feierabend… oder doch nicht.

2026 im historischen Kontext

Wie in Teil 1 beschrieben, ist dies aber nur ein Datenpunkt aus vielen Jahren. Wie war das in den vorherigen Jahren. Kann man aus den Graphen etwas ableiten?

Hinweis: Für die folgenden Diagramme benutzen wir nur die vorher geladenen historischen Daten. Die kontinuierliche Aktualisierung wird in Teil 4 beschrieben.

1. Der Tech-Stack

Wir nutzen ein klassisches Backend-/Frontend-System, bestehend aus PHP und JavaScript, das auch auf günstigen Managed Servern funktioniert.
PHP ist dabei für die Auswertung der Daten zuständig, JavaScript für die Darstellung der Diagramme.
Die Diagramme werden mit dem Apache-Projekt ECharts dargestellt. ECharts mag zunächst als Overkill erscheinen, immerhin 1MB Programmcode, aber wenn man später komplexere oder interaktive Diagramme erstellen möchte, muss man nicht die Bibliothek wechseln.

2. Eine einfache Darstellung

Die einfachste Gegenüberstellung der historischen Daten ist die Mittelwertbildung je Monat aller Jahre vor 2026 und die Mittelwerte der Monate im Jahr 2026 und die Darstellung als Liniendiagramm mit zwei Graphen.

Zusätzlich gibt eine Diagramm Auskunft über den Unterschied der Werte der beiden Graphen:

Oiginal-DiagrammCode bei GitHub

☝️ Hinweis: Hier werden nur die wichtigsten Programmteile beschrieben. Daher kann es sein, das Variablen und Funktionen „magisch“ auftauchen, die vorher nicht beschrieben wurden.

2.1 Einlesen der Daten

$raw = file_get_contents($dataFile); // Rohinhalt der JSON-Datei einlesen
$entries = json_decode($raw, true); // JSON in assoziatives Array dekodieren

Mit file_get_contents($dataFile) wird der komplette Inhalt der JSON-Datei als String eingelesen, anschließend dekodiert json_decode($raw, true) diesen String in ein PHP-Array. Der zweite Parameter true sorgt dafür, dass JSON-Objekte als assoziative Arrays statt als stdClass-Objekte dekodiert werden – das erlaubt den direkten Zugriff über eckige Klammern wie $entry['full'], ohne Objekt-Syntax ($entry->full) zu benötigen.

2.2 Aggregation je Monat
$sums = array_fill(1, 12, 0.0);
$counts = array_fill(1, 12, 0); 

foreach ($entries as $entry) {
        $month = (int) substr($entry['gasDayStart'], 5, 2);
        $sums[$month] += $entry['full'];
        $counts[$month]++;
} // Ende foreach
  1. Initialisierung: Zwei Arrays ($sums für Füllstände mit 0.0, $counts für Anzahl mit 0) werden via array_fill mit den Schlüsseln 1 bis 12 vorbelegt, damit jeder Monat garantiert existiert.
  2. Aggregation: Für gültige Datensätze wird der Monat extrahiert, der Füllstand (full) auf $sums aufaddiert und der Monatscounter in $counts um 1 erhöht.
2.3 Mittelwertbildung
$averages = []; // Ergebnis-Array initialisieren
    for ($month = 1; $month <= 12; $month++) {
        $averages[$month] = $counts[$month] > 0 
        ? round($sums[$month] / $counts[$month], 4) : null; 
    }
  1. Mittelwertberechnung: Eine for-Schleife dividiert für jeden Monat die Summe durch die Anzahl ($sums / $counts) und rundet das Ergebnis auf 4 Nachkommastellen.
  2. Umgang mit fehlenden Daten: Monate ohne Datensätze erhalten den Wert null statt 0.
  3. Visualisierung: Der Wert null sorgt in ECharts für eine saubere Datenlücke im Liniendiagramm, während eine 0 fälschlicherweise als leerer Speicherstand dargestellt würde.
2.4 Berechnung der für das Diagramm notwenigen Felder
$dataFile = __DIR__ . 'history.json';
$monthlyAverages = getCalendarMonthFullAverages($dataFile);
$monthlyAverages2026 = getCalendarMonthFullAverages($dataFile, 2026);

Die zuvor beschriebenen Funktionen werden in einer Funktion getCalendarMonthFullAverages zusammengefasst, denn wir müssen sie einmal für die historischen Daten und einmal für das Jahr 2026 aufrufen.

$months = array();
$values = array();
$values2026 = array();
$diffValues = array(); 

foreach ($monthlyAverages as $monthNumber => $averageAllYears) {
  array_push($months, $monthNames[$monthNumber]);
  array_push($values, $averageAllYears); 
  $val2026 = isset($monthlyAverages2026[$monthNumber]) ?
             monthlyAverages2026[$monthNumber] : null;
  array_push($values2026, $val2026);
  if ($val2026 !== null && $averageAllYears !== null) {
    array_push($diffValues, round($val2026 - $averageAllYears, 2)); }
  else { 
	  array_push($diffValues, null); 
  }
}
  1. Schleifendurchlauf: Eine foreach-Schleife iteriert über alle 12 Monate der Eingabedaten.
  2. Befüllung der Reihen: Per array_push werden der Monatsname, der Gesamtdurchschnitt ($values) sowie der 2026er-Mittelwert ($values2026) in die jeweiligen Arrays übertragen.
  3. Differenzberechnung: Die Differenz zwischen 2026 und dem Gesamtdurchschnitt wird nur berechnet und auf zwei Nachkommastellen gerundet, wenn für beide Werte Daten vorliegen – andernfalls wird null als Platzhalter für Datenlücken eingesetzt.
2.5 Übergabe der Daten an JavaScript

Dieser Teil ist sehr PHP-spezifisch, sollte aber trotzdem an dieser Stelle beschrieben werden, um das ganze Beispiel zu verstehen.

<script>
  <br>
  window.gasData = <?php echo json_encode([<br>'months' => $months,<br>            'values' => $values,<br>            'values2026' => $values2026,<br>            'diffValues' => $diffValues,<br>        ]); ?>;<br>    </script><br><script src="script.js">
</script><br>

2.5.1 Auf dem Server
PHP verarbeitet die JSON-Datei, berechnet die Arrays und stößt auf die Zeile echo json_encode(...). json_encode wandelt das PHP-Array in ein valides JavaScript-Objekt um.

PHP wird auf dem Server ausgeführt und wenn das Skript durchgelaufen ist, wird fertiger HTML-Code an den Browser geliefert. Aus dem PHP-Code

window.gasData = <?php echo json_encode([<br>    'months' => ['Januar', 'Februar'],<br>    'values' => [45.2, 50.1]<br>]);<br>

generiert der PHP-Interpreter den folgenden Text für die HTML-Ausgabe

window.gasData = {"months":["Januar","Februar"],"values":[45.2,50.1]};

2.5.2 Die Übertragung
Der Server schickt das fertige HTML-/JS-Dokument an den Browser. Der PHP-Code ist darin vollständig verschwunden.

2.5.3 Im Browser
Wenn der Browser die Seite von oben nach unten liest und ausführt:

  1. Erstellt er die globale Variable window.gasData und füllt sie mit dem von PHP geschriebenen Objekt.
  2. Lädt er direkt danach die script.js.
  3. Da script.js erst nach diesem Block geladen wird, kann es problemlos auf window.gasData zugreifen und die Werte an ECharts übergeben:
console.log(window.gasData.months);

3. Die Darstellung als Diagramm

3.1 Basis-Darstellung

Zunächst werden die Daten des übergebenen Objekts dekonstruiert:

const { months, values, values2026, diffValues } = window.gasData;

Dann wird das DOM-Element, am dem das Diagram dargestellt wird, ermittelt und das ECharts-Objekt dort initialisiert:

const chartDom = document.getElementById('chart');
const chart = echarts.init(chartDom);

Anschließend werden die Optionen des Diagramms gesetzt:

chart.setOption({ // Konfiguration für erstes Diagramm setzen
        backgroundColor: '#fff', // weißer Diagrammhintergrund
        legend: { // Legenden-Konfiguration
            data: ['Ø full (alle Jahre)', 'Ø full (2026)']
        },
        
        xAxis: { // X-Achsen-Konfiguration
            type: 'category', // Kategorie-Achse verwenden
            data: months // Monatsnamen als Achsenbeschriftung
        },

        yAxis: {
            type: 'value', // Werteachse verwenden
            name: '%' // Achsenbeschriftung Prozent
        },

        series: [ // Datenreihen des Diagramms
            { // erste Datenreihe
                name: 'Ø full (alle Jahre)', // Name der Reihe
                type: 'line', // Liniendiagramm
                smooth: true, // Linie geglättet darstellen
                data: values, // Werte der Reihe
                itemStyle: { color: '#9bca3c' }, // Punktfarbe
                lineStyle: { color: '#9bca3c' } // Linienfarbe
            }, // Ende erste Datenreihe

            { // zweite Datenreihe
                name: 'Ø full (2026)', // Name der Reihe
                type: 'line', // Liniendiagramm
                smooth: true, // Linie geglättet darstellen
                data: values2026, // Werte der Reihe
                itemStyle: { color: '#5182e4' }, // Punktfarbe
                lineStyle: { color: '#5182e4' } // Linienfarbe
            }, // Ende zweite Datenreihe

        ] // Ende series
    });

Das zweite Diagramm wird auf die gleiche Weise initialisiert, nur die Werte stammen aus diffValues.

3.2 Erweiterung der Darstellung

Man erkennt, dass die Werte des Jahres 2026 ca. 25 bis 30 Prozent unter denen des historischen Mittels liegt. Das ist aber nur grob abgeschätzt. Man kann zur genaueren Bestimmung der Unterschiede die Differenzen direkt in das Diagramm einzeichnen:

Original-DiagrammProgrammcode bei GitHub

Die zugefügten Linien nenn sich „range lines“. Sie werden in das Feld series in den Chart-Optionen eingefügt (ausgelassener Code ist mit ‚…‘ gekennzeichnet):

rangeChart.setOption({ // Konfiguration für Differenzlinien-Diagramm setzen
 ...
  series: [ // Datenreihen des Diagramms
     ...
     { // erste Datenreihe: Ø alle Jahre
       ...
       markLine: { // Verbindungslinien mit Differenzbeschriftung
          silent: true, // keine eigene Interaktion/Tooltip für die Linien
          symbol: 'none', // keine Pfeilspitzen an den Enden
          lineStyle: { color: '#999', type: 'dashed' }, // dezente, gestrichelte Linie
          label: { // Beschriftung mit dem Differenzwert
            formatter: '{c}', // zeigt den 'value' des Startpunkts
            position: 'middle', // Beschriftung mittig auf der Linie
            color: '#333', // Textfarbe
            fontSize: 11 // Textgröße
          }, // Ende label
          data: months // je Monat eine Linie zwischen den beiden Punkten
           .map((_, i) => [ // Start- und Endpunkt der Linie
             { coord: [i, values[i]], value: diffValues[i] }, // Startpunkt mit Differenzwert
               { coord: [i, values2026[i]] } // Endpunkt
             ])
           .filter((_, i) => diffValues[i] !== null) // Monate ohne 2026-Wert auslassen
          } // Ende markLine
        }, // Ende erste Datenreihe
	    ...	
      ] // Ende series
    }); // Ende rangeChart.setOption

… und nun?

Aus diesen reinen Datendarstellungen Gründe abzuleiten, warum die Füllstände so niedrig sind, ist nicht möglich. Dafür müssen weitere Daten einbezogen und der Zusammenhang mit Wirtschaft, Gesellschaft und Politik hergestellt werden.

Wie das geht sehen wir in Teil 3: Daten und Politik.

Comments

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Falls du auf diesen Beitrag mit einem Artikel auf deiner eigenen Webseite geantwortet hast, kannst du hier die URL deines Beitrags eingeben. Dabei sollte es sich um die Permalink-URL handeln. Deine Antwort wird dann (möglicherweise nach der Moderation) auf dieser Seite angezeigt. Falls du deine Antwort aktualisieren oder entfernen möchtest, aktualisiere oder lösche deinen Beitrag auf deiner eigenen Webseite und gib die URL des Beitrags erneut ein. (Erfahre mehr über Webmentions.)