Schlüsselwörter

figure a

Die ersten Schritte der automatisierten Datenerhebung und -analyse lassen sich noch gut in einzelnen Skripten auf dem eigenen Computer umsetzen. Wenn Projekte über einen längeren Zeitraum laufen oder zwischen verschiedenen Beteiligten verteilt sind, sammeln sich jedoch schnell umfangreiche Datenbestände und Skripte an. Bei der Koordination solcher Projekte helfen Versionsverwaltungen wie Git, in denen alle Änderungen erfasst und miteinander abgeglichen werden (Abschn. 6.1). Umfangreicher Code wird im Laufe von Projekten häufig zunehmend modularisiert. Das heißt, aus einzelnen Codeblöcken entstehen in sich geschlossene Bausteine, die meist auf mehrere Skripte mit unterschiedlichen Funktionen aufgeteilt werden. Entwicklungsumgebungen wie RStudio (RStudio 2022a) oder PyCharm (JetBrains 2022a) stellen dabei eine Vielzahl an nützlichen Werkzeugen bereit, um die Übersicht zu behalten, den Code zu dokumentieren und auf Fehlersuche zu gehen (Abschn. 6.2). Damit die entwickelten Skripte auch auf anderen Computern oder Servern im Internet laufen, können Laufzeitumgebungen festgelegt und untereinander geteilt werden, sodass zum Beispiel alle Beteiligten über die gleiche virtuelle Maschine verfügen (Abschn. 6.3). Spätestens wenn Datensätze zu groß oder Datenanalysen zu rechenintensiv für den eigenen Computer werden, führt der Weg schließlich in die Cloud, das heißt ein Projekt wird auf einer verteilten Serverinfrastruktur ausgeführt (Abschn. 6.4).

Die folgenden Kapitel führen kurz in die jeweiligen Techniken zur Organisation von Daten, Skripten und Ressourcen ein. Wenn man sich das erste Mal damit beschäftigt, ist diese Welt ziemlich abstrakt. Beispielsweise werden Dateien, um sie an andere Wissenschaftler:innen weiterzugeben, nicht mehr kopiert, sondern geklont – und sind somit an verschiedenen Orten gleichzeitig aktuell. R- oder Python-Skripte werden nicht mehr auf der Hardware des eigenen Computers ausgeführt, sondern in einer virtuellen Maschine mit einem eigenen Betriebssystem. Das nachfolgende Kapitel bietet keine vollumfängliche Einführung in die jeweiligen Techniken, vielmehr werden diese konzeptionell besprochen. Versuchen Sie sich beim Lesen des Textes bzw. beim Ausführen der Skripte eine kognitive Landkarte aufzubauen und halten Sie sich vor Augen, wie die unterschiedlichen Konzepte zusammenspielen. Am Ende des Kapitels sollten Sie eine Vorstellung davon haben, wie Computational Methods organisiert werden, sobald Projekte an Größe gewinnen.

1 Versionsverwaltung

Systeme zur Versionsverwaltung sind hilfreich, um einzelne Arbeitsschritte zu dokumentieren und Zwischenstände zu speichern. Dadurch lassen sich Entwicklungsschritte im Nachhinein leichter nachvollziehen und verschiedene Versionen des Arbeitsstandes können jederzeit wiederhergestellt werden. Auch wenn man gemeinsam an Projekten arbeitet, können über mehrere Computer hinweg Arbeitsstände über die Versionsverwaltung ausgetauscht werden. Wenn mehrere Personen parallel an den gleichen Skripten arbeiten, übernimmt die Versionsverwaltung anschließend das Zusammenführen der unterschiedlichen Versionen.

Verbreitet ist vor allem die Versionsverwaltung Git. Git kann vollständig über die Kommandozeile bedient werden (siehe Abschn. 1.2.2 zur Arbeit mit der Kommandozeile). Dazu installieren Sie zunächst die auf der Download-Seite für Ihr Betriebssystem passende Version.Footnote 1 Die Vielfalt der Kommandos kann am Anfang verwirrend sein.Footnote 2 Sie müssen die folgenden Konzepte nicht sofort im Detail verstehen, viele dieser Vorgänge erschließen sich nach und nach:

  • Init: Zunächst wird ein lokales Repositorium eingerichtet. Das bedeutet, ein Verzeichnis auf dem eigenen Computer wird unter Versionsverwaltung gestellt. Dafür navigieren Sie mit der Kommandozeile zu dem entsprechenden Verzeichnis und führen den Befehl git init aus. Dadurch wird ein verstecktes Verzeichnis mit dem Namen .git angelegt, in dem die Versionsverwaltung Änderungen speichert. Dieses Verzeichnis sollte nicht angetastet werden, außer in einem Fall: Wenn Sie es löschen, ist die Versionsverwaltung wieder aufgehoben.

  • Commit: Nachdem Dateien im Arbeitsverzeichnis erstellt und bearbeitet wurden, fügt der Befehl git add <dateiname> die anstelle des Platzhalters <dateiname> angegebene Datei zur Versionsverwaltung hinzu. Mit git add . können auch einfach alle geänderten Dateien aufgenommen werden, beachten Sie den Punkt am Ende des Befehls. Anschließend führt man einen Commit durch. Dieser fixiert den Zwischenstand, wobei nur die geänderten Zeilen in den Dateien gespeichert werden. Jeder Commit bekommt automatisch eine eindeutige Nummer, sodass man später darauf zurückgreifen kann. Außerdem formuliert man eine Commit Message, in der die letzten Änderungen kurz erläutert werden. Auf der Kommandozeile wird dies über git commit -m <message> umgesetzt, wobei <message> in Anführungszeichen gesetzt und durch eine kurze Erläuterung ersetzt wird.

  • Branch: Man kann mehrere Versionen parallel bearbeiten, indem man einen Branch erzeugt. Zu Beginn steht lediglich ein main-BranchFootnote 3 zur Verfügung. Vor allem vor umfangreichen Änderungen, zum Beispiel bei der Entwicklung einer neuen Funktion, sollte ein neuer Branch angelegt werden: git branch <name>. Der Platzhalter <name> wird durch einen eigenen Namen ersetzt. Zu diesem neuen Branch kann mit git checkout <name> gewechselt werden. Nachdem Änderungen an den Dateien committed sind, kann man mit git checkout main wieder zurückwechseln.Footnote 4

  • Merge: Ist die Arbeit auf anderen Branches abgeschlossen, können die Änderungen in den main-Branch übertragen werden. Vor dem sogenannten Mergen wechselt man auf den Zielbranch, in der Regel main. Mit git merge <name> wird dann der Stand aus dem mit <name> angegebenen Branch übertragen. Normalerweise ist es kein Problem, wenn zwischenzeitig auch im main-Branch Änderungen vorgenommen werden. Denn die Übertragung erfolgt zeilenweise, das heißt, es werden immer veränderte Zeilen gelöscht und neu eingefügt (Abb. 6.1). Eine Alternative zum Mergen ist das Rebasen, bei dem die Versionsgeschichte so umgeschrieben wird, dass alle Commits eines Branches hinter die Commits des anderen Branches verschoben werden. Bei Änderungen der Versionsgeschichte sind allerdings Absprachen im Entwicklungsteam nötig.

  • Konflikte auflösen: Nur wenn die gleichen Zeilen geändert wurden, kommt es beim Mergen zu einem Konflikt. In diesem Fall wird das Mergen angehalten und in der betroffenen Datei werden beide Versionen eingetragen. Mit einem Texteditor kann die entsprechende Datei geöffnet werden, um nach Markierungen wie HEAD oder MAIN zu suchen, wobei HEAD die Version im aktuellen lokalen Branch kennzeichnet. Hier muss dann manuell entschieden werden, welche Zeilen beibehalten werden. Die nicht mehr benötigten Zeilen und die Markierungen werden gelöscht und die Datei committed, um den Merge abzuschließen.

  • Status: Mit git status erhalten Sie einen Überblick über den Zustand des Repositoriums und sehen beispielsweise, ob noch Konflikte vorliegen. Hilfreich sind zudem git diff, um Änderungen der Dateien und git log, um die Versionsgeschichte anzuzeigen.

Versionsverwaltungen ermöglichen vor allem die Zusammenarbeit mit mehreren Personen an den gleichen Skripten. Dazu werden die Zwischenstände auf eine gemeinsame Plattform wie GitHubFootnote 5 oder GitLabFootnote 6 hochgeladen und miteinander abgeglichen. Insbesondere Open-Source-Programme werden über diese Plattformen koordiniert. Dabei sind vor allem drei Konzepte wichtig:

  • Clone: Mit git clone <URL> wird ein komplettes Repositorium von der Plattform in das lokale Arbeitsverzeichnis, in dem man sich auf der Kommandozeile befindet, übertragen. Hinter dem Befehl kann noch der Name eines Unterverzeichnisses angegeben werden, in den das Repositorium heruntergeladen werden soll. Ohne Angabe wird automatisch der Name des Repositoriums zum Erstellen eines Unterverzeichnisses verwendet. Soll kein Unterverzeichnis entstehen, kann ein Punkt . angehängt werden (siehe für ein Beispiel Kap. 1).

  • Push: Über git push werden Änderungen zur ursprünglichen Quelle hochgeladen. Diese Quelle ist das Online-Repositorium, aus dem die Datei geklont wurde. Um darauf zuzugreifen, benötigt man ein Konto, zum Beispiel bei GitHub. Der Besitzer oder die Besitzerin des Repositoriums muss außerdem die entsprechenden Rechte freigeben. Bei neuen Projekten, die noch nicht veröffentlicht wurden, legt man auf dem Server zunächst ein eigenes Repositorium an, fügt die URL zum lokalen Repositorium mittels git remote add origin <URL> hinzu und kann anschließend die Änderungen auf den Server pushen.

  • Pull: Wenn man mit anderen zusammenarbeitet oder von verschiedenen Computern aus arbeitet, kann man sich über git pull alle aktuellen Änderungen vom Server herunterladen und lokal einspielen. Dieser Schritt muss auch immer geschehen, bevor man eigene Änderungen hochlädt. Achtung: Änderungen, die noch nicht committed sind, werden durch den pull-Befehl überschrieben. Verschaffen Sie sich vorher stets mit git status einen Überblick über den Zustand des lokalen Repositoriums.

  • Pull Request: Will man zu einem Projekt beitragen, zu dem man keinen Schreibzugriff hat, kann man das Repositorium auf den eigenen Computer klonen, verändern und dann einen Pull Request stellen. Der Inhaber oder die Inhaberin des originalen Repositoriums erhält eine Nachricht und kann die Änderungen ggf. übernehmen. Wie dabei vorgegangen wird, ist in der Hilfe der jeweiligen Plattform (zum Beispiel GitHub oder GitLab) beschrieben.

Es müssen nicht immer alle Dateien in einem Verzeichnis unter Versionsverwaltung gestellt werden und vor allem große binäre Dateien, wenn es sich also nicht um Texte, sondern zum Beispiel um Videos oder Bilder handelt, sollten tendenziell nicht in eine Versionsverwaltung aufgenommen werden. Solche Ausnahmen können in der Datei .gitignore festgelegt werden. Wenn Sie auf Plattformen wie GitHub oder GitLab ein Repositorium anlegen, können Sie in der Regel aus entsprechenden Vorlagen auswählen.

Abb. 6.1
figure 1

Beispiel für einen Commit, bei dem eine Zeile verändert wurde. Quelle: Jünger und Keyling (2015; https://github.com/strohne/Facepager/commit/897d000747ec0b4b5d393d0bff86a89a59d617f0#diff-40fe3da5e33280189b65195d7cb0220d))

Für den Einstieg in die Arbeit mit Versionsverwaltungen ist – zusätzlich zur Installation von Git – ein grafischer Client hilfreich, insbesondere um vor dem Commit und Push die Änderungen noch einmal zu überprüfen oder die Historie der letzten Änderungen nachzuvollziehen. Mit einem solchen Client erlernt man zudem nach und nach die Konzepte und Befehle. Bei der Installation von Git wird in der Regel bereits ein passender Client mitgeliefert. Eine einfache Alternative ist GitHub DesktopFootnote 7 (Abb. 6.2).

Abb. 6.2
figure 2

Die wichtigsten Funktionen von GitHub Desktop. (Quelle: eigene Darstellung)

In GitHub Desktop lässt sich über das File-Menü ein neues Repositorium erstellen (New Repository) oder ein vorhandenes Repositorium verwalten (Add Local Repository). Außerdem kann ein Repositorium von GitHub oder einer anderen Plattform heruntergeladen werden. Dafür wählt man im File-Menü den Punkt Clone Repository aus. Dort sind über den Reiter URL anschließend Felder verfügbar, um die Adresse des Repositoriums und das Arbeitsverzeichnis einzutragen. Zuletzt wird das Repositorium über Clone heruntergeladen und in die lokale Ordnerstruktur eingebunden.

Mit der Schaltfläche Fetch werden das lokale und das Online-Repositorium automatisch durch Pull und Push abgeglichen – die Bezeichnung der Schaltfläche ändert sich entsprechend. Nach dem Bearbeiten von Ordnern und Dateien kann man eigene Änderungen über die Schaltfläche Commit eintragen. Dafür beschreibt man zunächst in dem Summary-Feld die Änderungen, bevor diese in den main-Branch committet werden können. Über die Schaltfläche Push werden die Änderungen abschließend in das Online-Repositorium übertragen. Falls es dort zwischenzeitig Änderungen gab, muss man vorher die aktuelle Online-Version über Pull herunterladen und mergen, anschließend wird der eigene Stand über erneutes Klicken zur Primärquelle gepusht.

Auch wenn die Vielzahl der Funktionen anfänglich erschlagend sein kann, lohnt sich der Einstieg in Versionsverwaltungen auf lange Sicht. Sie müssen (außer in sehr speziellen Fällen) keine Sorge haben, etwas zu zerstören und können sich nach und nach einarbeiten.

Übungsfragen

  1. 1.

    Was bedeutet Klonen in der Versionsverwaltung Git?

  2. 2.

    Was passiert in einer Versionsverwaltung, wenn zwei Personen gleichzeitig eine Datei ändern?

  3. 3.

    Was ist der Unterschied zwischen den Befehlen git pull und git push?

  4. 4.

    Mit welchen Befehlen tragen Sie geänderte Dateien in die Versionsverwaltung ein und laden diese in ein Online-Repositorium hoch?

2 Entwicklungsumgebungen

Für den Start mit Computational Methods reicht ein einfacher Texteditor aus: In einer Textdatei werden die Anweisungen an den Computer Zeile für Zeile eingegeben, diese Datei wird dann zum Beispiel über die Kommandozeile an R oder an Python zum Abarbeiten übergeben und das Ergebnis wird am Bildschirm oder in einer neuen Datei ausgegeben. Dieses Input-Throughput-Output-Verfahren setzt voraus, dass man den Programmablauf konzeptionell vordenkt, vollständig verschriftlicht und die Maschine schließlich fehlerfrei arbeitet. Eine solche Perfektion ist allerdings weder realistisch noch erstrebenswert. Denn viele Ideen entwickeln sich erst schrittweise im Verlauf der Analyse, zum Dataminingprozess siehe Kap. 4, und Fehler sind immer einprogrammiert.

Man braucht also eine Möglichkeit, um die Ideen zu organisieren sowie in den Programmablauf und die verarbeiteten Daten hineinzuschauen. Hierbei helfen Integrierte Entwicklungsumgebungen (engl. Integrated Development Environments, IDEs), die man danach unterscheiden kann, ob sie auf eine bestimmte Programmiersprache beschränkt oder universell einsetzbar sind. Einige Entwicklungsumgebungen sind in den Kapiteln zu R (Abschn. 5.1) und Python (Abschn. 5.2) bereits eingeführt worden. Während RStudio auf R zugeschnitten ist (RStudio 2022a), sind PyCharm (JetBrains 2022a) und Spyder (Spyder 2022) für die Entwicklung mit Python ausgelegt. Alle diese Umgebungen stellen auch Funktionen zum Umgang mit Datenbanken, zur Bedienung von Terminals oder zum Erzeugen von Berichten mit Markdown zur Verfügung. Universeller sind dagegen Umgebungen wie Jupyter Lab (wenn auch meist in Kombination mit Python verwendet; Jupyter 2022), Eclipse (auch wenn es aus der Java-Welt kommt; Eclipse Foundation 2022) oder Visual Studio Code (Microsoft 2022b). Die genannten Entwicklungsumgebungen sind zumindest in den Basisversionen für akademische Zwecke kostenlos erhältlich oder stehen sogar unter Open-Source-Lizenzen.

Diese und andere Entwicklungsumgebungen vereinen eine Vielzahl von Hilfsmitteln unter einem Dach:

  • Projektorganisation: Damit nicht der gesamte Code in einer einzigen, vermutlich mit der Zeit sehr unübersichtlichen Datei steht, wird er auf mehrere Dateien aufgeteilt. Wenn sich mehrere Dateien aufeinander beziehen, es wird etwa eine Variable aus einer anderen Datei verwendet, dann kann leicht zu den entsprechenden Stellen im Code gesprungen werden. In einer Entwicklungsumgebung werden alle Dateien eines Projekts nicht nur aufgelistet, sondern können auf einmal durchsucht werden, zum Beispiel um dateiübergreifend Variablen umzubenennen. Eine gute IDE unterstützt zudem die Refaktorierung des Codes – darunter sind neben Umbenennungen auch Umstrukturierungen zu verstehen, die zwar keine Änderungen der Funktionalität bewirken, aber den Code eleganter, lesbarer oder besser wartbar machen. Viele IDEs integrieren eine Versionsverwaltung (siehe Abschn. 6.1), mit der die Arbeit im Team erleichtert wird.

  • Dokumentation und Coding Style: Was einem im Moment der Entwicklung noch selbstverständlich vorkommt, ist einige Wochen später schon wieder kryptisch geworden. Deshalb sind die Dokumentation von Code und ein einheitlicher Coding Style empfehlenswert (Abb. 6.3). Zur Dokumentation von Funktionen in Python haben sich beispielsweise sogenannte Docstrings etabliert, in denen vor jeder Funktion eine Beschreibung, die Input- und die Rückgabewerte festgehalten werden. Ein Coding Style legt zudem fest, welche Konventionen bei der Benennung von Variablen oder der Definition von Funktionen eingehalten werden sollten. IDEs unterstützen dabei, entsprechende Standards einzuhalten, indem sie – wie die automatisierte Rechtschreibprüfung in einer Textverarbeitungssoftware – auf Regelverstöße aufmerksam machen oder Platzhalter für die Dokumentation einfügen.

  • Coding: Nicht zu unterschätzen ist die Unterstützung beim Schreiben von Code. Entwicklungsumgebungen bringen eine Syntaxhervorhebung mit, durch die fehlerhafte Befehle oder Klammern schneller sichtbar werden. Ebenso weisen sie darauf hin, wenn verwendete Objekte noch nicht definiert sind oder definierte Objekte nicht verwendet werden. Daneben vervollständigen sie nach dem Eintippen der ersten Buchstaben automatisch Befehle, zeigen die Hilfe zu einer Funktion an oder fügen automatisch Grundgerüste für Funktionen und Kontrollstrukturen ein.

  • Debugging: Unter Debugging wird die Fehlersuche in Programmcode verstanden. Einige Fehler sind so knifflig, dass sie nur schwer aufzuspüren sind. Mit einem Debugger kann man den Code Zeile für Zeile abarbeiten und dabei zusehen, wie sich die Daten (bzw. Variablen und Objekte) verändern. So werden die einzelnen Programmierschritte kontrolliert und diejenigen Stellen enttarnt, an denen sich Fehler eingeschlichen haben. Gerade in komplexen Programmabläufen ist es hilfreich, einen Breakpoint zu setzen, der den Code entweder an einer bestimmten Zeile oder wenn eine Bedingung eingetreten ist – eine Variable wurde zum Beispiel verändert – pausiert und den Debugger an ebendieser Stelle startet.

  • Profiling: Auch wenn Computer viele schematische Aufgaben schneller erledigen als Menschen, kann dies immer noch zu langsam sein. Vergleicht man etwa eine Million Texte miteinander, dann ergeben sich daraus bereits eine Billion Vergleiche. Selbst wenn ein einzelner Vergleich nur eine Millisekunde benötigen würde, müsste man über dreißig Jahre auf das Ergebnis warten. Auch der Arbeitsspeicher wird dabei knapp. Deshalb kann es wichtig werden, die verwendeten Algorithmen zu optimieren. Beim Profiling lässt man das Programm laufen und kann hinterher für jede einzelne Teilfunktion die Laufzeit und den Arbeitsspeicherverbrauch analysieren, um so Flaschenhälse zu identifizieren (Abb. 6.4).

  • Testing: Auch wenn automatisierte Verfahren insofern reliabel sind, dass sie bei gleichem Input das immer gleiche Ergebnis ausgeben, bedeutet dies nicht, dass ein Verfahren auch wirklich valide ist und ausgibt, was es soll. Die Beurteilung darüber liegt bei den Programmierer:innen, gerade bei komplexen Operationen ist die Funktionsweise aber nicht immer gut überschaubar. Um die Qualität des Codes zu sichern und Fehler frühzeitig zu erkennen (vor allem, wenn man umfangreiche Refaktorierungen vornimmt), schreibt man idealerweise zu jeder Funktion mindestens eine zugehörige Testfunktion. Diese führt die Funktion aus, vergleicht sie mit dem zu erwartenden Ergebnis und schlägt Alarm, wenn das Resultat nicht den Erwartungen entspricht. Bei der sogenannten testgetriebenen Entwicklung (engl. test-driven development) werden die Tests vor der Umsetzung der eigentlichen Funktion angelegt. Gerade bei komplexen Berechnungen, etwa der Bestimmung der Ähnlichkeit zwischen zwei Texten, wird vorab der zu erwartende Wert festgelegt und erst dann die konkrete Funktion implementiert. Die Implementierung ist erfolgreich, wenn sie auch tatsächlich das festgelegte Ergebnis ausgibt. Entwicklungsumgebungen helfen bei der Entwicklung und beim Ausführen von Tests.

  • Server-Management: Forschungssoftware umfasst auf der einen Seite kleinere Skripte für die explorative Datenanalyse, die ausschließlich auf dem eigenen Computer ausgeführt werden. Auf der anderen Seite finden sich größere Programme, die auf einem Webserver laufen oder für die Nutzung durch andere Wissenschaftler:innen veröffentlicht werden. Entwicklungsumgebungen unterstützen das Management virtueller Maschinen und Container und das Deployment, das heißt das Ausspielen auf einen Server, zum Beispiel zur Veröffentlichung einer Software (siehe Abschn. 6.3).

Nicht alle diese Funktionen werden bei der Arbeit mit Integrierten Entwicklungsumgebungen von Anfang an benötigt, es lohnt sich aber bei der Wahl der Entwicklungswerkzeuge darauf zu achten, welche Optionen perspektivisch zur Verfügung stehen. Das Angebot bestimmt gewissermaßen die Nachfrage – nach einiger Zeit der Eingewöhnung lassen sich immer mehr hilfreiche Features entdecken. Zum Erlernen von Computational Methods gehört dazu, sich nach und nach Entwicklungsroutinen anzueignen, die man später nicht mehr missen möchte. Zu Beginn sind die grafischen Oberflächen vor allem hilfreich, um den Überblick über das Projekt zu wahren und weil sie viele Hilfestellungen anbieten. Um die möglichen Optionen zu erkunden, kann man sich durch die Menüs klicken, statt in Dokumentationen zu lesen. Achten Sie dennoch von vornherein darauf, sich die Bedienung über die Tastatur anzueignen und halten Sie Ausschau nach Shortcuts. Während Computational Methods die Datenanalyse automatisieren, kann eine Entwicklungsumgebung die Entwicklungsarbeit automatisieren. Spätestens wenn man sich im Klickverhalten wiederholt – weil man zum Beispiel immer wieder auf Run klickt, um ein Skript laufen zu lassen – sollte man auf das dazugehörige Shortcut umsteigen. Auch auf die Shortcuts zum Kommentieren von Text (häufig Strg + / oder Command + /), die Autovervollständigung (Tabulatortaste sowie Strg + Leertaste bzw. Command + Leertaste) und die Hilfe (F1) wollen Sie nach kurzer Zeit nicht mehr verzichten. Im Zeitverlauf tritt die Entwicklungsumgebung dadurch immer mehr in den Hintergrund und die Produktivität nimmt zu. Sie fangen an, die Gedanken in Code umzusetzen und müssen nicht mehr über das „Wie?“, sondern nur noch über das „Was?“ nachdenken.

Abb. 6.3
figure 3

Beispiele für den Coding Standard PEP 8. Die jeweiligen Varianten funktionieren beide. Dennoch ist es für die bessere Lesbarkeit empfehlenswert, sich an einen Standard zu halten. (Quelle: van Rossum et al. (2013; https://peps.python.org/pep-0008/))

Abb. 6.4
figure 4

Profiling eines Skripts. (Quelle: eigene Darstellung)

Einführend wurde bereits darauf hingewiesen, dass sich Entwicklungsumgebungen dahingehend unterscheiden, ob sie auf eine bestimmte Programmiersprache abgestimmt oder ob sie universell einsetzbar sind. Eine weitere Unterscheidung betrifft den Entwicklungsprozess. Bei der interaktiven Entwicklung werden Codezeilen oder kurze Schnipsel direkt ausgeführt, um das Ergebnis zu betrachten. Für wissenschaftliche Zwecke ist dies nützlich, wenn explorative Datenanalyse betrieben wird bzw. ein Skript nach und nach aufgebaut und nur für einmalige Analysen verwendet werden soll. Insbesondere Notebooks, die in JupyterLab für die Programmiersprachen Python, R und Julia eingesetzt werden, fördern diesen Entwicklungsstil und dokumentieren gleichzeitig die Ergebnisse. Bei der nichtinteraktiven Entwicklung werden dagegen erst Funktionen und Klassen (siehe zur objektorientierten Programmierung Kap. 5) aufgebaut, um dann das ganze Programm ablaufen zu lassen. Hier steht die effiziente, elegante und erweiterbare Konstruktion des Programms im Vordergrund. Dieser Stil ist angebracht, sobald ein Programm längerfristig genutzt werden soll – zum Beispiel, weil es eine grafische Benutzeroberfläche hat oder in Datenanalyseprozesse von Organisationen eingebettet ist. Für die nichtinteraktive Entwicklung geeignet sind etwa PyCharm oder Visual Studio Code.

Schließlich sollte man bei der Wahl der Entwicklungswerkzeuge darauf achten, inwiefern die verarbeiteten Daten einsehbar sind. In RStudio sind die aktuell erzeugten Objekte und Datensätze jederzeit gut sichtbar, während sie in JupyterLab erst über print()-Befehle ausgegeben werden müssen oder bei der nichtinteraktiven Entwicklung nur mit dem Debugger (über Breakpoints) zugänglich sind.

Beachten Sie schließlich, dass sich Entwicklungswerkzeuge genauso weiterentwickeln wie die Programmiersprachen und die Verfahren automatisierter Datenerhebung und -analyse. Deshalb haben die genannten Empfehlungen möglicherweise eine geringe Halbwertzeit – erkunden Sie selbst, welche Entwicklungsumgebung für Sie und das Team am besten geeignet ist.

Übungsfragen

  1. 1.

    Was versteht man unter der Refaktorierung von Code?

  2. 2.

    Suchen Sie sich ein selbstgeschriebenes Python-Skript und prüfen Sie, ob es dem Codingstandard PEP 8 entspricht!

  3. 3.

    Finden Sie in einer Entwicklungsumgebung Ihrer Wahl heraus, mit welchem Shortcut Kommentare angelegt werden können und mit welcher Taste ein Befehl automatisch vervollständigt wird!

  4. 4.

    Probieren Sie für eine Programmiersprache Ihrer Wahl das Debugging in einer integrierten Entwicklungsumgebung aus: Setzen Sie einen Breakpoint in einem Skript, starten Sie das Skript und inspizieren Sie die Variablen, während Sie den Code schrittweise ablaufen lassen!

3 Laufzeitumgebungen und virtuelle Boxen

Die Welt der Computer ist vielfältig: In den verschiedenen Geräten sind je andere Prozessoren, Festplatten, Schnittstellen, Grafikkarten und Zubehöre verbaut oder Betriebssysteme installiert. Durch die stetige Weiterentwicklung der Technik sind Computersysteme zudem unbeständig und kurzlebig. Nach einigen Jahren in Gebrauch sind Computer meist schon nicht mehr auf dem aktuellen technischen Stand und werden durch Nachfolgemodelle ersetzt. Die Vielfalt der Computersysteme fördert Innovation, geht aber auch mit einigen Herausforderungen einher. Diesen begegnet man spätestens, sobald man selbst Programme entwickelt, die nicht nur auf dem eigenen Computer laufen sollen. Dies ist beispielsweise der Fall, wenn man ein R- oder Python-Skript im wissenschaftlichen Kontext für andere Wissenschaftler:innen bereitstellen will und sichergehen möchte, dass die eigene Arbeit auch nach Jahren noch reproduzierbar ist – oder weil man eine Forschungssoftware entwickelt, die auf möglichst vielen verschiedenen lokalen oder serverbasierten Systemen gleichermaßen laufen soll.

Um in der vielfältigen und dynamischen Welt der Computersysteme Stabilität zu garantieren, müssen die Laufzeitbedingungen fixiert werden. Eine erste Lösung dafür besteht darin, in Skripten die konkreten Packageversionen anzugeben, sodass man sich später mit einem Paketmanager die gleichen Komponenten noch einmal installieren kann. Unter Python wird dazu etwa der Paketmanager pip verwendet und zusätzlich lassen sich in Python sogenannte virtuelle Umgebungen (engl. virtual environment, kurz venv) einrichten, in denen die für ein Skript benötigten Packages unabhängig von anderen Skripten installiert werden.

Mitunter hängt eine Software aber vom Gesamtsystem ab – etwa, wenn man einen bestimmten Datenbankserver benötigt oder eine Komponente nur für ein spezielles Betriebssystem verfügbar ist. Statt alle benötigten Komponenten einzeln auf einem Computer zu installieren, können in diesem Fall virtuelle Laufzeitumgebungen definiert werden, die von dem eigenen Computersystem losgelöst und auf andere Computer übertragbar sind. Indem ein virtueller Computer im eigenen Computer eingerichtet wird, ergibt sich ein weiterer Vorteil: Man kann sich das Beste aus allen Welten zusammenstellen. So lassen sich etwa auch Linuxprogramme auf Windowscomputern verwenden.Footnote 8 Insbesondere Serveranwendungen wie Datenbankmanagementsysteme oder Webseiten (siehe Kap. 3) sind häufig für Linux entwickelt, sodass sich mit der Virtualisierung zum Beispiel gut SQL-Datenbankserver auf dem eigenen Computer einrichten lassen.

Bei der Umsetzung von Virtualisierung spielen in der Regel vier Schichten zusammen:

  • Das Wirtssystem (Host) ist der eigene Computer, beispielsweise ein PC mit einem Intel-Prozessor, 16GB Arbeitsspeicher und mit einem Betriebssystem wie Windows oder macOS.

  • Auf dem Host läuft ein sogenannter Hypervisor. Mit diesem kann eine virtuelle Maschine erzeugt werden, die einen selbst gewählten Prozessor, Arbeitsspeicher, Festplatten oder auch Bildschirme simuliert.

  • In der virtuellen Maschine wird ein Gastsystem installiert, das auf die Ressourcen des Wirtssystems zugreift. Typischerweise sind das Linuxsysteme wie Ubuntu oder Debian.

  • Zur Einrichtung des Gastsystems wird mitunter Provisionierungssoftware wie AnsibleFootnote 9 verwendet. Die Provisionierung stellt sicher, dass die benötigten Datenbanken eingerichtet werden und Programme und Packages bei allen Nutzenden mit den gleichen Einstellungen vorhanden sind.

Dieser Aufbau führt dazu, dass erstens die Umgebung für eine bestimmte Forschungssoftware von der Umgebung des eigenen Computers isoliert ist und zweitens die identische Umgebung auf verschiedenen Computern hergestellt werden kann. Dabei kommen aktuell vor allem zwei Techniken (oder ähnliche Alternativen) zum Einsatz, Vagrant und Docker. Während mit Vagrant vollständig autonome virtuelle Maschinen eingerichtet werden, erzeugt Docker sogenannte Container. Ein einzelner Container enthält lediglich die konkret benötigten Komponenten, zum Beispiel einen Datenbankserver. Eine vollständige Laufzeitumgebung setzt sich in der Regel aus mehreren spezialisierten Containern zusammen. Hinzu kommt, dass Container stärker in das Wirtssystem eingebettet sind und dadurch schneller starten.

Ein Anwendungsfall für die Virtualisierung mit Vagrant oder Docker besteht darin, einen vollständigen Webserver auf dem eigenen Computer einzurichten. Das ist nicht nur eine Grundlage für die Entwicklung von Webseiten – wenn etwa im wissenschaftlichen Kontext Tools zur Datenaufbereitung im Web bereitgestellt oder geisteswissenschaftliche Editionen historischer Schriften veröffentlicht werden. Virtuelle Boxen lassen sich auch dazu einsetzen, um beispielsweise eine Kopie der Wikipedia inklusive der dazugehörigen Datenbank auf einem Computer zu erstellen, sodass man diese Daten ohne Webscraping oder APIs analysieren kann.Footnote 10

3.1 Der LAMP-Stack

Um einen vollständigen Webserver aufzusetzen, ist eine Reihe an Komponenten nötig. Die in den folgenden Abschnitten vorgestellten Setups sind typischerweise im Kontext der Webentwicklung anzutreffen, aber gleichzeitig auch eine solide Ausgangsbasis für viele weitere Computational-Methods-Szenarien. Eine klassische Webseite baut auf dem sogenannten LAMP-Stack auf, die Abkürzung setzt sich aus vier verbreiteten Open-Source-Anwendungen zusammen (Kunze 1998):

  • Linux: Webserver sind ganz normale Computer, die aber häufig ohne Bildschirm und Tastatur auskommen und nicht Windows oder MacOS, sondern verschiedene Varianten von Linux als Betriebssystem verwenden. Für Entwicklungssysteme werden beispielsweise Ubuntu ServerFootnote 11 und AlpineFootnote 12 eingesetzt.

  • Apache: Für die Auslieferung der Webseiten über das Internet wird eine Webserversoftware benötigt. Das Apache-ProjektFootnote 13 hat in diesem Bereich eine lange Tradition: Die dahinterstehende, ehrenamtlich organisierte Apache Software Foundation ist eine der bedeutendsten Organisationen im Bereich der Open-Source-Software.

  • MySQL: Die Daten, aus denen eine Webseite generiert wird, können unter anderem in einer relationalen SQL-Datenbank verwaltet werden (siehe Abschn. 3.6). MySQL oder alternativ die kompatible Open-Source-Software MariaDB sind dafür eine klassische Wahl.Footnote 14

  • PHP: Selbst wenn Sprachen wie Python und JavaScript durchaus in der Webentwicklung beliebt sind und der Tod von PHPFootnote 15 immer wieder vorhergesagt wird, ist diese Sprache nach wie vor die vorherrschende Programmiersprache für Webseiten.Footnote 16

Prinzipiell lassen sich all diese Komponenten einzeln installieren. Um jedoch das Zusammenspiel zu gewährleisten, ist ein recht hoher Konfigurationsaufwand nötig. Vagrant und Docker helfen dabei, den eigenen Computer mit wenig Aufwand in einen vollwertigen Server zu verwandeln. Alle Dateien und Funktionen bleiben dennoch lokal und sind nicht über das Internet erreichbar.

3.2 Vagrant

Für die Koordination des Zusammenspiels von Wirtssystem, Hypervisor und Gastsystem finden sich verschiedene kommerzielle und nichtkommerzielle Programme. Eigenständige virtuelle Maschinen lassen sich beispielsweise mit der Open-Source-Software VirtualBoxFootnote 17 erzeugen, für die Einrichtung einer solchen Box ist zudem VagrantFootnote 18 empfehlenswert. Nach der Installation der beiden Komponenten kann mit nur einem einzigen Befehl auf der Kommandozeile eine vollständige Serverumgebung erzeugt werden. Dazu erstellt man mit einem Texteditor eine Konfigurationsdatei mit dem Namen Vagrantfile. Diese Datei kann anschließend an andere Entwickler:innen weitergegeben werden (☛ Repositorium).

Um die einzelnen Bestandteile des Vagrantfile zu verstehen, ist die DokumentationFootnote 19 hilfreich. Im Vagrantfile wird zunächst festgelegt, welche Box verwendet werden soll. Eine Box enthält das benötigte Betriebssystem, also das Gastsystem, sie wird häufig von anderen Anbietern heruntergeladen, statt dass sie selbst erstellt wird. Im folgenden Beispiel wird scotch/boxFootnote 20 verwendet. Diese vorbereitete Box enthält ein Ubuntu-System mit allen Komponenten, die für die Webentwicklung benötigt werden, insbesondere einen PHP-Webserver und einen mySQL-Server:Footnote 21

Vagrant.configure("2") do |config| config.vm.box = "scotch/box" config.vm.network "private_network", ip: "192.168.33.10" config.vm.hostname = "scotchbox" config.vm.synced_folder ".", "/var/www ", :mount_options =>; ["dmode=777", "fmode=666"] end

Wenn Sie Vagrant installiert und eine Vagrant-Datei erstellt haben, starten und initialisieren Sie die Box ausgehend von dem Ordner mit dem Vagrantfile mit nur einem Befehl auf der Kommandozeile (siehe Abschn. 1.2.2):

vagrant up

Damit Skripte und Daten zwischen dem Wirts- und dem Gastsystem ausgetauscht werden können, werden Ordner zwischen beiden Systemen synchronisiert (synced_folder). Im Beispiel wird durch die Angabe des Punktes der Ordner, in dem das Vagrantfile auf dem Wirtssystem liegt, mit dem Ordner /var/www innerhalb des Gastsystems synchronisiert. Legt man dort ein Skript ab – die Entwicklung kann auf dem Wirtssystem erfolgen –, so kann es über den Webbrowser aufgerufen werden. Dazu gibt man in die Adressleiste des Browsers die im Vagrantfile vorgegebene IP-Adresse http://192.168.33.10 ein.Footnote 22 Die Box ist so weit vorkonfiguriert, dass sich in der Box beispielsweise Content-Management-Systeme wie WordPress nutzen lassen. Auf der Webseite von ScotchBox und im ☛ Repositorium des Buchs finden Sie eine kurze Anleitung zur Einrichtung der Box mit einer Miniwebseite.

3.3 Docker

Eine Alternative zur Einrichtung von vollständigen virtuellen Maschinen bietet Docker.Footnote 23 Die Software kann auf der Download-Seite von DockerFootnote 24 heruntergeladen und installiert werden.Footnote 25 Unter Windows wird zusätzlich das Windows Subsystem for Linux (WSL) benötigt.Footnote 26 Auch hier gilt: man muss ein wenig Erfahrung sammeln, um die vielen Begriffe aus der Welt der DevOps – mit diesem Begriff wird das Zusammenspiel von Softwareentwicklung und IT-Infrastruktur bezeichnet – zu verstehen.

Zentral bei der Arbeit mit Docker sind die sogenannten Container. Docker-Container verhalten sich wie virtuelle Computer, stellen anders als bei der Arbeit mit Vagrant aber immer nur einzelne Dienste zur Verfügung, beispielsweise in einem Container einen Webserver oder in einem anderen Container einen Datenbankserver. Diese Container bauen auf Images mit Betriebssystemen wie Ubuntu auf und fügen die für einen Dienst nötigen Schichten und Pakete hinzu. Ein fertiger Container kann wiederum als Image abgespeichert und an andere Entwickler:innen weitergegeben werden. Das Image kann sowohl direkt zum Starten von Containern verwendet werden, als auch um wiederum andere Images mit weiteren Funktionen abzuleiten. Fertige Images werden über sogenannte Registries verwaltet, beispielsweise in der offiziellen Docker-Registry.Footnote 27

Ein Docker-Container wird durch eine Datei mit dem Namen Dockerfile definiert, sie kann einfach mit einem Texteditor angelegt werden (☛ Repositorium). Diese Datei beginnt mit der Benennung eines Basisimages (FROM) und der Zuweisung eines Arbeitsverzeichnisses innerhalb des Containers (WORKDIR). Anschließend werden Befehle angegeben, die bei der Ersteinrichtung des Containers abgearbeitet werden sollen (RUN). Die im folgenden gekürzten Beispiel verwendeten Linux-BefehleFootnote 28 installieren nach der Aktualisierung des Paketmanagers apt-get einen SQL-Client, damit auf den Datenbankserver zugegriffen werden kann:

FROM php:7.1.2-apache WORKDIR /var/www/html # Add SQL client RUN apt-get update RUN apt-get install -y mysql-client RUN rm -rf /var/lib/apt RUN docker-php-ext-install pdo_mysql

Einzelne über ein Dockerfile definierte Container werden mit dem Befehl docker up gestartet. Eine vollständige Entwicklungsumgebung benötigt jedoch in der Regel mehrere Dienste, die über unterschiedliche Container bereitgestellt werden. Für die klassische Webentwicklung benötigt man mindestens einen Webserver und einen Datenbankserver. Damit man nicht jeden Container einzeln starten und konfigurieren muss, kann man das Zusammenspiel mit Docker Compose orchestrieren. Dazu wird eine Datei docker-compose.yml angelegt und darin festgelegt, welche Container aus welchen Images gestartet werden sollen. Im folgenden Beispiel werden zwei Container verwendet: ein PHP-Webserver server_php und ein SQL-Datenbankserver server_sql. Ein Unterschied zwischen den beiden Containern besteht darin, dass für server_php durch die build-Anweidung ein Dockerfile im Unterordner php verwendet wird, während der SQL-Dienst direkt das MariaDB-Image aus der Docker-Registry verwendet:

services: php: build: ./php/ container_name: "server_php" ports: - "80:80" - "443:443" volumes: - "./html:/var/www/html:rw" networks: - database depends_on: - sql environment: MYSQL_ROOT_PASSWORD: "root" sql: image: mariadb:10.3 container_name: "server_sql" ports: - "3306:3306" networks: - database environment: MYSQL_ROOT_PASSWORD: "root" MYSQL_DATABASE: "devel" networks: database: driver: bridge

Die beiden Container hängen miteinander zusammen: Der Webserver wird nur gestartet, wenn auch der Datenbankserver vorhanden ist (depends_on-Anweisung). Die beiden Container tauschen sich dann über ein Docker-internes Netzwerk (networks-Anweisung) mit dem Namen „database“ untereinander aus. Während im Datenbankcontainer eine Datenbank mit dem Namen „devel“ erzeugt wird, kann vom PHP-Container mit dem Benutzernamen „root“ und dem Passwort „root“ auf diese Datenbank zugegriffen werden (environment-Anweisung) – deshalb wurde im oben besprochenen Dockerfile der SQL-Client installiert.

Bislang ist eine in sich geschlossene Containerwelt beschrieben worden – wie kann vom Wirtssystem auf diese Server zugegriffen werden? Hier sind zwei Wege zu unterscheiden, einerseits der Netzwerkzugriff und andererseits der Zugriff auf Dateien. Ausgehend vom Wirtssystem gelingt der Netzwerkzugriff auf den Webserver über den Browser mit der Adresse http://localhost. Dazu wurde in der Konfigurationsdatei eine Weiterleitung von Port 80 des Wirtssystems auf den Port 80 des Gastsystems festgelegt (ports-Anweisung). Ports sind Türen im Netzwerk, durch die Daten zwischen verschiedenen Computern übertragen werden, seien es physische oder virtuelle Computer. Der Port 80 wird im Internet dazu verwendet, Webserver bereitzustellen. Der SQL-Server stellt seine Dienste dagegen unter dem Port 3306 zur Verfügung. Jeder Server kann Ports mit nahezu beliebigen Nummern öffnen: Auf einem Server ist immer ein bestimmter Dienst mit einem einzelnen Port verbunden. Die im Beispiel verwendeten Nummern sind die Standardports für Web- und Datenbankserver. Im hier verfolgten Szenario sind diese Ports aber nur lokal auf dem eigenen Computer erreichbar, dafür sorgt unter anderem die Firewall des eigenen Computers. Für den Dateiaustausch zwischen den Systemen wird das Unterverzeichnis html im Wirtssystem mit dem Verzeichnis /var/www/html im Gastsystem synchronisiert (volumes-Anweisung). Dort können Skripte abgelegt werden, die vom PHP-Server verarbeitet werden. Der Webserver sorgt dann in der Standardkonfiguration dafür, dass beim Aufrufen von http://localhost im Browser das Skript html/index.php auf dem Server ausgeführt wird.

Ein Dockersystem besteht also aus Images, von denen ausgehend Container gestartet werden, die ihre Dienste in Netzwerken bereitstellen und deren Dateien über synchronisierte Ordner erreichbar sind. Wie bei der Arbeit mit Vagrant reicht ein einziger Befehl auf der Kommandozeile (siehe Abschn. 1.2.2). Um das so definierte Ensemble von Containern zum Leben zu erwecken, muss man sich im Ordner der docker-compose.yaml befinden und die Kommandozeile ggf. im Administratormodus starten:

docker compose up -d

Die benötigten Images werden automatisch heruntergeladen bzw. erstellt. Anschließend werden die Container gestartet, mit dem Dateisystem verbunden und miteinander vernetzt. Die Option -d sorgt dafür, dass die Kommandozeile nach dem Starten wieder für weitere Befehle zur Verfügung steht (detached mode). Das kann beim ersten Mal eine Weile dauern, die Zeit reicht für einen Tee oder Kaffee. Schon beim zweiten Mal sollten die Container aber blitzschnell starten – das ist ein Vorteil gegenüber der vollständigen Virtualisierung, wie sie etwa mit Vagrant möglich wäre. Für die weitere Arbeit mit Docker gibt es eine Vielzahl nützlicher Befehle. Um die weiteren Möglichkeiten besser kennenzulernen, empfiehlt sich ein Blick in die Dokumentation von Docker.Footnote 29

3.4 Anwendungsbeispiel: Mit SQL-Datenbanken arbeiten

Angenommen Sie wollen die Wikipedia auf Ihrem Computer auswerten, dann können Sie sich einen vollständigen SQL-Dump herunterladen und für die Analyse auf Ihrem Computer in eine lokale Datenbank einspielen (siehe Abschn. 3.6). Ein SQL-Dump besteht aus Befehlen, um die Tabellen und Daten zu erstellen und kann deshalb auch nur in Verbindung mit einem Datenbankmanagementsystem genutzt werden. Wenn Sie entsprechend des vorangegangenen Abschnitts eine Docker-Umgebung einrichten, dann sind Sie bestens dafür ausgestattet (☛ Repositorium). Die folgenden Schritte sind an einem kleineren Beispiel ausgerichtet, das sich auf andere SQL-Dumps übertragen lässt – es wird ein Ausschnitt aus der International Movie Database in eine SQL-Datenbank eingespielt.

Schritt 1 – Zur Orientierung: Ein- und Auftauchen in den SQL-Server

Bei der Arbeit mit Docker ist zunächst wichtig, dass man versteht, wie die Systeme ineinander verschachtelt sind. Bevor es im nächsten Schritt um das Einlesen der Datenbank geht, wird deshalb zunächst das Ein- und Auftauchen in den Server demonstriert. Öffnen Sie eine Kommandozeile im Docker-Arbeitsverzeichnis und starten Sie die Container:

docker compose up -d

Noch befinden Sie sich im Betriebssystem Ihres Computers, also etwa unter Windows oder MacOS. Von hier aus können Sie in den PHP-Container eintauchen, indem Sie dort eine Kommandozeile starten, typischerweise wird dafür auf Linuxsystemen Bash verwendet:

docker exec -it webdock_php /bin/bash

Sie tippen anschließend zwar im gleichen Fenster, sind tatsächlich aber in einen anderen (virtuellen) Computer eingetaucht. Das erkennen Sie auch daran, dass sich die Darstellung der Kommandozeile leicht verändert hat und nun mit einer Angabe wie root@a74182ba3e1e:/var/www/html# beginnt (Abb. 6.5). Vor dem @-Zeichen steht der Nutzername im Container, es folgen eine Nummer oder ein Name, die den Container identifizieren, und schließlich ist das aktuelle Arbeitsverzeichnis angegeben. Führen Sie den Befehl ls aus, um zu sehen, welche Dateien sich aus Sicht des Containers dort befinden.

Abb. 6.5
figure 5

Ein- und Auftauchen auf der Kommandozeile. (Quelle: eigene Darstellung)

Aus dem PHP-Container können Sie sich nun zum SQL-Server verbinden, auf dem durch das vorgegebene Docker-Setup bereits eine leere Datenbank „devel“ eingerichtet ist:

mysql --host=sql --user=root --password=root devel

Und wieder sind Sie in eine neue Ebene abgetaucht, die Kommandozeile ändert sich dahingehend, dass sie mit mysql> beginnt. Sie befinden sich nun im SQL-Server und können dort Befehle auf der Datenbank ausführen (siehe Abschn. 4.1.4):

SHOW TABLES;

Aktuell ist die Datenbank noch leer, es werden keine Tabellen angezeigt, deshalb wird im nächsten Schritt ein SQL-Dump in diese Datenbank eingespielt. Tauchen Sie zunächst wieder auf, indem Sie den SQL-Server verlassen:

exit

Um wieder zum Ausgangssystem zurückzukehren, geben Sie ein weiteres Mal „exit“ ein:

exit

Bevor Sie weitergehen, vergegenwärtigen Sie sich die verschiedenen Ebenen, auf denen Sie gerade unterwegs waren: das Wirtssystem (z. B. Windows oder Mac), das Gastsystem (Ubuntu im Docker-Container) und der SQL-Server (Abb. 6.6). Der nächste Schritt beginnt noch einmal mit dem Eintauchen in das Gastsystem.

Abb. 6.6
figure 6

Ein- und Auftauchen in den SQL-Server. Die Befehle sind zu Darstellungszwecken umgebrochen. In der Kommandozeile müssen sie auf einer Zeile eingegeben werden. (Quelle: eigene Darstellung)

Schritt 2 – Einspielen des Dumps

Wenn Sie aus dem ☛ Repositorium des Buchs den Ordner zum Kapitel heruntergeladen haben, dann befindet sich im Unterordner dump ein gezippter SQL-Dump (siehe Abschn. 3.6). Öffnen Sie wieder die Kommandozeile, starten Sie Docker und begeben Sie sich in den PHP-Container (siehe oben). Gehen Sie mit folgenden Befehlen in das Verzeichnis mit dem SQL-Dump (cd = change dir) und entpacken Sie die ZIP-Datei mit unzip. Sie können anschließend mit ls kontrollieren, ob eine SQL-Datei vorliegt:

cd /var/dump unzip imdb.zip

Statt dass Sie sich nun zum SQL-Server verbinden und Befehle eingeben, spielen Sie die SQL-Satements mit der Pipe < direkt aus der SQL-Datei in die Datenbank devel ein (alles muss in einer Zeile eingegeben werden):

mysql --host=sql --user=root --password=root devel < /var/dump/imdb.sql

Wenn Sie sich anschließend, wie oben beschrieben, die Tabellen in der Datenbank ausgeben lassen, sollten diese nicht mehr leer sein. Auch wenn Sie im Browser die Adresse http://localhost aufrufen, sollte ein kleiner Ausschnitt an Personen mit Geburts- und Sterbedaten sichtbar werden.

Schritt 3 – Zugriff auf die Daten

Für den Zugriff auf die Daten gibt es viele unterschiedliche Optionen, von grafischen Datenbankprogrammen wie DBeaverFootnote 30 oder HeidiSQLFootnote 31 bis zur Kommandozeile. Sobald der Server läuft, brauchen Sie lediglich die Serveradresse bzw. den Hostnamen („localhost“) sowie Benutzername und Passwort (beides „root“). Suchen Sie sich selbst eines der Programme aus und versuchen Sie damit die Verbindung herzustellen.

Für die Datenanalyse können Sie zudem mit Skripten auf die Datenbank zugreifen. Unter R gelingt das etwa mit dem Package RMySQL (Ooms et al. 2021). Im Zusammenspiel mit dbplyr (Wickham, Girlich & Ruiz 2022; nicht zu verwechseln mit dplyr) können Sie für den Datenabruf auf Tidyverse-kompatible Befehle zurückgreifen, statt SQL-Statements zu formulieren. Zunächst stellen Sie dafür die Verbindung zur Datenbank her:

# Packages library(tidyverse) library(RMySQL) library(dbplyr) # Verbindung herstellen con <- dbConnect( RMySQL::MySQL(), host = "localhost", port = 3306, username = "root", password = "root", dbname = "devel" )

Das daraus hervorgehende Verbindungsobjekt con kann dann mit der tbl()-Funktion zur Auswahl einer Tabelle innerhalb der Datenbank verwendet werden. Mit collect() wird der Inhalt der Tabelle in einem Dataframe für die weitere Analyse abgelegt:

people <- tbl(con, "people") people <- collect(people) count(people, born, sort = T)

Lassen Sie die Zeile mit dem collect()-Befehl einmal weg, die Auszählung sollte dennoch klappen. Denn es ist nicht unbedingt nötig, die Daten vollständig zu laden – wenn Sie Befehle wie filter(), select() oder count() verwenden, kann dbplyr daraus im Hintergrund passende SQL-Abfragen formulieren, die dann auf dem Server ausgeführt werden, ohne alle Daten zu R zu transferieren. Die Daten werden dann automatisch am Ende eingesammelt. Das Package versucht also vor einem collect()-Befehl die Datenaufbereitung auf dem Server durchzuführen, hinter diesem Befehl findet die Auswertung dagegen in R statt.

Mit Python ist die Vorgehensweise ganz ähnlich, wenn Sie die Packages sqlalchemy (Bayer 2012) und pymysql (Matsubara et al. 2016) in Kombination mit pandas (siehe Abschn. 5.2) verwenden. Zunächst wird eine Verbindung hergestellt:

# Packages from sqlalchemy import create_engine import pymysql import pandas as pd # Verbindung herstellen db_str = 'mysql+pymysql://root:root@localhost/devel' db_con = create_engine(db_str)

Die Verbindungsdaten werden hier über einen Verbindungsstring angegeben. Dieser enthält eine Protokollangabe mysql+pymysql, dann vor dem Doppelpunkt den Nutzernamen root und danach das Passwort root sowie nach dem @-Zeichen den Hostnamen localhost und die gewünschte Datenbank devel. Über das Verbindungsobjekt db_con lassen sich nun SQL-Befehle absetzen, um das Ergebnis in einem Dataframe abzulegen:

df = pd.read_sql ('SELECT * FROM people', con = db_con) display(df)

Sie können sich damit beispielsweise an einer Visualisierung der Geburts- und Sterbejahre im Datensatz versuchen. Vergessen Sie nicht, die Datenbankverbindung abschließend mit db_con.dispose() wieder zu beenden!

Übungsfragen

  1. 1.

    Wozu wird die Virtualisierung von Maschinen eingesetzt?

  2. 2.

    Was ist ein LAMP-Stack und wofür wird er eingesetzt? Recherchieren Sie, was die Stacks LEMP, MEAN und XAMPP enthalten!

  3. 3.

    Was unterscheidet Vagrant von Docker?

  4. 4.

    Wie starten Sie Docker-Container?

  5. 5.

    Unter welcher Adresse können Sie mit einem Browser auf einen lokalen Webserver zugreifen?

  6. 6.

    Richten Sie die im ☛ Repositorium des Buchs definierte Vagrant-Box oder die dort vorgegebenen Docker-Container ein!

  7. 7.

    Was ist ein SQL-Dump und wie gehen Sie vor, um damit zu arbeiten?

  8. 8.

    Joinen Sie die Tabellen „people“ und „crew“ im IMDb-Datensatz (☛ Repositorium), um die Anzahl der Schauspieler:innen, Regisseur:innen und Produzent:innen von Filmen nach Jahren auszuzählen!

  9. 9.

    Welche Möglichkeiten kennen Sie, um auf SQL-Datenbanken zuzugreifen?

4 Parallelisierung mit Cores, Clustern und Cloud Computing

Die Zeit eines Menschen ist begrenzt. Und auch ein einzelner Computer schafft es bei einigen Aufgaben nicht, in überschaubarer Zeit zu einer Lösung zu kommen. Ein Grund dafür kann sein, dass eine große Datenmenge zu bearbeiten ist, eine Funktion also zum Beispiel auf Millionen oder Milliarden von Fällen angewendet werden soll. Doch auch kleine Datensätze können je nach Fragestellung schnell zu praktisch unendlichem Zeitaufwand führen. Sollen etwa eintausend Texte alle miteinander verglichen werden, um die Ähnlichkeit festzustellen, ergeben sich daraus bereits 1000 × 1000 = 1 Million Vergleiche – wenn dabei die Wörter einzeln verglichen werden, wird es noch umfangreicher. So ein Textvergleich kann etwa sinnvoll sein, um Duplikate oder automatisiert durch Bots verschickte Spammitteilungen zu entdecken. Eine solche Textvergleichsfunktion benötigt also umso mehr Zeit, je umfangreicher das Korpus ist.

Wenn die Ausführung einzelner Befehle sehr lange braucht, sollte man sich Gedanken über die Optimierung der Skripte machen. Ein erster Ansatzpunkt kann die Aufbereitung der Datengrundlage sein – wenn man weiß, dass im Korpus Kochrezepte und Nachrichtenartikel enthalten sind, müssen nur jeweils die Kochrezepte und die Nachrichtenartikel untereinander verglichen werden. Man könnte also zunächst eine Vorprüfung durchführen und die Texte aufgrund festgelegter Stichwörter in eine der beiden Gruppen einteilen. Doch auch die verwendeten Algorithmen können optimiert werden, etwa indem effizientere Datentypen wie Zahlen statt Texten oder optimierte Sortieralgorithmen eingesetzt werden.

Beim Textvergleich stößt man mitunter noch an eine weitere Grenze: Sehr lange Texte brauchen viel Arbeitsspeicher, in den die Daten für die Verarbeitung hineingeladen werden, und auch dieser ist limitiert. Der Umfang der Ressourcen, das heißt Zeit und Raum, die ein Algorithmus benötigt, wird auch als Komplexität bezeichnet (einführend König et al. 2016, S. 313 ff.). Eine lineare Komplexität bedeutet, dass der Ressourceneinsatz mit jedem weiteren Fall gleichbleibend ansteigt, weil eine einzelne Aufgabe immer die gleiche Zeit benötigt. Bei einem naiven (d. h. nicht optimierten) Textvergleich liegt dagegen bei unterschiedlich großen Korpora quadratische Komplexität vor, jeder weitere Text führt dazu, dass alle Texte mit dem neuen Text abgeglichen werden.

An Ressourcengrenzen kann man somit in Bezug auf den Input (Datensatz) oder den Throughput (angewendete Funktion) einer Analyse stoßen. Aber auch wenn es um den Output (Visualisierung und Bericht) wissenschaftlicher Forschung bzw. die Veröffentlichung geht, spielen die Ressourcen von Computersystemen eine Rolle. Will man etwa eine interaktive Grafik on the fly angepasst an Einstellungen der Nutzenden auf einer Webseite generieren, so greifen mitunter viele Personen gleichzeitig auf diese Seite zu. Es wäre bei aufwendigen Darstellungen kaum hinnehmbar, wenn jede Person so lange auf die eigene Grafik warten müsste, bis alle anderen Nutzenden ihre Grafiken erhalten haben.

Wenn Sie mit einem Problem an solche Grenzen stoßen, die von einzelnen Menschen oder Computern nicht in angemessener Zeit bewältigt werden können, dann beginnt die ‚eigentliche‘ Welt der Computational Methods. Neben der Optimierung von Daten und Funktionen kommt in diesen Fällen vor allem ein Verfahren zur Anwendung: Parallelisierung. Im Bereich der Datenanalyse spricht man auch von Big Data, der Begriff bezeichnet im engeren Sinn Daten, die nicht in ein einzelnes Gerät passen (Cox und Ellsworth 1997), und entsprechend in verteilten und im besten Fall parallelisierten Systemen verarbeitet werden.

Praktisch kann Parallelisierung bereits auf dem eigenen Computer beginnen, aber auch über mehrere Maschinen verteilt werden, es lassen sich drei Kernkonzepte unterscheiden:

  1. 1.

    Cores: Die meisten Computer verfügen über mehrere Recheneinheiten, sogenannte Cores, die parallel arbeiten können. Ein Skript kann auf dem eigenen Computer auf mehrere Cores aufgeteilt werden. Unter Windows können Sie beispielsweise im Taskmanager (Strg + Alt + Entf) im Bereich CPU sehen, über wie viele Cores Ihr Gerät verfügt (Abb. 6.9).

  2. 2.

    Cluster: Skripte lassen sich parallel auf mehreren Computern ausführen, die über ein Netzwerk zu einem Cluster verbunden sind. Bei dieser als High Performance Computing (HPC) bekannten Technologie wird ein Skript über einen Kontrollknoten auf die (virtuellen oder physichen) Knoten (engl. nodes) verteilt. Eine in wissenschaftlichen Einrichtungen häufig anzutreffende Software zur Verteilung der Skripte ist Slurm.

  3. 3.

    Cloud: Bei Cloud-Computing-Anbietern wie Amazon Webservices (AWS) können flexibel Ressourcen gemietet werden, zum Beispiel einzelne Server oder zu einem Cluster verbundene virtuelle Maschinen. Die Ausrüstung der Server kann mit wenigen Mausklicks nach Bedarf umkonfiguriert werden. Solche Lösungen eignen sich nicht nur für High Performance Computing, sondern auch zum Hosten von Anwendungen und Webseiten, etwa für die Publikation interaktiver Auswertungen.

Wenn man das erste Mal in die Welt der Parallelisierung eintaucht, ist das vermutlich eine echte Herausforderung. Wir begeben uns mit den folgenden Beispielen wie Alice im Wunderland ins Rabbit Hole und tauchen Ebene für Ebene immer weiter ab. Wenn Sie beim Lesen am Ende wieder in der Wirklichkeit ankommen, sind Sie auf einem guten Weg.

4.1 Parallelisierung auf mehreren Cores

Die einfachste Form der Parallelisierung können Sie auf dem eigenen Computer ausprobieren. Als Ausgangspunkt dient im Beispiel eine Document-Feature-Matrix, in der die Berichterstattung von Nachrichtenseiten erfasst ist, die laut Reuters Digital News Report im Jahr 2020 am meisten genutzt wurden (Puschmann und Haim 2021).Footnote 32 Diese Document-Feature-Matrix enthält in den Zeilen die Nachrichtenmeldungen (Dokumente) und in den Spalten die Wörter der Texte (Features). In den Zellen ist jeweils eingetragen, wie häufig das Wort im Text vorkommt (Abb. 6.7; siehe Kap. 9). Dadurch wird jede Nachrichtenmeldung durch einen sogenannten Vektor, also die Zahlen in der entsprechenden Zeile, abgebildet. Auf Grundlage der Matrix kann die Ähnlichkeit der Texte berechnet werden, etwa um Duplikate zu identifizieren. Dazu müssen alle Zeilen mit allen Zeilen verglichen werden. Eine einfache Variante zur Messung der Ähnlichkeit von zwei Vektoren stellt die Kosinusähnlichkeit dar (siehe Abschn. 4.2.4). Diese Maßzahl reicht von 1, wenn die Muster in den beiden Vektoren identisch sind, bis 0, wenn sich die Vektoren vollständig unterscheiden.Footnote 33

Abb. 6.7
figure 7

Eine Document-Feature-Matrix. (Quelle: eigene Darstellung, Auszug aus Puschmann und Haim (2021; https://osf.io/uzca3/))

Die Arbeit mit diesem Datensatz benötigt nicht nur Rechenkapazität, sondern zudem eine gute Ausstattung des Arbeitsspeichers, je nach Vorgehensweise werden über 100 GB benötigt. Grundsätzlich ist es deswegen empfehlenswert, die ersten Gehversuche und die Entwicklung von Skripten an kleineren Ausschnitten von Datensätzen vorzunehmen, auch um die Wartezeit zu verkürzen. Die entwickelten prototypischen Skripte können anschließend über Nacht oder auf einem High Performance Cluster die vollständigen Datensätze abarbeiten. Ein Auszug des Datensatzes (☛ Repositorium) kann in R wie folgt eingelesen werden:

# Packages laden library(tidyverse) library(quanteda) library(quanteda.textstats) # Document-Feature-Matrix einlesen dfm <- read_rds("usenews_small.rds") dfm

Die Dokument-Feature-Matrix umfasst hier 10.000 Dokumente und 1428 Wörter. Die folgenden Skripte laufen damit sehr schnell durch, eine Parallelisierung lohnt sich dafür noch nicht. Sie können die Skripte aber anschließend auf den Gesamtdatensatz anwenden.

Im R-Package quanteda (Benoit et al. 2018) findet sich die Funktion textstat_simil(), mit der die Kosinusähnlichkeit zwischen allen Dokumenten berechnet werden kann:

textstat_simil( dfm, margin = "documents", method = "cosine", min_simil = 0.99 ) %>% as_tibble()

Der erste Parameter enthält die Matrix. Anschließend wird angegeben, dass die Dokumente (margin) mit der Kosinusmethode (cosine) verglichen und nur Paare mit einem Ähnlichkeitswert über 0,99 (min_simil) ausgegeben werden sollen. Mit diesem sehr hohen Schwellwert werden fast identische Dokumente ermittelt. Das Ergebnis wird im Beispiel für die bessere Lesbarkeit mit der Funktion as_tibble() in eine Tabelle umgewandelt, die 14 sehr ähnliche Paare ausgibt (Tab. 6.1).

Tab. 6.1 Kosinusähnlichkeit von Nachrichten

Tatsächlich ist das Ermitteln der Kosinusähnlichkeit bereits ein parallelisierter Aufruf gewesen – ohne dass es direkt sichtbar war. Denn das Package quanteda sorgt bei einigen Funktionen selbst dafür, die auf dem Computer verfügbaren Cores bestmöglich auszunutzen. Unter Windows können Sie das beispielsweise im Task-Manager beobachten (Abb. 6.8), die Auslastung aller Prozessoren steigt beim Aufruf von textstat_simil() gleichzeitig an. Allerdings ist dies eine besonders komfortable Situation. Nicht alle Funktionen sind bereits für die Parallelisierung vorbereitet – insbesondere selbstgeschriebene Funktionen mit zusätzlichen Aufbereitungsschritten – und auch, wenn die Berechnung auf ein Computercluster übertragen werden soll, sind weitere Schritte nötig.

Abb. 6.8
figure 8

Die Auslastung der Kerne im Windows Taskmanager. (Quelle: eigene Darstellung)

Um einen Vorgang zu parallelisieren, muss die Gesamtaufgabe in Teile zerlegt werden. Eine Vorgehensweise kann dabei sein, immer nur wenige Zeilen mit der gesamten Matrix zu vergleichen und die Teilergebnisse am Ende wieder zusammenzuführen. Dazu muss die Matrix zunächst unterteilt werden. Wenn insgesamt 10.000 Dokumente vorliegen, können daraus beispielsweise 10 Chunks mit jeweils 1000 Dokumenten gebildet werden. Der folgende Befehl erzeugt zunächst einen Vektor, in dem für jedes der Dokumente zufällig festgelegt ist, zu welchem Chunk es gehören soll. Dazu werden 10.000 Zahlen zufällig aus dem Bereich 1-10 gezogen:Footnote 34

chunks <- sample(10, 10000, replace = TRUE)

Diese Zuteilung kann nun dazu genutzt werden, für jedes Chunk eine eigene Liste zu erzeugen.:

chunks <- split(c(1:10000), chunks)

Die resultierende Liste besteht entsprechend der Chunkanzahl aus zehn Elementen. Jedes der Elemente ist ein Vektor mit den Nummern der zugehörigen Dokumente.

Nach der Aufteilung der Fälle muss nun der Aufruf der Ähnlichkeitsberechnung so umgestaltet werden, dass die Funktion für jedes Chunk extra abläuft. Um eine Funktion auf eine Liste anzuwenden, gibt es mehrere Varianten (siehe Abschn. 5.1) – die Funktion map_dfr() bietet den Vorteil, dass die Teilergebnisse automatisch wieder zu einem Gesamtdatensatz zusammengebunden werden. Übergibt man zunächst die Liste der Chunks, wird die im zweiten Parameter übergebene Funktion für jedes einzelne Chunk ausgeführt. Die im Beispiel eingebettete FunktionFootnote 35 function(chunk) erhält als ersten Parameter den Chunk, für den sie jeweils zuständig ist. Der Aufruf von textstat_simil() unterscheidet sich von der oben angeführten Variante dahingehend, dass die Matrix zweimal übergeben wird, einmal vollständig und einmal nur mit den im Chunk definierten Zeilen. So wird der Vergleich beim Aufruf der Funktion nur für die ausgewählten Zeilen durchgeführt. Indem aber alle Chunks nacheinander abgearbeitet werden, sind am Ende alle Zeilen berücksichtigt worden:

map_dfr( chunks, function (chunk) { textstat_simil( dfm, dfm[chunk,], margin = "documents", method = "cosine", min_simil = 0.9 ) %>% as_tibble() } )

Bislang hat noch keine explizite Parallelisierung stattgefunden, die Abwandlung des Funktionaufrufs diente nur der Vorbereitung. Der Schritt in die Erzeugung von Parallelwelten ist nun aber nahezu trivial. Die Funktion map_dfr() kann dazu einfach gegen die Funktion future_map_dfr() aus dem furrr-Package (Vaughan und Dancho 2022) ausgetauscht werden. Mit der plan()-Funktion wird festgelegt, auf wie viele Worker die Aufgabe verteilt werden soll. Unter einem Worker versteht man einen eigenständigen Prozess, der in diesem Fall auf einem eigenen Prozessorkern läuft und nacheinander die ihm automatisch zugeteilten Chunks abarbeitet.

library(furrr) plan(multicore, workers = 4) future_map_dfr( chunks, function (chunk) { textstat_simil( dfm, dfm[chunk, ], margin = "documents", method = "cosine", min_simil = 0.9 ) %>% as_tibble() } )

Für R und auch für Python gibt es vielfältige Packages und Varianten zur Parallelisierung. Da sich die Möglichkeiten immer weiterentwickeln, lohnt sich eine eigene Recherche. Das Grundprinzip aber bleibt immer gleich: Eine Aufgabe wird in Teile zerlegt und an Worker verteilt, nach dem Abarbeiten werden die Ergebnisse wieder in einem gemeinsamen Datensatz abgelegt.

4.2 Parallelisierung auf einem Cluster

Für besonders umfangreiche Berechnungen können die Worker auf mehrere Computer verteilt werden, auf denen jeweils wieder mehrere Kerne zur Verfügung stehen. Das High-Performance-Computing-Cluster (HPC) der Universität Münster umfasst beispielsweise aktuell über 15.000 Kerne verteilt auf mehrere Hundert Knoten, so werden die einzelnen Computer genannt (Universität Münster 2022). Sofern eine Datenanalyse parallelisiert werden kann, lassen sich damit Berechnungen innerhalb eines Tages durchführen, für die sonst Jahre vergehen würden. Damit dies möglich ist, müssen die Einrichtung des Clusters, Skripte mit den Befehlen zur Berechnung und Anweisungen zum Verteilen der Aufgaben auf dem HPC zusammenspielen. Einen exemplarischen Durchlauf durch diesen Prozess finden Sie im ☛ Repositorium des Buchs. Wichtige Kernpunkte und Konzepte werden nachfolgend eingeführt.

Bei der Verteilung auf ein Cluster ist insbesondere zu bedenken, wie die Daten zugeteilt und wieder eingesammelt werden. Prinzipiell könnten die verschiedenen Worker miteinander kommunizieren, um sich dabei zu koordinieren. Das ist bei der Berechnung der Textähnlichkeit allerdings nicht nötig, da jeder Worker unabhängig von den anderen einen Teildatensatz bearbeiten kann. Stattdessen reicht es in solchen Fällen, wenn alle Worker auf einen gemeinsamen Ordner mit dem Gesamtdatensatz zugreifen können. Der Ablauf kann wie folgt gestaltet werden:

  1. 1.

    Der gesamte Datensatz wird in einen Ordner auf dem Cluster übertragen.

  2. 2.

    Das Skript wird auf das Cluster übertragen und auf jedem Knoten mit einer eigenen Aufgabennummer gestartet. Aus der Aufgabennummer wird abgeleitet, welchen Teildatensatz dieser Knoten bearbeiten soll.

  3. 3.

    Das Skript bearbeitet auf jedem Knoten seine Teilaufgabe und speichert das Ergebnis im gemeinsamen Ordner ab. Die Ergebnisse dürfen sich nicht überschreiben, deshalb wird die Nummer der Aufgabe in den Dateinamen aufgenommen.

  4. 4.

    Sobald alle Teilergebnisse vorliegen, können sie auf den lokalen Computer übertragen und mit einem weiteren Skript zusammengeführt werden. Alternativ kann man sich auf einen einzelnen Knoten des Clusters einloggen und die Zusammenführung dort vornehmen, um dann das Gesamtergebnis auf den eigenen Computer zu holen.

Da sich die Rahmenbedingungen einzelner Cluster durchaus unterscheiden, lässt sich kein allgemeingültiges Beispielskript konstruieren. Die Clusterbetreibenden sind aber in der Regel hilfsbereit, veröffentlichen einführende Materialien oder bieten Schulungen an. Das folgende Schema (☛ Repositorium) dürfte auf einer Vielzahl von Clustern funktionieren, sofern diese passend eingerichtet sind und zur Verteilung der Aufgaben SlurmFootnote 36 einsetzen. Slurm ist eine weitverbreitete Open-Source-Software zum Management von High Performance Clustern, die auch bei kommerziellen Cloudanbietern wie Amazon Web Services verwendet werden kann.

Um auf einem solchen Cluster ein Skript zu starten, öffnet man auf dem eigenen Computer eine Kommandozeile und loggt sich über das Internet per SSH auf dem Cluster ein:Footnote 37

ssh username@palma.uni-muenster.de

Die anschließend eingegebenen Befehle werden auf dem Kontrollknoten des Clusters ausgeführt. Achtung: Der Kontrollknoten ist in der Regel nicht für Berechnungen vorgesehen, wenn er blockiert wird, können die anderen Nutzenden das HPC nicht mehr erreichen. Deshalb wird auf dem Kontrollknoten lediglich ein Slurm-Befehl abgesetzt, der ein vorbereitetes Shellskript auf die anderen Knoten verteilt, welches die dort auszuführenden Befehle enthält:

sbatch ~/slurm_job.sh

In diesem Shellskript, im Beispiel slurm_job.sh, sind zum einen die Einstellungen für Slurm enthalten und zum anderen die Befehle, die vom Worker ausgeführt werden sollen:

#!/bin/bash #SBATCH --array=0-9 # Anzahl der Tasks #SBATCH --cpus-per-task=1 # Anzahl der Cores je Task #SBATCH --mem-per-cpu 32G # Benötigter RAM je Task #SBATCH --partition=normal # Teilbereich des Clusters #SBATCH --time 00:60:00 # Maximale Laufzeit #SBATCH –-oversubscribe # Knoten werden geteilt Rscript ~/slurm_rscript.R

Dieses etwas gekürzte Skript erstellt einen sogenannten Array-Job, bei dem insgesamt 10 Aufgaben auf dem Cluster verteilt werden. Slurm sorgt entsprechend der Angaben dafür, dass für jeden Worker ein Kern und 32GB Arbeitsspeicher zur Verfügung stehen. Mit einem solchen Array-Job und der Oversubscribe-Einstellung können auch Kerne genutzt werden, die andere Nutzenden des Clusters gerade nicht benötigen. Die letzte Zeile sorgt dafür, dass auf dem Knoten R gestartet und das Skript slurm_rscript.R ausgeführt wird.

Innerhalb des R-Skripts kann man sich zunutze machen, dass jeder Task des Array-Jobs eine eigene Nummer erhält, die im Beispiel von 0 bis 9 reicht. Diese Task ID kann aus den sogenannten Umgebungsvariablen ausgelesen werden, um daraus das zugeordnete Chunk zu bestimmen. Mit der folgenden Berechnung bearbeitet der erste Worker die Zeilen 1 bis 1000, der zweite die Zeilen 1001 bis 2000 und so weiter, obwohl alle das gleiche Skript ausführen:

task_id <- as.numeric( Sys.getenv('SLURM_ARRAY_TASK_ID') ) chunk_start <- (task_id * 1000) + 1 chunk_end <- chunk_start + 1000 - 1

Anschließend wird der Datensatz geladen, bearbeitet und das Ergebnis wird im Ordner output abgespeichert:

dfm <- read_rds("usenews_small.rds") sim <- textstat_simil( dfm, dfm[c(chunk_start:chunk_end), ], margin = "documents", method = "cosine", min_simil = 0.99 ) sim %>% as_tibble()%>% write_rds(paste0("output/sim_", task_id))

Haben die Worker ihre Arbeit beendet, dann loggt man sich auf einem einzelnen Knoten (nicht dem Kontrollknoten!) ein oder überträgt die Teilergebnisse auf den eigenen Computer. Dort können die Liste der relevanten Dateinamen mit dir() erstellt, die Dateien mit readRDS() eingelesen und mit map_dfr() zusammengefügt sowie schließlich mit write_rds() abgespeichert werden:

files <- dir( "output", pattern = "sim_.*\\.rds", full.names = TRUE ) sim <- map_dfr(files, readRDS) write_rds(sim, "sim.rds")

Bei der praktischen Umsetzung gibt es viele Stellschrauben, um die Beispiele zu verbessern. Vor der ersten Benutzung müssen etwa die benötigten Packages auf dem Cluster installiert werden und um dies zu vereinfachen, können ContainerFootnote 38 eingesetzt werden (siehe Abschn. 6.3 und ☛ Repositorium).

4.3 Hosting in der Cloud

Während das High Performance Computing auf umfangreiche Berechnungen ausgerichtet ist und Wartezeiten verkürzt, gibt es neben erweiterten Ressourcen viele weitere Gründe zur Nutzung von Cloud Computing. Dafür finden sich je nach Spezialisierung unterschiedliche Typen von Cloud-Anbietern.

Kommerzielle Cloud-Plattformen bieten nützliche APIs an, die sich für spezifische Aufgaben der Datenerfassung, -aufbereitung und -veröffentlichung einsetzen lassen. Onlinedienste wie Amazons Web ServicesFootnote 39 können etwa dazu verwendet werden, Tracking-Daten zu sammeln, indem beim Aufruf einer URL ein Log-Eintrag in einer Datenbank erzeugt wird. Auch für Speech-To-Text, die automatisierte Bilderkennung oder zur Bereitstellung großer Datenmengen kann auf spezialisierte Funktionalitäten zurückgegriffen werden.

Sollen zudem Forschungsergebnisse in einem Dashboard veröffentlicht werden, wird ein entsprechend spezialisierter Hosting-Anbieter benötigt. Dashboards sind interaktive Anwendungen, die zur Visualisierung von Daten eingesetzt werden. Sie erlauben den Nutzenden sowohl einen schnellen Einblick in die Daten als auch die eigene Exploration. Auf diese Weise können beispielsweise Ereignisse wie die Berichterstattung auf einem Zeitstrahl oder einer Karte dargestellt werden und auch Netzwerke lassen sich visualisieren. Zu bedenken ist dabei, dass in der Regel mehrere Nutzende gleichzeitig auf eine Webseite zugreifen sollen. Anders als beim Ausleihen von Büchern aus einer Bibliothek, warten sie nicht aufeinander. Die Optimierung von Wartezeiten betrifft damit nicht nur die Durchführung einer Analyse, sondern auch den Zugriff auf die Ergebnisse einer Analyse. Als Daumenregel gilt seit langer Zeit, dass Nutzende bei der Interaktion mit einem Computersystem eine Verzögerung von bis zu einer Zehntelsekunde noch als Echtzeit empfinden (Nielsen 1993, S. 135). Um dieses Ziel bei der Umsetzung von Dashboards und Webseiten auch für umfangreiche Datensätze zu erreichen, können vorberechnete Ergebnisse eingesetzt werden – anstatt den Datensatz mit allen Nachrichtenmeldungen neu auszuzählen, wird etwa die auf Tage aggregierte Zeitreihe in ein Dashboard eingebunden. Damit die Reaktionszeiten akzeptabel sind, ist darüber hinaus die ggf. parallelisierte Rechenleistung des Geräts ausschlaggebend, auf dem das Dashboard erzeugt wird.

Dashboards können direkt aus R oder Python erzeugt werden. In R wird dazu etwa das Paket ShinyApp und in Python die Software Dash eingesetzt. Das Grundprinzip ist jeweils gleich: In einem Skript werden Elemente der Benutzeroberfläche wie Buttons oder Balkendiagrame erzeugt. Einige der Elemente sind an Ereignisse wie das Klicken oder die Eingabe von Text gebunden, das heißt, bei Änderungen durch die Nutzenden wird eine Funktion aufgerufen, die passende Daten (zum Beispiel aus einer CSV-Datei oder einer Datenbank) ausliest und die grafischen Elemente entsprechend anpasst. Zur Verwendung von ShinyApps und Dash finden sich im Web viele gute Tutorials (☛ Repositorium; Abb. 6.9).

Abb. 6.9
figure 9

Auszug aus einer ShinyApp. Anmerkung: Eine Veränderung der Eingabewerte führt zur Aktualisierung der Grafik. (Quelle: eigene Darstellung, ☛ Repositorium)

Dashboards agieren in der Regel als Webserver und erzeugen eine Webseite, die im Browser auf dem eigenen Computer angezeigt wird. Ein Teil der Berechnungen findet auf dem Webserver (z. B. die Datenaufbereitung über R) und ein Teil direkt im Browser der Nutzenden statt (z. B. die Visualisierung mit JavaScript), wodurch die Last verteilt wird. Während der Entwicklung wird der eigene Computer als lokaler Server eingesetzt. Um ein solches Dashboard zu veröffentlichen, kann man prinzipiell den eigenen Computer für Zugriffe aus dem Internet öffnen. Dies ist aber schon aus Sicherheitsgründen kaum zu empfehlen. Insbesondere wenn hohe Zugriffszahlen zu erwarten sind, greift man besser auf spezialisierte Hoster zurück, die ggf. auch viele parallele Zugriffe managen. Für Dashboards eignen sich etwa Anbieter wie Shinyapps.io oder Plotly.Footnote 40

Über spezialisierte Dienste hinaus stellen Full-Stack-Anbieter die Infrastruktur für alle Arten von Serveranwendungen zur Verfügung. Hier können virtuelle Maschinen konfiguriert und (Docker-)Container bereitgestellt werden. Diese Dienste haben in der Regel für kleinere Projekte kostenlose Einstiegsangebote und umfangreiche Dokumentationen im Programm und lassen sich bei Bedarf hochskalieren. Solche Cloud-Plattformen werden insbesondere von den großen Technologiekonzernen Amazon, Microsoft oder Google bereitgestellt. Daneben finden sich auch lokale kommerzielle Dienste, die auf Cloud Computing spezialisiert sind, beispielsweise der deutsche Anbieter Hetzner. Dabei sollte man die Kosten im Blick behalten oder Kostendeckel einrichten – schließlich berechnen Hoster Geld dafür, dass sie ihre Computer zur Verfügung stellen. Deshalb lohnt es sich, die unterschiedlichen Dienste und deren Funktionen, Nutzungs- und Datenschutzbedingungen sowie Preise zu vergleichen. Universitäten stellen für wissenschaftliche Projekte häufig kostenfreie Dienste bereit und sind auch aufgrund des Datenschutzes in der Regel zu bevorzugen.

Übungsfragen

  1. 1.

    Was versteht man unter einem Kern und was unter einem Knoten?

  2. 2.

    Welche Fragestellungen fallen Ihnen ein, bei denen die Parallelisierung auf einem Cluster nötig sein könnte?

  3. 3.

    Recherchieren Sie, ob an Ihrer Hochschule ein HPC mit Slurm zur Verfügung steht!

  4. 4.

    Wie kann ein interaktives Dashboard erstellt werden?

Weiterführende Literatur

  • Chacon, S. & Straub, B. (2022). Pro Git. Everything you need to know about Git. (2. Aufl.). Berkeley: Apress. https://git-scm.com/book/en/v2

  • Martin, R. C. (2009). Clean Code: Refactoring, Patterns, Testen und Techniken für sauberen Code. Hamburg: MITP.

  • Öggl, B. & Kofler, M. (2018). Docker. Das Praxisbuch für Entwickler und DevOps-Teams. Bonn: Rheinwerk Verlag.

  • Santacroce, F. (2015). Git essentials. Create, merge, and distribute code with Git, the most powerful and flexible versioning system available. Birmingham: Packt Publishing.

  • Santacroce, F., Olsson, A., Voss, R. & Nare̜bski, J. (2016). Git. Mastering version control. Birmingham: Packt Publishing.

  • Trilling, D. & Jonkman, J. G. F. (2018). Scaling up Content Analysis. Communication Methods and Measures, 12(2-3), 158–174. https://doi.org/10.1080/19312458.2018.1447655

  • van Rossum, G., Warsaw, B. & Coghlan, N. (2013). PEP 8 – Style Guide for Python Code. Zugriff am 25.04.2022. https://peps.python.org/pep-0008/

  • Vasavada, N. (2021). Cracking Containers with Docker and Kubernetes. Delhi: BPB Publications.