Alter der DataFrames 2: Polars Ausgabe

23.05.2024

Wouter Gins

In dieser Veröffentlichung präsentiere ich einige Tricks und Funktionen von Polars.

Als eines der neueren Kids auf dem Block bietet Polars eine spannende Alternative zu PySpark für kleine bis mittelgroße Datensätze, wie bereits in anderen Blogbeiträgen nachgewiesen. Hier werden wir unsere Füße in das Wasser tauchen mit einem fokussierteren Thema, um zu zeigen, wie leistungsstark und ausdrucksstark Polars sein kann: E-Sports-Vorhersagen. Wir werden einen Blick auf ein Turnier des Videospiels Age of Empires II: Definitive Edition (AoE) werfen und versuchen, eine Vorhersage zu treffen, wie die erste Runde verlaufen wird. Abgesehen von einem ähnlichen Alter (Polars hatte sein erstes Commit im Juni 2020, AoE wurde im November 2019 veröffentlicht), hat die AoE-Community viele Community-Projekte hervorgebracht, um riesige Datenbanken zu pflegen, was es ideal für ein kleines Projekt wie dieses macht. Ich hoffe, dass Sie am Ende dieses Blogbeitrags meine Freude über die Funktionen von Polars, die einfach funktionieren 😄, teilen werden.

Um eine Vorhersage zu treffen, werde ich zuerst einige der Daten betrachten, die von der Community gesammelt wurden, dann ein einfaches Modell zur Berechnung einer Gewinnchance erstellen und schließlich dieses Modell auf das Turnier anwenden.

Wenn Sie mit diesen Konzepten experimentieren möchten, hier ist ein Link zum GitHub-Repo, mit dem Sie die Daten herunterladen und starten können.

📖 Datenquelle

Für unsere Daten haben wir von aoestats.io gescraped, das eine Datenbank von gespielten Online-Spielen pflegt. Ich interessiere mich derzeit nur für offiziell gerankte Spiele, was entspricht, dass raw_match_type zwischen 6 und 9 (einschließlich) liegt. Ich werde dies als Filter für das Lesen hinzufügen.

Ich habe die Daten bereits vorverarbeitet und ein wenig bereinigt. Nach der Filterung nach dem Spieltyp sehen die Daten so aus:

df_m = (
    (pl.scan_parquet("data/aoe2/matches/*/*.parquet"))
    .filter(pl.col("raw_match_type").is_between(6, 9))
    .select(pl.all().shrink_dtype())
)
df_m_c = df_m.collect()
print(f"Match data is about {df_m_c.estimated_size('gb'):.2f} GB")

Ein pfiffiger Trick, den Polars bietet, ist der .shrink_dtype()-Ausdruck, der numerische Typen auf die kleinste Version reduziert, die die derzeitigen Werte im DataFrame unterstützt. Sehr nützlich, um den Speicherverbrauch zu minimieren!

Warum filtern: Wachsame Leser werden auch von der Existenz des Semi-Joins wissen, den ich auch hätte verwenden können, um die Daten zu filtern, indem ich ein kleines DataFrame mit einer raw_match_type-Spalte erstelle. In Tests fand ich einen minimalen Leistungsunterschied zwischen den beiden (0,4s gegenüber 0,6s). Mein Fazit hier ist, dass der Semi-Join wahrscheinlich etwas Overhead hat. Für Filterung über größere Mengen von Werten oder über Werte, die vorher nicht bekannt sind, würde ich den Semi-Join empfehlen.

Nun, neben den Matches benötige ich auch die Daten der Spieler, die an diesen Spielen beteiligt sind. Dies ist ein idealer Fall für den Semi-Join!

df_p = pl.scan_parquet("data/aoe2/player/*/*.parquet")
df_p_c = (
    df_p.join(other=df_m_c.lazy(), on="game_id", how="semi")
    .collect()
    .select(pl.all().shrink_dtype())
)
print(f"Player data is about {df_p_c.estimated_size('gb'):.2f} GB")

Da der Datensatz fast 12 Millionen gespielte Spiele und 42 Millionen Aufzeichnungen von Spielern in diesen Spielen enthält, ist der Datensatz groß genug, um herumzuspielen und gerade klein genug, um auf meinem bescheidenen Laptop gespeichert zu werden (wenn ich Chrome schließe 😉).

🔮 Gewinnchance-Vorhersage: Erstellung einer Nachschlagetabelle

Von besonderem Interesse in diesem Datensatz sind die rating oder elo-Spalten, die eine Zahl darstellen, die zeigt, wie stark ein Spieler ist. Dieses Konzept stammt aus dem Schach und wird in verschiedenen E-Sports weit verbreitet genutzt, um Spieler zu bewerten. Als naive Schätzung für die Gewinnchance eines Spielers können wir den Unterschied in der Bewertung verwenden. Lassen Sie uns zuerst die Daten transformieren, damit sie einem Klassifikator zugeführt werden können, und dann eine Nachschlagetabelle für den Bewertungsunterschied erstellen:

rating_diffs = (
    df_p_c.join(
        other=df_m_c.filter(
            raw_match_type=6 # For filtering a column on equality, Polars offers some syntactical sugar
        ),  # Restrict the data to 1-vs-1 games to get a more accurate image
        on="game_id",
        how="inner",
    )
    .select(
        "match_rating_diff", "winner"
    )  # We only want the rating difference and the winner-flag
    .drop_nulls()  # Placement matches have players with no rating calculated yet, can be removed
)
rating_diffs

Für den Klassifikator habe ich einfach den Gaussian Naive Bayes-Klassifikator verwendet, der in scikit-learn verfügbar ist. Dies ist nicht unbedingt die am besten geeignete Wahl, und ich werde die Daten auch nicht in ein Trainings- und Testset aufteilen. Um gute Modelle zu erstellen, sind diese Schritte absolut entscheidend, aber ich möchte mich darauf konzentrieren, wie man Polars verwendet und nicht auf Machine Learning 😉 Nur zum Spaß habe ich das Modell auch bewertet, um zu sehen, wie es abschneidet:

from sklearn.naive_bayes import GaussianNB
import numpy as np

gnb = GaussianNB()
gnb.fit(X=rating_diffs.select("match_rating_diff"),
        y=rating_diffs.select("winner")
       )
gnb.score(X=rating_diffs.select("match_rating_diff"),
          y=rating_diffs.select("winner")
         )
>> 0.5476460560275515

Unser Klassifikator erzielt 55%, also etwas besser als ein einfacher Münzwurf! Die Klassifikation wählt aus, wer die höhere Gewinnquote hat, was etwas langweilig ist. Das Modell kann jedoch auch die Gewinnquote angeben, was nicht so langweilig ist! Lassen Sie uns dies in Schritten von 5 ELO-Punkten über einen anständigen Bereich auswerten, damit wir sehen können, wie sich die Gewinnchance sanft ändert. Da wir mit dem Klassifikator interagieren müssen, ist die Polars-Version einer benutzerdefinierten Funktion erforderlich, die entweder map_elements(...) oder map_batches(...) sein kann. Der genaue Unterschied ist ziemlich nuanciert, und es gibt einen großen Eintrag in der Dokumentation. In dieser Situation kann map_batches verwendet werden, um alle Daten auf einmal auszuwerten, anstatt Datensatz für Datensatz auszuwerten.

Latest

Why not to build your own data platform

A round-table discussion summary on imec’s approach to their data platform

Securely use Snowflake from VS Code in the browser
Securely use Snowflake from VS Code in the browser
Securely use Snowflake from VS Code in the browser

Securely use Snowflake from VS Code in the browser

A primary activity among our users involves utilizing dbt within the IDE environment.

The benefits of a data platform team
The benefits of a data platform team
The benefits of a data platform team

The benefits of a data platform team

For years, organizations have been building and using data platforms to get value out of data.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Belgien

Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen


USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm,
Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland


© 2025 Dataminded. Alle Rechte vorbehalten.


Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen

USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm, Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland

© 2025 Dataminded. Alle Rechte vorbehalten.


Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen

USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm, Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland

© 2025 Dataminded. Alle Rechte vorbehalten.