Zum Hauptinhalt springen

Polars: schneller entwickeln, schneller ausführen


Polars, der in Rust geschriebene Pandas-Herausforderer, sorgt für erhebliche Beschleunigung nicht nur in der Ausführung des Codes, sondern auch in der Entwicklung. Pandas krankt seit jeher an einer API, die an vielen Stellen „historisch gewachsen“ ist. Ganz anders Polars: Eine API, die von Anfang an auf logische Konsistenz ausgelegt ist und deren Stringenz mit jedem Release sorgfältig gepflegt wird (im Zweifelsfall auch unter Verlusten an Rückwärtskompatibilität), sorgt für eine erheblich schnellere Entwicklung. An vielen Stellen, wo man bisher Pandas eingesetzt hat, kann man es problem los durch Polars ersetzen: In Ibis-Analytics-Projekten, und natürlich einfach für die tägliche Datenaufbereitung aller Art. Gut macht sich die überlegene Performance auch in interaktiven Umfeldern wie PowerBI 

Polars-Plugins: eine unterschätzte Ressource

Obwohl Polars noch ein junges Projekt ist, gibt es bereits erste Ansätze zu einem Ökosystem an Plugins. Eines davon ist besonders nützlich in einer Situation, in der ich beispielsweise zu einem Dataframe mit geographischen Positionen von U-Bahn-Haltestellen jeweils alle Bushaltestellen (in einem zweiten Dataframe) finden möchte, die höchstens 500m entfernt sind. So etwas führt zu einem kartesischen Produkt (Cross Join) zwischen den beiden Dataframes, das ich anschließend mit Hilfe der Abstände zwischen U-Bahn- und Bushaltestellen filtern muss. 

Kartesische Produkte werden selbst dann sehr groß, wenn die beteiligten Dataframes recht moderate Größen haben. Darum ist es wichtig, den anschließenden Filterschritt, der typischerweise den allergrößten Teil dieser Datenflut verwirft, sehr performant durchführen zu können. Wir brauchen also eine schnelle Berechnung des geographischen Abstands zwischen zwei Punkten. Uns reicht der sogenannte Haversine-Abstand, eine Näherungsformel für den Luftlinienabstand zwischen zwei Punkten auf der Erde. Die Formel tut so, als wäre die Erde eine Kugel. In Wahrheit ist sie nicht ganz kugelförmig, sondern an den Polen abgeplattet und überhaupt etwas kartoffelähnlich unregelmäßig. Für die meisten Zwecke ist diese Näherung aber genau genug. Und natürlich gibt es in Python Packages dafür. Nur leider ist die Berechnung furchtbar langsam, wenn man sich damit durch ein kartesisches Produkt quält.

Polars-distance rettet den Tag

Glücklicherweise gibt es das Polars-Plugin polars-distance von Ion Koutsouris, das diese Funktion in Rust implementiert und von Polars aus direkt zugänglich macht. Gegenüber einer Python-Implementation verbessert das die Performance enorm. Konkret: Ich hatte neulich die Aufgabe, zu jedem der über 8.000 Postleitzahlgebiete in Deutschland alle anderen Postleitzahlgebiete zu finden, deren Mittelpunkt höchstens eine gewisse Maximalanzahl an Kilometern entfernt ist. Zunächst versuchte ich die Python-Variante (mit einer zugegeben etwas genaueren und aufwendigeren Näherung der Entfernung, der geodesic-Funktion aus geopy). Nach 20 Minuten brach ich die Ausführung ab, weil ich keine Lust mehr hatte zu warten. Mit polars-distance dagegen war das Ganze nach zwei Sekunden (!) erledigt. Abbildung 1 zeigt ein Beispiel für ein Ergebnis, in diesem Fall alle Postleitzahlgebiete (gelb), die höchstens 30 km von der Bremer Innenstadt (PLZ-Gebiet 28203, der kleine orange Fleck in der Mitte) entfernt sind.

Abbildung 1

Zeit für etwas Code

Wie sieht der Join für das Postleitzahlenbeispiel konkret aus? Nehmen wir mal an, dass unsere Postleitzahlengebiete in einem Polars-Dataframe „postcode2center“, der eine String-Spalte („postcode“) mit der Postleitzahl hat und eine Spalte „center“, die Längen- und Breitengrad des Mittelpunkts des Postleitzahlgebiets enthält.

Die Spalte „center“ ist ein Polars-Struct mit zwei Float64-Feldern, nämlich „latitude“ und „longitude“. Der Maximalabstand, den wir zu akzeptieren gewillt sind, steht in der Variablen „radius_km“. 


 

Ganz am Anfang („.lazy()“) und ganz am Ende („.collect(streaming=True)“) benutzen wir ein weiteres Polars-Feature, das bei Pandas fehlt: die lazy API. Sie sorgt hier dafür, dass uns der Prozess selbst dann nicht um die Ohren fliegt, wenn unser Hauptspeicher für die über 64 Millionen Zeilen des kartesischen Produkts ein wenig zu klein sein sollte. Der Einsatz von polars-distance (das wir hier als pld importiert haben) ist dann schnell erledigt in der Zeile, die mit „.with_columns“ anfängt. Dort wird eine neue Spalte mit dem Namen „distance_km“ erzeugt, nach der wir in der nächsten Zeile filtern. Man hätte das auch in einen Befehl packen können, aber so ist es lesbarer. Am Schluss erhalten wir einen Dataframe mit der Stringspalte „postcode“ und einer Spalte „postcodes_within_radius“, die die Postleitzahlgebiete in der Umgebung als Liste von Strings enthält.

Polars-distance: nicht nur für Geodaten

Das Plugin ist aber nicht nur für Geodaten nützlich. Auch wenn man zum Beispiel in einer Liste von Freitextfeldern alle Felder mit ähnlicher Schreibweise finden will, hat man dasselbe Problem: Man muss ein kartesisches Produkt schnell mit Hilfe eines Abstandsbegriffs filtern. polars-distance bietet hier den Levenshtein-Abstand an, der für solche Textoperationen erfunden wurde, sowie diverse Variationen davon. Auch Abstandsbegriffe für Listen bringt polars-distance mit. Die vollständige Liste findet sich in der Dokumentation. Also: Wenn ihr mal wieder ein Bedürfnis nach Abstand verspürt, schaut mal, ob euch polars-distance weiterhelfen kann! Und wenn ihr Fragen habt rund um den Übergang von Pandas zu Polars in eurem Unternehmen, sprecht uns gern an.

Dr. Michael Allgöwer
Dein Ansprechpartner
Dr. Michael Allgöwer
Management Consultant
Machine Learning ist Michaels langjährige Spielwiese. Michael ist überzeugt, dass gutes Machine Learning eine Menge Branchenverständnis voraussetzt, und er liebt es, sich dieses Verständnis immer wieder zu erarbeiten. Sein neuestes Lieblingsthema ist Reinforcement Learning.
#MachineLearning #ReinforcementLearning #HumanIntelligence