Data Science, Machine Learning und KI
Kontakt

Im ersten Teil dieses Blogs haben wir darüber gesprochen, was ein DAG ist, wie man dieses mathematische Konzept in der Projektplanung und -programmierung anwendet und warum wir bei STATWORX beschlossen haben, Airflow statt anderer Workflow-Manager einzusetzen. In diesem Teil werden wir jedoch etwas technischer und untersuchen eine recht informative Hello-World-Programmierung und wie man Airflow für verschiedene Szenarien einrichtet, mit denen man konfrontiert werden könnte. Wenn du dich nur für den technischen Teil interessierst und deshalb den ersten Teil nicht lesen willst, aber trotzdem eine Zusammenfassung möchtest, findest du hier eine Zusammenfassung:

  • DAG ist die Abkürzung für „Directed Acyclic Graph“ und kann als solcher Beziehungen und Abhängigkeiten darstellen.
  • Dieser letzte Aspekt kann im Projektmanagement genutzt werden, um deutlich zu machen, welche Aufgaben unabhängig voneinander ausgeführt werden können und welche nicht.
  • Die gleichen Eigenschaften können in der Programmierung genutzt werden, da Software bestimmen kann, welche Aufgaben gleichzeitig ausgeführt werden können oder in welcher Reihenfolge die anderen beendet werden (oder fehlschlagen) müssen.

Warum haben wir Airflow gewählt:

  1. Kein Cron – Mit Airflows integriertem Scheduler müssen wir uns nicht auf Cron verlassen, um unsere DAG zu planen und verwenden nur ein Framework (nicht wie Luigi).
  2. Code Bases – In Airflow werden alle Workflows, Abhängigkeiten und das Scheduling in Python Code durchgeführt. Daher ist es relativ einfach, komplexe Strukturen aufzubauen und die Abläufe zu erweitern.
  3. Sprache – Python ist eine Sprache, die man relativ leicht erlernen kann und Python Kenntnisse war in unserem Team bereits vorhanden.

Vorbereitung

Der erste Schritt war die Einrichtung einer neuen, virtuellen Umgebung mit Python und virtualenv.

$pip install virtualenv # if it hasn't been installed yet   
$cd  # change into home 

# create a separated folder with all environments  
$mkdir env   
$cd env   
$virtualenv airflow

Sobald die Umgebung erstellt wurde, können wir sie immer dann verwenden, wenn wir mit Airflow arbeiten wollen, so dass wir nicht in Konflikt mit anderen Abhängigkeiten geraten.

$source ~/env/airflow/bin/activate  

Dann können wir alle Python-Pakete installieren, die wir benötigen.

$ pip install -U pip setuptools wheel \
psycopg2\
Cython \
pytz \
pyOpenSSL \
ndg-httpsclient \
pyasn1 \
psutil \
apache-airflow[postgres]\  

A Small Breeze

Sobald unser Setup fertig ist, können wir überprüfen, ob Airflow korrekt installiert ist, indem wir airflow version in Bash eingeben und du solltest etwas wie dieses sehen:

version-sequential

Anfänglich läuft Airflow mit einer SQLite-Datenbank, die nicht mehr als eine DAG-Aufgabe gleichzeitig ausführen kann und daher ausgetauscht werden sollte, sobald du dich ernsthaft damit befassen willst oder musst. Doch dazu später mehr. Beginnen wir nun mit dem typischen Hello-World-Beispiel. Navigiere zu deinem AIRFLOW_HOME-Pfad, der standardmäßig ein Ordner namens airflow in deinem Stammverzeichnis ist. Wenn du das ändern willst, editiere die Umgebungsvariable mit export AIRFLOW_HOME=/your/new/path und rufe airflow version noch einmal auf.

# ~/airflow/dags/HelloWorld.py  

from airflow import DAG  
from airflow.operators.dummy_operator import DummyOperator  
from airflow.operators.python_operator import PythonOperator  
from datetime import datetime, timedelta  

def print_hello():
    return 'Hello world!'  

dag = DAG('hello_world',
            description='Simple tutorial DAG',
            start_date= datetime.now() - timedelta(days= 4),  
            schedule_interval= '0 12 * * *'  
         )  

dummy_operator= DummyOperator(task_id= 'dummy_task', retries= 3, dag= dag)  

hello_operator= PythonOperator(task_id= 'hello_task', python_callable= print_hello, dag= dag)  

dummy_operator >> hello_operator # same as  dummy_operator.set_downstream(hello_operator)  

Die ersten neun Zeilen sollten einigermaßen selbsterklärend sein, nur der Import der notwendigen Bibliotheken und die Definition der Hello-World-Funktion passieren hier. Der interessante Teil beginnt in Zeile zehn. Hier definieren wir den Kern unseres Workflows, ein DAG-Objekt mit dem Identifier hello _world in diesem Fall und eine kleine Beschreibung, wofür dieser Workflow verwendet wird und was er tut (Zeile 10). Wie du vielleicht schon vermutet hast, definiert das Argument start_date das Anfangsdatum des Tasks. Dieses Datum sollte immer in der Vergangenheit liegen. Andernfalls würde die Aufgabe ausgelöst werden und immer wieder nachfragen, ob sie ausgeführt werden kann, und als solche bleibt sie aktiv, bis sie geplant ist. Das schedule_interval definiert die Zeiträume, in denen der Graph ausgeführt werden soll. Wir setzen sie entweder mit einer Cron-ähnlichen Notation auf (wie oben) oder mit einem syntaktischen Hilfsmittel, das Airflow übersetzen kann. Im obigen Beispiel definieren wir, dass die Aufgabe täglich um 12:00 Uhr laufen soll. Die Tatsache, dass sie täglich laufen soll, hätte auch mit schedule_interval='@daily ausgedrückt werden können. Die Cron-Notation folgt dem Schema Minute - Stunde - Tag (des Monats) - Monat - Tag (der Woche), etwa mi h d m wd. Mit der Verwendung von * als Platzhalter haben wir die Möglichkeit, in sehr flexiblen Intervallen zu planen. Nehmen wir an, wir wollen, dass ein Job jeden ersten Tag des Monats um zwölf Uhr ausgeführt wird. In diesem Fall wollen wir weder einen bestimmten Monat noch einen bestimmten Wochentag und ersetzen den Platzhalter durch eine Wildcard * ( min h d * *). Da es um 12:00 laufen soll, ersetzen wir mi mit 0 und h mit 12. Schließlich geben wir noch den Tag des Monats als 1 ein und erhalten unsere endgültige Cron-Notation 0 12 1 * *. Wenn wir nicht so spezifisch sein wollen, sondern lediglich täglich oder stündlich, beginnend mit dem Startdatum Ausführungen benötigen, können wir Airflows Hilfsmittel verwenden – @daily, @hourly, @monthly oder @yeary.

Sobald wir diese DAG-Instanz haben, können wir damit beginnen, sie mit einer Aufgabe zu füllen. Instanzen von Operatoren in Airflow repräsentieren diese. Hier initiieren wir einen DummyOperator und einen PythonOperator. Beiden muss eine eindeutige id zugewiesen werden, aber dieses Mal muss sie nur innerhalb des Workflows eindeutig sein. Der erste Operator, den wir definieren, ist ein DummyOperator, der überhaupt nichts tut. Wir wollen nur, dass er unseren Graphen füllt und dass wir Airflow mit einem möglichst einfachen Szenario testen können. Der zweite ist ein PythonOperator. Neben der Zuordnung zu einem Graphen und der id benötigt der Operator eine Funktion, die ausgeführt wird, sobald die Aufgabe ausgelöst wird. Nun können wir unsere Funktion hello_world verwenden und über den PythonOperator an unseren Workflow anhängen.

Bevor wir unseren Ablauf schließlich ausführen können, müssen wir noch die Beziehung zwischen unseren Aufgaben herstellen. Diese Verknüpfung wird entweder mit den binären Operatoren << und >> oder durch den Aufruf der Methoden set_upstream und set_downstream vorgenommen. Auf diese Weise können wir die Abhängigkeit einstellen, dass zuerst der DummyOperator laufen und erfolgreich sein muss, bevor unser PythonOperator ausgeführt wird.

Nun da unser Code in Ordnung ist, sollten wir ihn testen. Dazu sollten wir ihn direkt im Python-Interpreter ausführen, um zu prüfen, ob wir einen Syntaxfehler haben. Führe ihn also entweder in einer IDE oder im Terminal mit dem Befehl python hello_world.py aus. Wenn der Interpreter keine Fehlermeldung ausgibt, kannst du dich glücklich schätzen, dass du es nicht allzu sehr vermasselt hast. Als nächstes müssen wir überprüfen, ob Airflow unsere DAG mit airflow list_dags kennt. Jetzt sollten wir unsere hello_world id in der gedruckten Liste sehen. Wenn dies der Fall ist, können wir mit airflow list_task hello_world überprüfen, ob jede Aufgabe ihm zugewiesen ist. Auch hier sollten wir einige bekannte IDs sehen, nämlich dummy_task und hello_task. So weit so gut, zumindest die Zuweisung scheint zu funktionieren. Als nächstes steht ein Unit-Test der einzelnen Operatoren mit airflow test dummy_task 2018-01-01 und airflow test hello_task 2018-01-01 an. Hoffentlich gibt es dabei keine Fehler, und wir können fortfahren.

Da wir nun unseren Beispiel-Workflow bereitstellen konnten, müssen wir Airflow zunächst vollständig starten. Dazu sind drei Befehle erforderlich, bevor wir mit der manuellen Auslösung unserer Aufgabe fortfahren können.

  1. airflow initdb um die Datenbank zu initiieren, in der Airflow die Arbeitsabläufe und ihre Zustände speichert:
    initdb-sequential
  2. airflow webserver, um den Webserver auf localhost:8080 zu starten, von wo aus wir die Weboberfläche erreichen können:
    webserver-sequential
  3. airflow scheduler, um den Scheduling-Prozess der DAGs zu starten, damit die einzelnen Workflows ausgelöst werden können:scheduler-sequential
  4. airflow trigger_dag hello_world um unseren Workflow auszulösen und ihn in den Zeitplan aufzunehmen.

Jetzt können wir entweder einen Webbrowser öffnen und zu der entsprechenden Website navigieren oder open http://localhost:8080/admin/ im Terminal aufrufen, und es sollte uns zu einer Webseite wie dieser führen.

web-ui

Unten solltest du deine Kreation sehen und der hellgrüne Kreis zeigt an, dass unser Ablauf geplant ist und ausgeführt wird. Jetzt müssen wir nur noch warten, bis er ausgeführt wird. In der Zwischenzeit können wir über das Einrichten von Airflow sprechen und darüber, wie wir einige der anderen Executors verwenden können.

Das Backend

Wie bereits erwähnt – sobald wir uns ernsthaft mit der Ausführung unserer Graphen beschäftigen wollen, müssen wir das Backend von Airflow ändern. Anfänglich wird eine einfache SQLite-Datenbank verwendet, die Airflow darauf beschränkt, jeweils nur eine Aufgabe sequenziell auszuführen. Daher werden wir zunächst die angeschlossene Datenbank auf PostgreSQL umstellen. Falls du Postgres noch nicht installiert hast und Hilfe dabei brauchst, empfehle ich dir diesen Wiki-Artikel. Ich könnte den Prozess nicht so gut beschreiben wie die Seite. Für diejenigen, die mit einem Linux-basierten System arbeiten (sorry, Windows), versucht es mit sudo apt-get install postgresql-client oder mit homebrew auf einem Mac – brew install postgresql. Eine andere einfache Möglichkeit wäre die Verwendung eines Docker-Containers mit dem entsprechenden image.

Nun erstellen wir eine neue Datenbank für Airflow, indem wir im Terminal psql createdb airflow eingeben, in dem alle Metadaten gespeichert werden. Als nächstes müssen wir die Datei airflow.cfg bearbeiten, die in dem AIRFLOW_HOME-Ordner erscheinen sollte (der wiederum standardmäßig airflow in Ihrem Home-Verzeichnis ist) und die Schritte 1 – 4 von oben (initdb…) neu starten. Starte nun deinen Lieblingseditor und suche nach Zeile 32 sql_alchemy_conn =. Hier werden wir den SQLite Connection String durch den von unserem PostgreSQL-Server und einen neuen Treiber ersetzen. Diese Zeichenkette wird zusammengesetzt aus:

postgresql+psycopg2://IPADRESS:PORT/DBNAME?user=USERNAME&password=PASSWORD  

Der erste Teil teilt sqlalchemy mit, dass die Verbindung zu PostgreSQL führen wird und dass es den psycopg2-Treiber verwenden soll, um sich mit diesem zu verbinden. Falls du Postgres lokal installiert hast (oder in einem Container, der auf localhost mappt) und den Standard-Port von 5432 nicht geändert hast, könnte IPADRESS:PORT in localhost:5432 oder einfach localhost übersetzt werden. Der DBNAME würde in unserem Fall in airflow geändert werden, da wir ihn nur zu diesem Zweck erstellt haben. Die letzten beiden Teile hängen davon ab, was du als Sicherheitsmaßnahmen gewählt hast. Schließlich könnten wir eine Zeile erhalten haben, die wie folgt aussieht:

sql_alchemy_conn = postgresql+psycopg2://localhost/airflow?user=postgres&password=password  

Wenn wir dies getan haben, können wir auch unseren Executor in Zeile 27 von „Executor = SequentialExecutor“ in einen „Executor = LocalExecutor“ ändern. Auf diese Weise wird jede Aufgabe als Unterprozess gestartet und die Parallelisierung findet lokal statt. Dieser Ansatz funktioniert hervorragend, solange unsere Aufträge nicht zu kompliziert sind oder auf mehreren Rechnern laufen sollen.

Sobald wir diesen Punkt erreicht haben, brauchen wir Celery als Executor. Dabei handelt es sich um eine asynchrone Task/Job-Warteschlange, die auf verteilter Nachrichtenübermittlung basiert. Um den CeleryExecutor zu verwenden, benötigen wir jedoch ein weiteres Stück Software – einen Message Broker. Ein Message Broker ist ein zwischengeschaltetes Programmmodul, das eine Nachricht von der „Sprache“ des Senders in die des Empfängers übersetzt. Die beiden gängigsten Optionen sind entweder redis oder rabbitmq. Verwende das, womit du dich am wohlsten fühlst. Da wir rabbitmq verwendet haben, wird der gesamte Prozess mit diesem Broker fortgesetzt, sollte aber für redis mehr oder weniger analog sein.

Wiederum ist es für Linux- und Mac-Benutzer mit apt/homebrew ein Einzeiler, ihn zu installieren. Tippe einfach in dein Terminal sudo apt-get install rabbitmq-server oder brew install rabbitmq ein und fertig. Als nächstes brauchen wir einen neuen Benutzer mit einem Passwort und einen virtuellen Host. Beides – Benutzer und Host – kann im Terminal mit dem rabbitsmqs Kommandozeilen-Tool rabbitmqctl erstellt werden. Nehmen wir an, wir wollen einen neuen Benutzer namens myuser mit mypassword und einen virtuellen Host als myvhost erstellen. Dies kann wie folgt erreicht werden:

$ rabbitmqctl add_user myuser mypassword  
$ rabbitmqctl add_vhost myvhost  

Doch nun zurück zur Airflows-Konfiguration. Navigiere in deinem Editor zur Zeile 230, und du wirst hoffentlich broker_url = sehen. Dieser Connection-String ist ähnlich wie der für die Datenbank und wird nach dem Muster BROKER://USER:PASSWORD@IP:PORT/HOST aufgebaut. Unser Broker hat das Akronym amqp, und wir können unseren neu erstellten Benutzer, das Passwort und den Host einfügen. Sofern du nicht den Port geändert hast oder einen Remote Server verwendest, sollte deine Zeile in etwa so aussehen:

broker_url = amqp://myuser:mypassword@localhost:5672/myvhost  

Als nächstes müssen wir Celery Zugriff auf unsere airflow-Datenbank gewähren und die Zeile 232 mit:

db+postgresql://localhost:5432/airflow?user=postgres&password=password

Dieser String sollte im Wesentlichen dem entsprechen, den wir zuvor verwendet haben. Wir müssen nur den Treiber psycopg2 weglassen und stattdessen db+ am Anfang hinzufügen. Und das war’s! Du solltest nun alle drei Executors in der Hand haben und die Einrichtung ist abgeschlossen. Unabhängig davon, welchen Executor du gewählt hast, musst du, sobald du die Konfiguration geändert hast, die Schritte 1-4 – Initialisierung der DB, Neustart des Schedulers und des Webservers – erneut ausführen. Wenn du dies jetzt tust, wirst du feststellen, dass sich die Eingabeaufforderung leicht verändert hat, da sie anzeigt, welchen Executor du verwendest.

webserver-celery

Schluss & Ausblick

Airflow ist ein einfach zu bedienender, codebasierter Workflow-Manager mit einem integrierten Scheduler und mehreren Executors, die je nach Bedarf skaliert werden können.

Wenn du einen Ablauf sequenziell ausführen willst oder wenn es nichts gibt, was gleichzeitig laufen könnte, sollten die Standard-SQLite-Datenbank und der sequenzielle Executor die Aufgabe erfüllen.

Wenn du Airflow verwenden willst, um mehrere Aufgaben gleichzeitig zu starten und so die Abhängigkeiten zu verfolgen, solltest du zuerst die Datenbank und einen LocalExecutor für lokale Mehrfachverarbeitung verwenden. Dank Celery sind wir sogar in der Lage, mehrere Maschinen zu verwenden, um noch fortgeschrittenere und komplexere Workflows ohne viel Aufwand und Sorgen auszuführen. Marvin Taschenberger Marvin Taschenberger Marvin Taschenberger

Kürzlich standen wir bei STATWORX vor der typischen Situation, dass wir einen Proof of Concept (POC) in etwas umwandeln mussten, das in der Produktion verwendet werden konnte. Der „neue“ Aspekt dieser Umwandlung bestand darin, dass der PoC mit einer winzigen Menge (ein paar hundert Megabyte) geladen war, aber für eine riesige Datenmenge (Terabyte) vorbereitet werden musste. Der Schwerpunkt lag auf dem Aufbau von Datenpipelines, die alle Einzelteile miteinander verbinden und den gesamten Workflow von der Datenbank über ETL (Extract-Transform-Load) und Berechnungen bis hin zur eigentlichen Anwendung automatisieren. Das einfache Master-Skript, das ein Skript nach dem anderen aufruft, kam also nicht mehr in Frage. Es war relativ klar, dass ein Programm oder ein Framework, das DAG’s verwendet, notwendig war. Daher werde ich in diesem Beitrag kurz darauf eingehen, was eine DAG in diesem Zusammenhang ist, welche Alternativen wir in Betracht gezogen haben und für welche wir uns letztendlich entschieden haben. Außerdem gibt es einen zweiten Teil, in dem genauer erklärt wird, wie der Arbeitsablauf mit Airflow aussieht, z.B. ein Hallo-Welt-Programm und das ganze Setup.

Was ist eine DAG?

DAG ist das Akronym für Directed Acyclic Graph und ist ein mathematisches Konzept, um Knoten in Beziehung zueinander darzustellen, allerdings ohne dabei Zyklen zu erlauben. Mit anderen Worten, es handelt sich einfach um einen Haufen von Knoten, die miteinander verbunden sind (linker Teil des Bildes unten). Als nächstes fügen wir Beziehungen zwischen den einzelnen Knoten hinzu (mittlerer Teil des Bildes unten), die in immer nur in eine bestimmte Richtung weisen dürfen, und schließlich beschränken wir die Verbindungen so, dass sie keine Zyklen zwischen den Knoten bilden (rechter Teil des Bildes unten).

In der Programmierung kann diese spezielle From eines Ausführungsplans verwendet werden, um alle notwendigen Prozessschritte als einen Knoten im Graphen zu definieren. Jede Aufgabe, die selbständig erledigt werden kann, ist ein Anfangsknoten ohne Vorgänger und hat als solcher keine abhängingen, vorgeschalteten Beziehungspunkte. Ausgehend von einem Anfangsknoten werden wir die Aufgaben verknüpfen, die direkt von diesem abhängig sind. Wenn wir diesen Prozess fortsetzen und alle Aufgaben mit dem daraus entstehenden Graphen verbinden, können wir ein ganzes Projekt in einem visuellen Ablaufplan darstellen. Auch wenn dies für einfache Projekte wie „führe erst A aus, dann B und schließlich C“ trivial sein mag, ist dies ab einer gewissen Größe oder Komplexität des Workflows nicht mehr der Fall. Komplexe Arbeitsabläufe, mit mehreren Aufgaben, die wiederum direkte und indirekte Abhängigkeiten aufweisen können, bietet das ideale Szenario die Stärken der DAG-Darstellung auszunutzen. Ein DAG gibt daher nicht nur einen nützlichen Einblick, wie sich Abhängigkeiten auf die eigentlich Ausführung des Prozesses auswirken (welche Aufgben dürfen parallel, welche müssen sequentiell ausgeführt werden). Sondern ist als visuelle Darstellung hilfreich, um diesen Prozess auch für Nicht-Experten leicht verständlich zu machen.

Stellen Sie sich vor, Sie haben einen Arbeitsablauf, der aus mehreren Dutzend Aufgaben besteht (wie die obige), von denen einige sequentiell und andere parallel ablaufen müssen. Stellen Sie sich vor, eine dieser Aufgaben würde fehlschlagen – ohne den DAG wäre nicht klar, was als nächstes geschehen soll. Welche Aufgabe muss warten, bis die fehlgeschlagene Aufgabe endlich erfolgreich ist? Welche kann weiterlaufen, da sie nicht von ihr abhängt? Mit einer DAG kann diese Frage schnell beantwortet werden und stellt sich für den Anwender nicht einmal dann, wenn ein Programm den Überblick darüber behält. Aufgrund dieser Bequemlichkeit haben viele Programme und Pakete diese Darstellung übernommen, um Arbeitsabläufe zu automatisieren.

Wonach haben wir gesucht?

Wie bereits erwähnt, waren wir auf der Suche nach einer Software, einem Framework oder zumindest einer Bibliothek als Orchestrator, die auf der Basis von DAGs arbeitet, den Überblick über die gesamten Workflows behält und mit eventuell auftretenden Fehlern (z.B. eine Aufgabe schlägt fehlt) umgehen kann. Darüber hinaus wäre ein eingebauter, „advanced“ Scheduler vorteilhaft, da der Ablauf jede Woche ausgeführt werden muss und eine manuelle Überwachung daher sehr mühsam wäre. Warum advanced? – Es gibt einfache Scheduler wie cron, die sich hervorragend eignen, um einen bestimmten Job zu einer bestimmten Zeit zu starten, die sich aber nicht, oder nur sehr aufwendig in den Workflow nativ integrieren lassen. Ein Scheduler, der auch die DAGs (mit allen Abhängigkeiten und Besonderheiten) im Auge behält, wäre also großartig. Schließlich sollte neben der leichten Erweiterbarkeit des Workflows dieser auch skalierbar sein. Es wäre also hilfreich, wenn wir ein Skript, z. B. zum Bereinigen von Daten, mehrmals nur mit einem anderen Argument (für verschiedene Datenstapel) als verschiedene Knoten im Workflow ohne viel Overhead und Code aufrufen könnten.

Was waren unsere Optionen?

Nachdem wir die Entscheidung getroffen hatten, dass wir einen DAG basierten Orchestrator implementieren müssen, tauchten in der anschließenden Google-Suche eine Vielzahl von Software, Frameworks und Paketen auf. Es war notwendig, die Menge der Optionen einzugrenzen, so dass nur einige wenige übrig blieben, die wir eingehend untersuchen konnten. Auch stellten wir fest, dass wir ein Tool brauchen, das nicht nur überwiegend GUI basiert ist, da dies die Flexibilität und Skalierbarkeit einschränkt. Es sollte allerdings auch nicht zu code-intensiv oder in einer unbequemen Programmiersprache sein, da die daraus resuliteriende, flache Lernkurve eine rasche Adaption verhindert und es zusätzlich länger dauert alle Projekt Stakeholder an Bord zu holen. Daher wurden Optionen wie Jenkins oder WAF sofort verworfen. Dennoch konnten wir die Auswahl auf drei Optionen eingrenzen.

Option 1 – Native Lösung: Cloud-Orchestrator

Da der PoC in einer Cloud bereitgestellt wurde, war die erste Option auch ziemlich offensichtlich – wir könnten einen der nativen Orchestratoren verwenden. Diese böten uns eine einfache GUI zur Definition unserer DAGs, einen Scheduler und waren darauf ausgelegt, Daten wie in unserem Fall notwendig zu routen. Auch wenn sich das gut anhört, war das unvermeidliche Problem, dass solche GUIs die oben genannte Flexibilität einschränken, man für die Benutzung natürlich bezahlen müsste und es ohne Coden überhaupt keinen Spaß machen würde. Trotzdem behielten wir die Lösung als Backup-Plan bei.

Option 2 – Apaches Hadoop-Lösungen: Oozie oder Askaban

Oozie und Azkaban sind beides Open-Source Workflow-Manager, die in Java geschrieben und für die Integration in Hadoop-Systeme konzipiert sind. Sie sind daher beide für die Ausführung von DAGs entworfen, skalierbar und haben einen integrierten Scheduler. Während Oozie versucht, hohe Flexibilität im Tausch gegen Benutzerfreundlichkeit zu bieten, ist es bei Azkaban genau andersherum. So ist die Orchestrierung im Falle von Azkaban nur über die WebUI möglich. Oozie hingegen stützt sich auf XML-Dateien oder Bash, um Prozesse zu verwalten und zu planen.

Option 3 – Python-Lösung: Luigi oder Airflow

Luigi und Airflow sind beides in Python geschriebene Workflow-Manager, die als Open-Source-Frameworks verfügbar sind.

Luigi wurde 2011 von Spotify entwickelt und sollte so allgemein wie möglich gehalten werden – im Gegensatz zu Oozie oder Azkaban, die für Hadoop gedacht waren. Der Hauptunterschied zu den beiden anderen ist, dass das Definieren von Aufgaben in Luigi ausschließlich code- und nicht GUI-basiert ist. Die ausführbaren Workflows werden alle durch Python Code dargestellt und die Benutzeroberfläche dient nur der Verwaltung. Diese WebUI von Luigi bietet dabei eine hohe Benutzerfreundlichkeit, wie das Suchen, Filtern oder Überwachen der Graphen und Aufgaben.

Ähnlich verhält es sich mit Airflow, das von Airbnb entwickelt und 2015 freigegeben wurde. Außerdem wurde es 2016 in den Apache Incubator aufgenommen. Wie Luigi ist es ebenfalls code basiert und verfügt über eine Benutzeroberfläche, die auch hier wieder die Endnutzerfreundlichkeit erhöht. Außerdem verfügt es über einen integrierten Scheduler, so dass man nicht auf Cron angewiesen ist.

Unsere Entscheidung

Unser erstes Kriterium für die weitere Filterung war, dass wir einen code basierten Orchestrator wollten. Auch wenn grafische Schnittstellen relativ einfach zu handhaben sind und man sich schnell zurechtfindet, würde dies zu Lasten einer langsameren Entwicklung gehen. Außerdem wäre das Bearbeiten und Erweitern zeitaufwändig, wenn jede einzelne Anpassung per Mausklick erfolgen müsste, anstatt Funktionen oder Codeschnipsel wiederzuverwenden. Deshalb haben wir uns gegen Option 1 entschieden – den lokalen Cloud-Orchestrator. Der Verlust an Flexibilität sollte nicht unterschätzt werden. Alle Erfahrungen und Skills, die wir mit einem unabhängigen Orchestrator gewonnen haben, können wahrscheinlich auf jedes andere Projekt übertragen werden. Dies wäre bei einem cloud-nativen Orchestrator nicht der Fall, da er an die spezifische Umgebung gebunden ist.

Der wichtigste Unterschied zwischen den beiden anderen Optionen ist die Programmiersprache, in denen sie arbeiten. Luigi und Airflow sind Python basiert, während Oozie und Azkaban auf Java und Bash-Skripten basieren. Auch diese Entscheidung war leicht zu treffen, denn Python ist eine hervorragende Skriptsprache, die leicht zu lesen, schnell zu erlernen und einfach zu schreiben ist. Unter dem Aspekt der Flexibilität und Skalierbarkeit bot uns Python einen besseren Nutzen als die (kompilierte) Programmiersprache Java. Außerdem musste die Workflow-Definition entweder über eine grafische Benutzeroberfläche (wieder) oder über XML erfolgen. Auf diese Weise konnten wir auch Option zwei ausschließen.

Abschließend bliebt nur zu klären, ob Spotifys Luigi oder Airbnbs Airflow zur Anwendungen kommen sollte. Es war eine Entscheidung zwischen dem ausgereiften und stabilen (Luigi) oder dem jungen Star (Airflow) unter den Workflow-Managern. Beide Projekte wurden nach wie vor gepflegt und sind auf GitHub sehr aktiv, mit über mehreren tausend Commits, mehreren hundert Stars und mehreren hundert Mitwirkenden. Nichtsdestotrotz gab es einen Aspekt, der für unsere Entscheidung ausschlaggebend war – Cron. Luigi kann Jobs (Aufgaben) nur mit Hilfe von cron planen, im Gegensatz zu Airflow, das einen integrierten, advanced Scheduler hat. Aber was ist überhaupt das Problem mit Cron?

Cron funktioniert gut, wenn Sie eine Aufgabe zu einer bestimmten Zeit erledigen wollen. Sobald Sie jedoch mehrere Aufträge planen wollen, die voneinander abhängen, wird es schwierig. Cron berücksichtigt diese Abhängigkeiten nicht. Nehmen wir an, wir brauchen einen Job, der alle fünf Minuten läuft und einige Echtzeitdaten aus einer Datenbank abgreift. Wenn nichts schief geht und die Laufzeit der Jobs bekannt ist (und sich nicht ändert), werden keine Probleme entstehen. Ein Job wird gestartet, er wird beendet, der nächste startet und so weiter. Was aber, wenn die Verbindung zur Datenbank nicht funktioniert? Auftrag eins wird gestartet, aber nie beendet. Fünf Minuten später wird der zweite Job das Gleiche tun, während Job eins noch aktiv ist. Dies kann sich fortsetzen, bis der gesamte Rechner durch nicht beendete Aufträge blockiert ist oder abstürzt. Mit Airflow könnte ein solches Szenario leicht vermieden werden, da es den Start neuer Aufträge automatisch stoppt, wenn die Anforderungen nicht erfüllt sind.

Zusammenfassung unserer Entscheidung

Wir haben uns für Apache Airflow gegenüber den anderen Alternativen entschieden, weil

  1. Kein Cron – Mit Airflows integriertem Scheduler müssen wir uns nicht auf Cron verlassen, um unsere DAG zu planen, und verwenden nur ein Framework (nicht wie Luigi).

  2. Code-Basierend – In Airflow werden alle Workflows, Abhängigkeiten und das Scheduling in Python Code ausgeführt. Daher ist es relativ einfach, komplexe Strukturen aufzubauen und die Abläufe zu erweitern.

  3. Sprache – Python ist eine Sprache, die man leicht erlernen kann und die in unserem Team vorhanden war.

Daher erfüllt Airflow alle unsere Anforderungen. Wir haben damit einen Orchestrator, der den Workflow, den wir mit Python Code definieren, im Auge behält. Daher können wir den Workflow auch leicht in jede Richtung erweitern – mehr Daten, mehr Batches, mehr Schritte im Prozess oder sogar auf mehreren Maschinen gleichzeitig ausweiten, sind kein Problem mehr. Darüber hinaus bietet Airflow auch eine schöne visuelle Oberfläche des Workflows, sodass man ihn auch leicht überwachen kann. Und schließlich erlaubt Airflow den Verzicht auf Cron, da es mit einem nativen (advanced) Scheduler ausgestattet ist. Dieser kann nicht nur eine Aufgabe starten, sondern behält auch den Überblick über alle definierten Abhängigkeiten und ist in seinen Ausführungseigenschaften stark anpassbar, was zusätzliche Flexibilität bedeutet.

Im zweiten Teil dieses Blogs schauen wir uns Airflow genauer an, erklären, wie man es verwendet und wie man es für verschiedene Anwendungsfälle konfiguriert.

Marvin Taschenberger Marvin Taschenberger

Im vorherigen Teil dieser STATWORX Reihe haben wir uns mit verschiedenen Datenstrukturen auseinander gesetzt. Darunter jene, die uns in Python direkt ‚Out of the box‘ zur Verfügung stehen, als auch NumPy’s ndarrays. Bei den nativen Containern (z.B. Tuples oder Listen) konnten wir feststellen, dass nur die Listen unseren Anforderungen im Rahmen der Arbeit mit Daten – veränderbar und indizierbar – erfüllen. Jedoch waren diese relativ unflexibel und langsam, sobald wir versuchten, diese für rechenintensive mathematische Operationen zu nutzen. Zum einen mussten wir Operationen per Schleife auf die einzelnen Elemente anwenden und zum anderen waren Anwendungen aus der linearen Algebra, wie Matrizenmultiplikation nicht möglich. Daher wandten wir unsere Aufmerksamkeit den ndarrays von NumPy zu. Da NumPy den Kern der wissenschaftlichen Python-Umgebung darstellt, werden wir uns in diesem Teil genauer mit den Arrays befassen. Wir betrachten ihre Struktur tiefergehend und untersuchen woher die verbesserte Performance kommt. Abschließend werden wir darauf eingehen, wie man seine Analyse bzw. seine Ergebnisse speichern und erneut laden kann.

Attribute und Methoden

N-Dimensionen

Wie sämtliche Konstrukte in Python sind auch die ndarrays ein Objekt mit Methoden und Attributen. Das für uns interessanteste Attribut bzw. die interessanteste Eigenschaft ist, neben der Effizienz, die Multidimensionalität. Wie wir schon im letzten Teil gesehen haben, ist es einfach ein zweidimensionales Array zu erschaffen ohne dabei Objekte ineinander zu verschachteln, wie es bei Listen der Fall wäre. Stattdessen können wir einfach angeben, wie groß das jeweilige Objekt sein soll, wobei eine beliebige Dimensionalität gewählt werden kann. Typischerweise wird dies über das Argument ndim angegeben. NumPy bietet uns zusätzlich die Möglichkeit beliebig große Arrays außerordentlich simpel umzustrukturieren. Die Umstrukturierung der Dimensionalität eines ndarray erfolgt dabei durch die reshape()Methode. Ihr wird ein Tupel oder eine Liste mit der entsprechenden Größe übergeben. Um ein umstrukturiertes Array zu erhalten, muss die Anzahl der Elemente mit der angegebenen Größe kompatibel sein.

# 2D-Liste 
list_2d = [[1,2], [3,4]]

# 2D-Array
array_2d = np.array([1,2,3,4]).reshape((2,2))

# 10D-Array
array_10d = np.array(range(10), ndmin=10)

Um an strukturelle Informationen eines Arrays wie zum Beispiel der Dimensionalität zu gelangen, können wir die Attribute ndim, shape oder size aufrufen. So bietet uns beispielsweise ndim Aufschluss über die Anzahl der Dimensionen, während uns size verrät, wie viele Elemente sich in dem jeweiligen Array befinden. Das Attribut shape verbindet diese Informationen und gibt an, wie die jeweiligen Einträge auf die Dimensionen aufgeteilt sind.

# Erstellung eines 3x3x3 Arrays mit den Zahlen 0 bis 26 
Arr = np.arange(3*3*3).reshape(3,3,3)

# Anzahl der Dimension
Arr.ndim # =3

# Anzahl der Elemente , 27
Arr.size # =27 

# Detaillierte Aufgliederung dieser beiden Informationen 
Arr.shape # = (3,3,3) Drei Elemente pro Dimension 

Indizierung

Nach dem wir nun herausfinden können wie unser ndarray aufgebaut ist, stellt sich die Frage, wie wir die einzelnen Elemente oder Bereiche eines Arrays auswählen können. Diese Indizierung beziehungsweise das Slicing erfolgt dabei prinzipiell wie bei Listen. Durch die []-Notation können wir auch bei den Arrays einen einzelnen Index oder per :-Syntax ganze Folgen abrufen. Die Indizierung per Index ist relativ simpel. Zum Beispiel erhalten wir den ersten Wert des Arrays durch Arr[0] und durch ein vorangestelltes - erhalten wir den letzten Wert des Arrays durch Arr[-1]. Wollen wir jedoch eine Sequenz von Daten abrufen, können wir die :-Syntax nutzen. Diese folgt dem Schema [ Start : Ende : Schritt ], wobei sämtliche Argumente optional sind. Dabei ist anzumerken, dass nur die Daten exklusiv des angegebenen Ende ausgegeben werden. Somit erhalten wir durch Arr[0:2] nur die ersten beiden Einträge. Die Thematik wird in der folgenden Grafik verdeutlicht.

Wollen wir das gesamte Array mit dieser Logik auswählen, kann man auch den Start und / oder das Ende weglassen wodurch es automatisch ergänzt wird. So könnten wir mit Arr[:2] vom Ersten bis zum zweiten Element oder mit Arr[1:] vom Zweiten bis zum Letzten Element selektieren.

Als nächstes wollen wir auf das bisher ausgelassene Argument Schritt eingehen. Dies erlaubt es uns die Schrittweite, zwischen dem Element, zwischen Start und Ende festzulegen. Wollen wir beispielsweise nur jedes zweite Element des gesamten Arrays können wir den Start und das Ende weglassen und nur eine Schrittweite von 2 definieren – Arr[::2]. Wie bei der umgedrehten Indizierung, ist auch eine umgedrehte Schrittweite durch negative Werte möglich. Demnach führt eine Schrittweite von -1 dazu, dass das Array in umgedrehter Reihenfolge ausgegeben wird.

arrarr = np.array([1,1,2,2,3,3]) 
arr = np.array([1,2,3])
arrarr[::2] == arr
rra = np.array([3,2,1]) 
arr[::-1] == rra & rra[::-1] ==arr  
True

Sofern wir diesen nun auf ein Array übertragen wollen, welches nicht im eindimensionalen Raum, sondern in einem mehrdimensionalen Raum vorliegt, können wir einfach jede weitere Dimension als eine weitere Achse betrachten. Demzufolge können wir auch das Slicen eines eindimensionalen Arrays relativ leicht auf höhere Dimensionen übertragen. Hierfür müssen wir nur jede Dimension einzeln zerteilen und die einzelnen Befehle nur per Kommata trennen. Um so anhand dieser Syntax eine gesamte Matrix der Größe 3×3 zu selektieren, müssen wir also die gesamte erste und zweite Achse auswählen. Analog zu vorher würden wir also zweimal [:] benutzen. Dieses würden wir nun in einer Klammer formulieren als [:,:]. Dieses Schema lässt sich für beliebig viele Achsen erweitern. Hier ein paar weitere Beispiele:

arr = np.arange(8).reshape((2,2,2))
#das ganze Array 
arr[:,:,:]
# Jeweils das erste Element 
arr[0,0,0]
# Jeweils das letzte Element 
arr[-1,-1,-1]

Rechnen mit Arrays

UFuncs

Wie schon des Öfteren innerhalb dieses und des letzten Beitrages erwähnt, liegt die große Stärke von NumPy darin, dass das Rechnen mit ndarray äußerst performant ist. Der Grund dafür liegt zunächst an den Arrays die ein effizienter Speicher sind und es ermöglichen höherdimensionale Räume mathematisch abzubilden. Der große Vorteil von NumPy liegt dabei jedoch vor allem an den Funktionen die wir zur Verfügung gestellt bekommen. So ist es erst durch die Funktionen möglich, nicht mehr über die einzelnen Elemente per Schleife zu iterieren, sondern das gesamte Objekt übergeben zu können und auch nur eins wieder herauszubekommen. Diese Funktionen werden ‚UFuncs‘ genannt und zeichnen sich dadurch aus, dass sie so konstruiert und kompiliert sind, um auf einem gesamten Array zu arbeiten. Sämtliche Funktionen, die uns durch NumPy zugänglich sind, besitzen diese Eigenschaften, so auch die np.sqrt-Funktion, die wir im letzten Teil genutzt haben. Hinzu kommen auch noch die speziellen – extra für Arrays – definierten mathematischen Operatoren, wie +, -, * . Da auch diese letztendlich nur Methoden eines Objektes sind (z.B. ist a.__add__(b) das Gleiche wie a + b), wurden die Operatoren für NumPy Objekte als Ufunc-Methoden implementiert, um eine effiziente Kalkulation zu gewährleisten.
Die entsprechenden Funktionen könnten wir auch direkt ansprechen:

Operator Equivalente ufunc Beschreibung
+ np.add Addition (e.g., 1 + 1 = 2)
- np.subtract Subtraktion (e.g., 3 - 2 = 1)
- np.negative Unäre Negation (e.g., -2)
* np.multiply Multiplikation (e.g., 2 * 3 = 6)
/ np.divide Division (e.g., 3 / 2 = 1.5)
// np.floor_divide Division ohne Rest (e.g. ,3 // 2 = 1)
** np.power Exponent (e.g., 2 ** 3 = 8)
% np.mod Modulo/Rest (e.g., 9 % 4 = 1)

Dynamische Dimensionen via Broadcasting

Ein weiterer Vorteil von Arrays besteht außerdem darin, dass eine dynamische Anpassung der Dimensionen durch Broadcasting stattfindet, sobald eine mathematische Operation ausgeführt wird. Wollen wir also einen 3×1 Vektor und eine 3×3 Matrix elementweise miteinander multiplizieren, wäre dieses in der Algebra nicht trivial zu lösen. NumPy ’streckt‘ daher den Vektor zu einer weiteren 3×3 Matrix und führt dann die Multiplikation aus.

Dabei erfolgt die Anpassung über drei Regeln:

  • Regel 1: Wenn sich zwei Arrays in der Anzahl der Dimensionen unterscheiden, wird das kleine Array angepasst mit zusätzlichen Dimensionen auf der linken Seite, z.B. (3,2) -> (1,3,2)
  • Regel 2: Sofern sich die Arrays in keiner Dimension gleichen, wird das Array mit einer unären Dimension gestreckt, wie es im oberen Beispiel der Fall war (3x1) -> 3x*3
  • Regel 3: Sofern weder Regel 1 noch Regel 2 greifen, wird ein Fehler erzeugt.
# Regel 1
arr_1 = np.arange(6).reshape((3,2))
arr_2 = np.arange(6).reshape((1,3,2))
arr_2*arr_1 # ndim = 1,3,2

# Regel 2
arr_1 = np.arange(6).reshape((3,2))
arr_2 = np.arange(2).reshape((2))
arr_2*arr_1 # ndim = 3,2

# Regel 3 
arr_1 = np.arange(6).reshape((3,2))
arr_2 = np.arange(6).reshape((3,2,1))
arr_2*arr_1 # Error, da rechts aufgefüllt werden müsste und nicht links 
ValueErrorTraceback (most recent call last)

<ipython-input-4-d4f0238d53fd> in <module>()
     12 arr_1 = np.arange(6).reshape((3,2))
     13 arr_2 = np.arange(6).reshape((3,2,1))
---> 14 arr_2*arr_1 # Error da rechts aufgefüllt werden müsste und nicht links
ValueError: operands could not be broadcast together with shapes (3,2,1) (3,2) 

An diesem Punkt sollte man noch anmerken, dass Broadcasting nur für elementweise Operationen gilt. Sofern wir uns der Matrizenmultiplikation bedienen, müssen wir selber dafür sorgen, dass unsere Dimensionen stimmen. Wollen wir bNueispielhaft eine 3×1 Matrix mit einem Array der Größe 3 multiplizieren, wird nicht wie in Regel 2 die kleine Matrix links erweitert, sondern direkt ein Fehler erzeugt.

arr_1 = np.arange(3).reshape((3,1))
arr_2 = np.arange(3).reshape((3))

# Fehler 
arr_1@arr_2
ValueErrorTraceback (most recent call last)

<ipython-input-5-8f2e35257d22> in <module>()
      3 
      4 # Fehler
----> 5 arr_1@arr_2
ValueError: shapes (3,1) and (3,) not aligned: 1 (dim 1) != 3 (dim 0)

Bekannter und verständlicher als diese dynamische Anpassung sollten Aggregation als Beispiel für Broadcasting sein.
Neben den direkten Funktionen für die Summe oder den Mittelwert, lassen sich diese nämlich auch über die Operatoren abbilden. So kann man die Summe eines Array nicht nur per np.sum(x, axis=None) erhalten, sondern auch über np.add.reduce(x, axis = None ). Diese Form des Operatoren-Broadcasting erlaubt es uns auch die jeweilige Operation akkumuliert anzuwenden, um so rollierende Werte herauszubekommen. Über die Angabe der axis können wir bestimmen, entlang welcher Achse die Operation ausgeführt werden soll. Im Fall von np.sum oder np.mean ist None der Standard. Dies bedeutet, dass sämtliche Achsen einbezogen werden und ein Skalar entsteht. Sofern wir das Array jedoch nur um eine Achse reduzieren möchten, können wir den jeweiligen Index der Achse angeben:

# Reduziere alle Achsen 
np.add.reduce(arr_1, axis=None)

# Reduzieren der dritten Achse 
np.add.reduce(arr, axis=2)

# Kummulierte Summe 
np.add.accumulate(arr_1)

# Kummmuliertes Produkt 
np.multiply.accumulate(arr_1)

Speichern und Laden von Daten

Als letztes wollen wir nun auch noch in der Lage sein unsere Ergebnisse zu speichern und beim nächsten Mal zu laden. Hierbei stehen uns in NumPy generell zwei Möglichkeiten offen. Nummer 1 ist die Verwendung von textbasierten Formaten wie z.B. .csv durch savetxt und loadtxt. Werfen wir nun einen Blick auf die wichtigsten Eigenschaften dieser Funktionen, wobei der Vollständigkeit halber sämtliche Argumente aufgelistet werden, jedoch nur Bezug auf die wichtigsten genommen wird.

Das Speichern von Daten erfolgt dem Namen entsprechend durch die Funktion:

  • np.savetxt(fname, X, fmt='%.18e', delimiter=' ', newline='n', header='', footer='', comments='# ', encoding=None)

Über diesen Befehl können wir ein Objekt X in einer Datei fname speichern. Hierbei ist es prinzipiell egal, in welchem Format wir es speichern wollen, da das Objekt in Klartext gespeichert wird. Somit spielt es keine Rolle, ob wir als Suffix txt oder csv anfügen, die Namen entsprechen dabei nur Konventionen. Welche Werte zur Separierung genutzt werden sollen, geben wir durch die Schlüsselworte delimeter und newline an, welche im Fall eines csv ein ',' zur Separierung der einzelnen Werte / Spalten und ein n für eine neue Reihe sind. Per header und footer können wir optional angeben, ob wir weitere (String) Informationen an den Anfang oder das Ende der Datei schreiben wollen. Durch die Angabe von fmt – was für Format steht – können wir beeinflussen, ob und wie die einzelnen Werte formatiert werden sollen, also ob, wie und wie viele Stellen vor und nach dem Komma angezeigt werden sollen. Hierdurch können wir die Zahlen z.B. besser lesbar machen oder den Bedarf an Speicher auf der Festplatte verringern in dem wir die Präzision senken. Ein simples Beispiel wäre fmt = %.2 würde sämtliche Zahlen auf die zweite Nachkommastelle Runden ( 2.234 -> 2.23).

Das Laden der vorher gespeicherten Daten erfolgt durch die Funktion loadxt, die viele Argumente besitzt, die mit den Funktionen zum Speichern der Objekte übereinstimmt.

  • np.loadtxt(fname, dtype=<class 'float'>, comments='#', delimiter=None, converters=None, skiprows=0, usecols=None, unpack=False, ndmin=0, encoding='bytes')

Die Argumente fname und delimiter besitzen die selbe Funktionalität und Standardwerte wie beim Speichern der Daten. Durch skiprows kann angegeben werden, ob und wie viele Zeilen übersprungen werden sollen und durch usecols wird mit einer Liste von Indizes bestimmt, welche Spalten eingelesen werden sollen.

# Erstelle ein Bespiel
x = np.arange(100).reshape(10,10)

# Speicher es als CSV
np.savetxt('example.txt',x)

# Erneutes Laden 
x = np.loadtxt(fname='example.txt')

# Überspringen der ersten fünf Zeilen 
x = np.loadtxt(fname='example.txt', skiprows=5)

# Lade nur die erste und letzte Spalte 
x = np.loadtxt(fname='example.txt', usecols= (0,-1))

Die zweite Möglichkeit Daten zu speichern sind binäre .npy Dateien. Hierdurch werden die Daten komprimiert, wodurch Sie zwar weniger Speicherplatz benötigen, jedoch auch nicht mehr direkt lesbar sind wie zum Beispiel txt oder csv Dateien. Darüber hinaus sind auch die Möglichkeiten beim Laden und Speichern vergleichsweise limitiert.

  • np.save(file, arr, allow_pickle=True, fix_imports=True)
  • np.load(file, mmap_mode=None, allow_pickle=True, fix_imports=True, encoding='ASCII')

Für uns sind lediglich file und arr interessant. Wie der Name wahrscheinlich vermuten lässt, können wir durch das Argument file wieder angeben, in welcher Datei unser Array arr gespeichert werden soll. Analog dazu können wir auch beim Laden per load die zu ladende Datei über file bestimmen.

# Komprimieren und Speichern 
np.save('example.npy', x )

# Laden der komprimierten Datei
x = np.load('example.npy')

Vorschau

Da wir uns nun mit dem mathematischen Kern der Data Science Umgebung vertraut gemacht haben, können wir uns im nächsten Teil damit beschäftigen, unseren Daten etwas mehr inhaltliche Struktur zu verpassen. Hierfür werden wir die nächste große Bibliothek erkunden – nämlich Pandas. Dabei werden uns mit den zwei Hauptobjekten – Series und DataFrame – der Bibliothek bekannt machen durch die wir wir bestehende Datensätze nutzen und diese direkt modifizieren und manipulieren können. Marvin Taschenberger Marvin Taschenberger

Zu Beginn ein kurzer Rückblick in unserem ersten Blogbeitrag zum Thema Data Science mit Python. Wir haben uns mit mit einigen grundlegenden Python-Werkzeugen auseinander gesetzt haben, die uns es ermöglicht, mit IPython oder auch mit Jupyter Notebooks sehr interaktiv zu arbeiten. In diesem Teil stellen wir Euch nun Möglichkeiten vor Zahlen und Variablen eine Struktur zu geben sowie Berechnungen von Array/Matrizen durchzuführen. Schauen wir uns also zuerst einmal an, welche Möglichkeiten uns ‚Out of the box‘ zur Verfügung stehen.

Vorstellung von Datenstrukturen in Python

Um mehrere Objekte, diese können z.B. Zahlen, Zeichen, Wörter, Sätze bzw. jegliches Python-Objekt sein, in eine Art Container zu packen, bietet uns Python unterschiedliche Möglichkeiten an, so gibt es:

  • Tupel
  • Sets
  • Listen
  • Dictionaries

Data Science impliziert bereits durch seinen Namen, dass viel mit Daten gearbeitet wird, so ist ein wichtiges Kriterium für eine Datenstruktur, dass sich Daten verändern lassen und sie zudem indiziert sind. Diese Anforderungen wird nur von Listen und Dictonaries erfüllt. Bei Tupeln sind die Daten zwar indiziert aber können nicht verändert werden. Sets erfüllen weder die Anforderung der Indizierung noch der Datenmanipulation. So lassen sich zwar Elemente hinzufügen und entfernen, aber nicht direkt verändern. Ihr Anwendungsbereich liegt vor allem in der Mengenlehre wie man sie aus der Mathematik kennt. Für einen schnellen Einstieg in Data Science stellen wir Euch nun Dictionaries und Listen als praktische Datenstrukturen in Python vor.

Dictionaries

Ein Dictonary zu Deutsch Lexikon bzw. Wörterbuch könnt ihr Euch Wort wörtlich so vorstellen. Es verbindet allgemein gesagt, ein Objekt – dieses kann beliebiger Natur sein – mit einem einzigartigen Schlüssel. Dopplungen innerhalb eines Dictonarys werden daher ausgeschlossen. Somit bieten sich diese eher für die Strukturierung von verschiedenen Variablen zu einem Datensatz, als jeden Eintrag einzeln zu Speichern. Wie ein dict() beispielhaft aufgebaut ist, seht ihr im folgenden Code Auszug:

# Beispiel Aufbau ohne der Funktion 'dict()'
example_dict_1 = {'Zahl': 1, 'Satz': 'Beispiel Satz in einem Dict'}

# Beispiel Aufbau mit der Funktion 'dict()'
example_dict_2 = dict([('Zahl', 1), ('Satz', 'Beispiel Satz in einem Dict')])

Vergleichen wir die verschiedenen Möglichkeiten ein Dictonary zu erstellen, so fällt auf, dass die erste Möglichkeit einfacher aufgebaut ist. Das eindeutige Erkennungsmerkmal für ein Dictonary sind die geschweiften Klammern. Ein Richtig oder Falsch wie man das Dictonary erstellt gibt es allerdings nicht.
Nachdem wir nun ein Dictonary erstellt haben, möchten wir Euch zuerst zeigen, wie man zum einen Elemente aufruft und zum anderen sie ersetzen kann. Abschließend seht ihr noch ein Beispiel wie man die Existenz eines Elements überprüft.

# Auswahl eines Elements aus einem dict
example_dict_1['Zahl']
# Ausgabe: 1

# Ändern des Inhaltes eines Elements aus einem dict
example_dict_2['Satz'] = 'Dies ist nun ein neuer Satz'

# Überprüfen der Existenz eines Elements in einem dict
'Satz' in example_dict_2

# Ausgabe: True, da Element in dict vorhanden
'Zahl1' in example_dict_2
# Ausgabe: False, da Element in dict vorhanden

Listen

Kommen wir nun zu unserer zweiten „Data Science“ Datenstruktur in Python: Listen. Sie lassen sich ähnlich zu Dictionaries in einer Zeile erstellen, zeichnen sich im Gegensatz aber dazu aus, dass sie keine feste Zuweisung von Elementen über einen Schlüssel vornehmen. Die Elemente einer Liste lassen sich daher über ihren Index aufrufen. An dieser Stelle eine kurze Anmerkung zu Indizierung in Python. Der Index beginnt mit der Zahl 0 und wird im Sinne der natürlichen Zahlen stufenweisen hochgezählt: 0,1,2,3,… der letzte Index kann zwar eine beliebige hohe, natürliche Zahl kann aber auch einfach über die Zahl -1 aufgerufen werden. Die Funktionsweise verdeutlichen wir gleich.
Erstellen wir eine Liste, so wird der Beginn und das Ende einer Liste durch eine eckige Klammer verdeutlicht. An dieser Stelle sei noch einmal betont, dass der Datentyp der in der Liste gespeichert wird, nicht für jedes Element identisch sein muss. Zahlen, Strings und Co. lassen sich beliebig mischen.

# Erstellen einer Liste
demo_list = [1, 2, 4, 5, 6, 'test']

Die Auswahl von Elementen gliedert sich in zwei Punkte auf:

  • Auswahl einzelner Elemente
  • Auswahl mehrere Elements

Ersteres funktioniert über den Index sehr einfach, für Zweiteres muss ein Doppelpunkt bis zu dem jeweiligen nächsten Index gesetzt werden. Möchte man also die ersten drei Elemente (Index: 0,1,2) auswählen, so muss der Index nach dem Doppelpunkt 3 betragen. Die Zuweisung von neuen Daten/Elementen an eine bestimmte Indexstelle einer Liste erfolgt ähnlich zu einem Dictionary.

# Auswahl eines Elements (genauer: Auswahl des ersten Elements)
demo_list[0]
# Ausgabe: 1

# Auswahl mehrerer Elementen (genauer: Auswahl der ersten drei Elemente)
demo_list[:3]
# Ausgabe: 1, 2, 3

# Auswahl des letzen Elements der Liste
demo_list[-1]
# Ausgabe: 'test'

# Zuweisung eines neuen Elements
demo_list[3] = 3
# Die Liste hat danach folgende Struktur [1, 2, 4, 3, 6, 'test']

Einen Nachteil von Listen ist allerdings, dass sie im Wesentlich nur zum Speichern von Daten geeignet sind. Einfache mathematische Funktionen können zwar von Element zu Element angewandt werden, für komplexe Matrizen- oder Vektor-Algebra bedarf es anderer Werkzeuge wie z.B. die Bibliothek NumPy.

Einführung in NumPy

NumPy ermöglicht es uns durch seine eingeführten Multi-Dimensionalen-Arrays (kurz ndarrays) ist, komplexe mathematische Operationen und Algorithmen einfach und effizient durchzuführen. Da NumPy im Normalfall nicht direkt installiert ist, müssen wir dieses z.B. durch pip oder conda manuell erledigen. Sofern eine aktuelle Python-Version ( >=3.3) installiert ist, sollte pip direkt verfügbar sein. Wir können sodann einfach im Terminal per pip install numpy btw. pip3 install numpy NumPy installieren. Für diejenigen die Anaconda nutzten, sollte NumPy direkt verfügbar sein. Um sicher zu gehen kann man jedoch per conda install NumPy sicherstellen, dass NumPy vorhanden ist bzw. es updaten.

Ein einfaches Beispiel kann zeigen wie effizient und nützlich NumPy ist. Nehmen wir an, wir haben einige Datenpunkte und wollen eine mathematische Operation vornehmen, wie etwa die Wurzel ziehen. Hierzu soll unsere Liste li dienen.

li = [1,3,5,6,7,6,4,3,4,5,6,7,5,3,2,1,3,5,7,8,6,4,2,3,5,6,7]

Da Pythons Mathe-Modul math jeweils nur eine Zahl als Input nimmt, bleibt uns nichts anderes übrig, als die Wurzel per Listcomprehension zu zuweisen. Listcomprehension ermöglich eine sehr kompakte Form der Listenerstellung.

import math
s = [math.sqrt(i) for i in li]

Währendessen können wir mit NumPy direkt auf dem gesamten Array mit einem geringen Aufwand arbeiten zu betreiben.

import numpy as np 
arr = np.array(li)
s = np.sqrt(arr) 

Wertet man die Laufzeiten der Operationen aus, so dauert es mit dem math Modul 3,3 Mikrosekunden, verwenden wir hingegen NumPy so reduziert sich die Laufzeit um ein Drittel auf 0,9 Mikrosekunden. Dieser Aspekt unterstreicht die effiziente Implementierung von Arrays in NumPy. Sie eignen sich daher sehr gut, um auch mit relativ vielen Daten gut zurecht zu kommen. Darüber hinaus ermöglichen eine Vielzahl von Funktionen, Möglichkeiten zur Konstruktion, Transformation und Restrukturierung von Arrays ohne vorab Listen zu definieren. Hierüber möchten wir Euch abschließend noch einen Überblick geben.
Eine Matrix mit Zufallszahlen können wir sehr schnell erstellen. Ist man sich über die Struktur seiner Daten unsicher, so kann man sich diese über das Attribut shape ausgeben lassen.

# 25x1 große Matrix mit einem Mittelwert von 20 und Standartabweichung von 10 
ran = np.random.randn(25,1) * 10 + 20

# Struktur eines Arrays/Matrix
print(ran.shape)

In der Praxis kommt es allerdings häufig vor, dass die vorliegenden Daten nicht unbedingt der gewünschten Struktur entsprechen. NumPy bietet für dieses Problem verschiedene Funktionen an, so können die Arrays mit reshape transformiert werden oder mit hstack/ vstack horizontal oder vertikal angeordnet werden. Bei reshape wird die gewünschte Struktur als Liste übergeben.

# Umstrukturierung der Zufallszahlen
ran = ran.reshape([5,5])

# Zweite Zufallsmatrix 
ran2 = np.random.randn(25,1) * 5 + 1

# Stapeln  zu 25x2
vstack = np.vstack([ran, ran2])

# Verbinden zu 50x1 
hstack= np.hstack([ran, ran2])

NumPy bildet somit ein solides Grundgerüst um schnell mit Zahlen zu hantieren. Für diejenigen, die Erfahrung mit linearer Algebra haben muss an dieser Stelle noch dazu gesagt werden, dass ndarrays keine Matrizen sind! Worauf ich hier hinaus will ist, dass ndarrays sich nicht wie Matrizen verhalten wenn es z.B. um Multiplikation geht. ndarrays multiplizieren Element für Element. Somit kann auch ein 4×1 Array quadriert werden ohne es zu transponieren. Jedoch lässt NumPy dennoch die Standard Matrizenmultiplikation zu mit der np.dot()-Funktion, oder der Operation @

# Element * Element 
np.ones([4,1]) * np.ones([4,1]) 

# oder Matrizenmultiplikation 
np.ones([1,4]) @ np.ones([4,1]) == np.dot(np.ones([1,4]) , np.ones([4,1]) ) == np.ones([1,4]).dot(np.ones([4,1]))

Fazit

In diesem Blogbeitrag haben wir die wesentlichen Datenstrukturen, die sich zum Arbeiten mit unterschiedlichen Datenelement eignen, kennengelernt diese sind Listen und Dictonary. Ihr solltet diese sowohl Erstellen wie auch Manipulieren können. Auch das Abrufen von Elementen sollte für Euch ab sofort kein Problem mehr darstellen. Für die Verarbeitung von Zahlen und Matrizen hat die Bibliothek NumPy bewiesen, dass sie eine performant Umsetzung von Berechnung ermöglicht.

Vorschau

Im nächsten Teil dieser Reihe werden wir uns noch etwas tiefer gehend mit NumPy beschäftigen. Da NumPy und die ndarrays den Kern der wissenschaftlichen Umgebung in Python darstellen und wir immer wieder auf Sie stoßen werden ist daher ein gutes Verständnis dieser von fast schon obligatorisch. Im folgenden Teil werden wir uns genauer mit den wichtigsten Eigenschaften – Attributen und Methoden – vertraut machen. Marvin Taschenberger Marvin Taschenberger

Teil 0 – Vorschau und Werkzeuge

In Sachen Datenaufbereitung, Datenformatierung und statistischer Auswertung oder kurz Data Science, war (und hier in Deutschland ist immer noch) R die Sprache der Wahl. Global hat Python hier deutlich an Popularität gewonnen und ist mittlerweile sogar vorherrschend in diesem Gebiet (siehe Studie von KDnuggets). Daher soll diese Reihe schon einmal einen Einblick geben „Warum Python?“, und wie die Sprache in Gebieten der „Datenwissenschaft“ funktioniert. Dementsprechend widmen wir uns hier zuerst einmal der Frage „Warum Python?“ sowie einer Beschreibung nützlicher Tools.

Danach folgt ein Blick auf die Datenstruktur in Python mit Pandas, sowie das mathematische Ökosystem mit NumPy und SciPy. Damit wir unsere Beobachtungen oder mathematischen Transformationen auch veranschaulichen können, schauen wir kurz auf Matplotlib. Am interessantesten ist jedoch die Validierung gegebener Hypothesen oder Vermutungen, die wir bezüglich unserer Daten haben, und welche wir mit Hilfe von Statsmodels oder SciKit-Learn (SKLearn) erledigen werden. Problematisch ist bis hier jedoch, dass es sich bei fast all diesen Modulen und Erweiterungen Pythons um sehr grundlegende Frameworks handelt. Mit anderen Worten, wenn man weiß, was man will und diese Werkzeuge einzusetzen weiß, sind sie überaus mächtig. Jedoch setzt dieses eine intensive Auseinandersetzung mit ihren Objekten und Funktionen voraus. Daher haben sich diverse Wrapper um diese Bibliotheken gelegt, um uns das Leben zu vereinfachen.

Was statistische Visualisierung angeht, erledigt Seaborn nun für uns das ‚heavy lifting‘ und wer seine Graphiken lieber interaktiv hat, dem helfen Bokeh und Altair. Selbst für das maschinelle Lernen (Maschine Learning, ML) gibt es zahlreiche Wrapper, wie zum Beispiel MlXtend für den klassischen Bereich oder Keras im Bereich DeepLearning.

Warum Python

Dementsprechend beginnt die Reise mit der Rechtfertigung ihrer selbst – Warum Python? Im Gegensatz zu R ist Python eine Sprache für jeglichen Zweck. Folglich ist Python nicht nur flexibler, was den Umgang mit nicht-numerischen Objekten angeht, sondern bietet durch seinen objektorientierten Programmieransatz (OOP) die Möglichkeit jedes Objekt frei zu manipulieren oder zu kreieren. Diese zeigt sich vor allem dann, wenn ein Datensatz entweder sehr ‚unsauber‘ ist oder die in ihm enthaltenen Informationen nicht ausreichen. In Python stehen einem zahlreiche Bibliotheken offen, um Daten auf nicht konventionelle Methode zu bereinigen oder das World Wide Web direkt nach Informationen zu durchforsten oder zu scrappen. Zwar kann dies auch in R getan werden, allerdings nur unter gehörigem Mehraufwand. Sobald man sich jedoch noch weiter weg von den eigentlichen Daten bewegt (z.B. Einrichtung einer API) muss sich R komplett geschlagen geben.

In diesem Zuge möchte ich noch einmal betonen, was numerische Objekte und den Umgang mit (relativ sauberen) Daten angeht, wird Python immer etwas hinter R hinterherhinken. Zum einen weil R genau für diesen Zweck geschrieben wurde und zum anderen, weil neuste Forschungsergebnisse zuerst in R umgesetzt werden. Genau hier kann man sich entscheiden welche Sprache einem mehr bietet. Will man immer up to date sein, neuste Methoden und Algorithmen anwenden und sich dabei auf das Wesentliche konzentrieren, dann ist wahrscheinlich R die bessere Wahl. Will man jedoch eher Daten-Pipelines bauen, für jedes Problem eine Antwort finden und dabei immer nahe an seinen Daten sein, dann ist man wohl mit Python besser beraten.

Werkzeuge

Bevor wir uns nun Pandas und dem Kernbereich von Data Science widmen, möchte ich noch ein Multifunktionstool vorstellen, welches für mich im Alltag geradezu unentbehrlich geworden ist: Jupyter.

Es ist eine Erweiterung der IPython Konsole und ermöglicht einem seinen Code in verschiedenen Zellen zu schreiben, diesen auszuführen und somit auch lange Skripte effektiv zu unterteilen.

#definition einer Funktion in einer Zelle
def sayhello(to ):
    'Print hello to somboday'
    print(f'Hello {to}')

def returnhello(to):
    'Return a string which says Hello to somboday'
    return f'Hello {to}'    
# Introspektion des Docstrings
returnhello?
# Aufruf dieser Funktion in einer weiteren Zelle 
# Schreibt die Nachricht 
sayhello('Statworx-Blog')
Hello Statworx-Blog
# gibt ein Objekt zurück welches vom Notebook formatiert wird 
returnhello('Statworx-Blog') 
'Hello Statworx-Blog'

Das besondere hierbei ist sowohl das interaktive Element, z.B. kann man sich jederzeit schnell und einfach Feedback geben lassen wie ein Objekt aussieht, als auch IDE Elemente wie die Introspektion durch ?. Jupyter kann vor allem durch seine schöne Visualisierung punkten. Wie man in der dritten Zelle (returnhello('Statworx-Blog')) sehen kann, wird immer das letzte Objekt einer Zelle visualisiert. In diesem Fall mag es nur ein String gewesen sein, den wir natürlich mit print einfach ausgeben lassen können. Jedoch werden wir im weiteren Verlauf sehen, dass gerade diese Visualisierung bei Daten unheimlich nützlich ist. Darüber hinaus können die Blöcke aber auch anderes interpretiert werden. Beispielhaft kann man sie im Rohzustand belassen ohne zu kompilieren oder als Markdown verwenden, um den Code nebenbei zu dokumentieren (oder diesen Beitrag hier zu schreiben).

Da Jupyter Notebooks letzten Endes auf der IPython Shell basieren, können wir natürlich auch sämtliche Features und magics benutzten. Hierdurch können automatische Vervollständigungen genutzt werden. Man erhält Zugang zu den Docstring der einzelnen Funktionen und Klassen sowie die Zeilen- und Zellen-magics in IPython. Einzelne Blöcke sind hierdurch für mehr als nur Python Code verwendbar. Zum Beispiel kann Code schnell und einfach auf seine Geschwindigkeit geprüft werden mit %timeit, die System Shell direkt mit %%bash angesprochen oder deren Output direkt aufgenommen werden mit ! am Anfang der Zeile. Darüber hinaus könnte auch Ruby-Code durch %%ruby, Javascript mit %%js ausgeführt oder sogar zwischen Python2 und Python3 %%python2 & %%python3 in der jeweiligen Zelle gewechselt werden.

%timeit returnhello('Statworx-Blog')
170 ns ± 3.77 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%%bash
ls
Blog_Python.html
Blog_Python.ipynb
Blog_Python.md

Eine Anmerkung, die hier auch nicht fehlen darf, ist, dass Jupyter nicht nur exklusiv für Python ist, sondern auch mit R oder Julia verwendet werden kann. Die jeweiligen Kerne sind zwar nicht ‚Out of the Box‘ enthalten, aber sofern man eine andere Sprache benutzen möchte / soll, ist es in meinen Augen den geringen Aufwand wert.

Wem das Ganze nicht gefällt und lieber eine gute alte IDE für Python haben möchte, dem stehen eine Vielzahl an Möglichkeiten offen. Diverse Multi-Sprachen IDE’s mit einem Python-Plug-In (Visuale Studio, Eclipse, etc.) oder jene, die direkt auf Python zugeschnitten sind. Zum letzteren gehören PyCharm und Spyder.

PyCharm wurde von JetBrains entworfen und ist in zwei Versionen erhältlich, der Community Version (CE) und der Pro-Version. Während die CE ein opensource Projekt mit ’nur‘ intelligentem Editor, graphischen Debugger, VSC-Support und Introspektion ist, enthält die Pro Version gegen ein Entgelt einen Support für Web-Devolopment. Während so die Pflege der IDE gesichert ist, fehlt mir persönlich die Optimierung für Data Science spezifische Anwendung (IPython Konsole). Spyder jedoch, als wissenschaftliches opensource Projekt, bietet alles was man sich wünscht. Ein Editor (nicht ganz so intelligent wie die JetBrains Version) für Skripte, einen Explorer für Variablen und Dateien und eine IPython Konsole.

Das letzte Tool, welches benötigt wird und nicht fehlen darf, ist ein Editor. Dieser sollte vor allem schnell sein, um jede Datei zumindest im Roh-Format öffnen zu können. Nötig ist dieses, da man oft durch einen kurzen Blick z.B. in CSV-files schnell einen Eindruck über die Kodierung bekommt: welches Zeichen trennt Zeilen und welches trennt Spalten. Zum anderen kann man schnell und einfach andere Skripte aufrufen, ohne dass man Warten muss bis die IDE geladen ist.

Vorschau

Da wir nun ein Arsenal an Werkzeugen haben, um uns ins Datamunging zu stürzen, können wir uns im nächsten Teil dieser Reihe direkt mit Pandas, seinen DataFrames und dem mathematischen Ökosystem befassen. Marvin Taschenberger Marvin Taschenberger

Teil 0 – Vorschau und Werkzeuge

In Sachen Datenaufbereitung, Datenformatierung und statistischer Auswertung oder kurz Data Science, war (und hier in Deutschland ist immer noch) R die Sprache der Wahl. Global hat Python hier deutlich an Popularität gewonnen und ist mittlerweile sogar vorherrschend in diesem Gebiet (siehe Studie von KDnuggets). Daher soll diese Reihe schon einmal einen Einblick geben „Warum Python?“, und wie die Sprache in Gebieten der „Datenwissenschaft“ funktioniert. Dementsprechend widmen wir uns hier zuerst einmal der Frage „Warum Python?“ sowie einer Beschreibung nützlicher Tools.

Danach folgt ein Blick auf die Datenstruktur in Python mit Pandas, sowie das mathematische Ökosystem mit NumPy und SciPy. Damit wir unsere Beobachtungen oder mathematischen Transformationen auch veranschaulichen können, schauen wir kurz auf Matplotlib. Am interessantesten ist jedoch die Validierung gegebener Hypothesen oder Vermutungen, die wir bezüglich unserer Daten haben, und welche wir mit Hilfe von Statsmodels oder SciKit-Learn (SKLearn) erledigen werden. Problematisch ist bis hier jedoch, dass es sich bei fast all diesen Modulen und Erweiterungen Pythons um sehr grundlegende Frameworks handelt. Mit anderen Worten, wenn man weiß, was man will und diese Werkzeuge einzusetzen weiß, sind sie überaus mächtig. Jedoch setzt dieses eine intensive Auseinandersetzung mit ihren Objekten und Funktionen voraus. Daher haben sich diverse Wrapper um diese Bibliotheken gelegt, um uns das Leben zu vereinfachen.

Was statistische Visualisierung angeht, erledigt Seaborn nun für uns das ‚heavy lifting‘ und wer seine Graphiken lieber interaktiv hat, dem helfen Bokeh und Altair. Selbst für das maschinelle Lernen (Maschine Learning, ML) gibt es zahlreiche Wrapper, wie zum Beispiel MlXtend für den klassischen Bereich oder Keras im Bereich DeepLearning.

Warum Python

Dementsprechend beginnt die Reise mit der Rechtfertigung ihrer selbst – Warum Python? Im Gegensatz zu R ist Python eine Sprache für jeglichen Zweck. Folglich ist Python nicht nur flexibler, was den Umgang mit nicht-numerischen Objekten angeht, sondern bietet durch seinen objektorientierten Programmieransatz (OOP) die Möglichkeit jedes Objekt frei zu manipulieren oder zu kreieren. Diese zeigt sich vor allem dann, wenn ein Datensatz entweder sehr ‚unsauber‘ ist oder die in ihm enthaltenen Informationen nicht ausreichen. In Python stehen einem zahlreiche Bibliotheken offen, um Daten auf nicht konventionelle Methode zu bereinigen oder das World Wide Web direkt nach Informationen zu durchforsten oder zu scrappen. Zwar kann dies auch in R getan werden, allerdings nur unter gehörigem Mehraufwand. Sobald man sich jedoch noch weiter weg von den eigentlichen Daten bewegt (z.B. Einrichtung einer API) muss sich R komplett geschlagen geben.

In diesem Zuge möchte ich noch einmal betonen, was numerische Objekte und den Umgang mit (relativ sauberen) Daten angeht, wird Python immer etwas hinter R hinterherhinken. Zum einen weil R genau für diesen Zweck geschrieben wurde und zum anderen, weil neuste Forschungsergebnisse zuerst in R umgesetzt werden. Genau hier kann man sich entscheiden welche Sprache einem mehr bietet. Will man immer up to date sein, neuste Methoden und Algorithmen anwenden und sich dabei auf das Wesentliche konzentrieren, dann ist wahrscheinlich R die bessere Wahl. Will man jedoch eher Daten-Pipelines bauen, für jedes Problem eine Antwort finden und dabei immer nahe an seinen Daten sein, dann ist man wohl mit Python besser beraten.

Werkzeuge

Bevor wir uns nun Pandas und dem Kernbereich von Data Science widmen, möchte ich noch ein Multifunktionstool vorstellen, welches für mich im Alltag geradezu unentbehrlich geworden ist: Jupyter.

Es ist eine Erweiterung der IPython Konsole und ermöglicht einem seinen Code in verschiedenen Zellen zu schreiben, diesen auszuführen und somit auch lange Skripte effektiv zu unterteilen.

#definition einer Funktion in einer Zelle
def sayhello(to ):
    'Print hello to somboday'
    print(f'Hello {to}')

def returnhello(to):
    'Return a string which says Hello to somboday'
    return f'Hello {to}'    
# Introspektion des Docstrings
returnhello?
# Aufruf dieser Funktion in einer weiteren Zelle 
# Schreibt die Nachricht 
sayhello('Statworx-Blog')
Hello Statworx-Blog
# gibt ein Objekt zurück welches vom Notebook formatiert wird 
returnhello('Statworx-Blog') 
'Hello Statworx-Blog'

Das besondere hierbei ist sowohl das interaktive Element, z.B. kann man sich jederzeit schnell und einfach Feedback geben lassen wie ein Objekt aussieht, als auch IDE Elemente wie die Introspektion durch ?. Jupyter kann vor allem durch seine schöne Visualisierung punkten. Wie man in der dritten Zelle (returnhello('Statworx-Blog')) sehen kann, wird immer das letzte Objekt einer Zelle visualisiert. In diesem Fall mag es nur ein String gewesen sein, den wir natürlich mit print einfach ausgeben lassen können. Jedoch werden wir im weiteren Verlauf sehen, dass gerade diese Visualisierung bei Daten unheimlich nützlich ist. Darüber hinaus können die Blöcke aber auch anderes interpretiert werden. Beispielhaft kann man sie im Rohzustand belassen ohne zu kompilieren oder als Markdown verwenden, um den Code nebenbei zu dokumentieren (oder diesen Beitrag hier zu schreiben).

Da Jupyter Notebooks letzten Endes auf der IPython Shell basieren, können wir natürlich auch sämtliche Features und magics benutzten. Hierdurch können automatische Vervollständigungen genutzt werden. Man erhält Zugang zu den Docstring der einzelnen Funktionen und Klassen sowie die Zeilen- und Zellen-magics in IPython. Einzelne Blöcke sind hierdurch für mehr als nur Python Code verwendbar. Zum Beispiel kann Code schnell und einfach auf seine Geschwindigkeit geprüft werden mit %timeit, die System Shell direkt mit %%bash angesprochen oder deren Output direkt aufgenommen werden mit ! am Anfang der Zeile. Darüber hinaus könnte auch Ruby-Code durch %%ruby, Javascript mit %%js ausgeführt oder sogar zwischen Python2 und Python3 %%python2 & %%python3 in der jeweiligen Zelle gewechselt werden.

%timeit returnhello('Statworx-Blog')
170 ns ± 3.77 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%%bash
ls
Blog_Python.html
Blog_Python.ipynb
Blog_Python.md

Eine Anmerkung, die hier auch nicht fehlen darf, ist, dass Jupyter nicht nur exklusiv für Python ist, sondern auch mit R oder Julia verwendet werden kann. Die jeweiligen Kerne sind zwar nicht ‚Out of the Box‘ enthalten, aber sofern man eine andere Sprache benutzen möchte / soll, ist es in meinen Augen den geringen Aufwand wert.

Wem das Ganze nicht gefällt und lieber eine gute alte IDE für Python haben möchte, dem stehen eine Vielzahl an Möglichkeiten offen. Diverse Multi-Sprachen IDE’s mit einem Python-Plug-In (Visuale Studio, Eclipse, etc.) oder jene, die direkt auf Python zugeschnitten sind. Zum letzteren gehören PyCharm und Spyder.

PyCharm wurde von JetBrains entworfen und ist in zwei Versionen erhältlich, der Community Version (CE) und der Pro-Version. Während die CE ein opensource Projekt mit ’nur‘ intelligentem Editor, graphischen Debugger, VSC-Support und Introspektion ist, enthält die Pro Version gegen ein Entgelt einen Support für Web-Devolopment. Während so die Pflege der IDE gesichert ist, fehlt mir persönlich die Optimierung für Data Science spezifische Anwendung (IPython Konsole). Spyder jedoch, als wissenschaftliches opensource Projekt, bietet alles was man sich wünscht. Ein Editor (nicht ganz so intelligent wie die JetBrains Version) für Skripte, einen Explorer für Variablen und Dateien und eine IPython Konsole.

Das letzte Tool, welches benötigt wird und nicht fehlen darf, ist ein Editor. Dieser sollte vor allem schnell sein, um jede Datei zumindest im Roh-Format öffnen zu können. Nötig ist dieses, da man oft durch einen kurzen Blick z.B. in CSV-files schnell einen Eindruck über die Kodierung bekommt: welches Zeichen trennt Zeilen und welches trennt Spalten. Zum anderen kann man schnell und einfach andere Skripte aufrufen, ohne dass man Warten muss bis die IDE geladen ist.

Vorschau

Da wir nun ein Arsenal an Werkzeugen haben, um uns ins Datamunging zu stürzen, können wir uns im nächsten Teil dieser Reihe direkt mit Pandas, seinen DataFrames und dem mathematischen Ökosystem befassen. Marvin Taschenberger Marvin Taschenberger