Polare gegen Pandas

# Einführung

Im letzten Jahrzehnt Pandas conflict die Grundlage für die Datenarbeit in Python. Bei Datensätzen, die in den Speicher passen, ist es schnell und vertraut genug, dass einem Programmierer kaum in den Sinn kommt, die Bibliothek zu wechseln.

Sobald Sie jedoch mit der Arbeit mit Millionen von Zeilen beginnen, treten die Schwachstellen auf: Groupby-Vorgänge, die mehrere Sekunden dauern, Zwischenkopien, die RAM verbrauchen, und Fensterfunktionen, die als Schleifen auf Python-Ebene statt vektorisiert ausgeführt werden C oder Rost Code.

Polaren ist eine auf Rust basierende DataFrame-Bibliothek Apache-Pfeil. Es wurde mit den erstklassigen Merkmalen Parallelität und verzögerte Auswertung entwickelt. Pandas führt jeden Vorgang im Voraus und nacheinander aus, während Polars einen Abfrageplan erstellen und ihn vor der Ausführung optimieren kann, wobei die meisten Vorgänge automatisch auf allen verfügbaren CPU-Kernen gleichzeitig ausgeführt werden.

In diesem Artikel untersuchen wir drei reale Datenprobleme anhand realer Fragen aus dem StrataScratch Codierungsplattform. Für jedes Downside vergleichen wir die Lösungen beider Bibliotheken und zeigen auf, wo der Leistungsunterschied am wichtigsten ist.

Polare gegen Pandas

# Verwendung von rank() vs. with_row_count(): Aktivitätsrang

In diese FrageZiel ist es, den E-Mail-Aktivitätsrang für jeden Benutzer basierend auf der Gesamtzahl der gesendeten E-Mails zu ermitteln. Der Benutzer mit den meisten E-Mails erhält Rang 1. Die Ergebnisse müssen nach der Gesamtzahl der E-Mails in absteigender Reihenfolge sortiert werden, wobei die alphabetische Reihenfolge als Tiebreaker verwendet wird, und jeder Rang muss eindeutig sein, auch wenn zwei Benutzer die gleiche E-Mail-Anzahl haben.

// Datenansicht

Der google_gmail_emails Die Tabelle speichert eine Zeile professional gesendeter E-Mail mit einer Absender-ID (from_user), Empfänger-ID (to_user) und den Tag, an dem die E-Mail gesendet wurde. Hier ist eine Vorschau der Tabelle:

Ausweis from_user to_user Tag
0 6edf0be4b2267df1fa 75d295377a46f83236 10
1 6edf0be4b2267df1fa 32ded68d89443e808 6
2 6edf0be4b2267df1fa 55e60cfcc9dc49c17e 10
3 6edf0be4b2267df1fa e0e0defbb9ec47f6f7 6
4
314 e6088004caf0c8cc51 e6088004caf0c8cc51 5

Getreide (was eine Ausgabezeile bedeutet): ein Benutzer mit seiner gesamten E-Mail-Anzahl und seinem eindeutigen Aktivitätsrang.

// Häufiger Fehler

Die Frage fragt nach einem eindeutigen Rang, auch wenn zwei Benutzer die gleiche E-Mail-Anzahl haben. Ein häufiger Fehler ist die Verwendung von rank(technique='dense') Methode in Pandas, die gleichrangigen Benutzern den gleichen Rang zuweist. Die richtige Methode ist 'first'wodurch Bindungen nach Place im sortierten Rahmen aufgelöst werden. Da wir alphabetisch nach sortieren user_id Vor dem Rating sind die resultierenden Ränge eindeutig und deterministisch.

Die optimale Lösung von Polar vermeidet das rank voll funktionsfähig. Nach dem Sortieren nach ("total_emails", "user_id") in absteigender bzw. aufsteigender Reihenfolge .with_row_count("activity_rank", offset=1) Die Klausel weist sequentielle Ganzzahlen beginnend bei 1 zu. Es ist keine Tie-Breaking-Logik erforderlich, da die Sortierung dies bereits verarbeitet hat.

// Lösungen

1. Pandas-Lösung

Wir benennen um from_user Zu user_idnach Benutzer gruppieren, E-Mails zählen, den ersten Rang berechnen und nach E-Mail-Anzahl in absteigender Reihenfolge sortieren, mit alphabetischer Gleichstandstrennung.

import pandas as pd
import numpy as np
google_gmail_emails = google_gmail_emails.rename(columns={"from_user": "user_id"})
end result = google_gmail_emails.groupby(
    ('user_id')).dimension().to_frame('total_emails').reset_index()
end result('activity_rank') = end result('total_emails').rank(technique='first', ascending=False)
end result = end result.sort_values(by=('total_emails', 'user_id'), ascending=(False, True))

2. Polare Lösung

Wir verwenden a faule Kette das in einem einzigen Durchgang umbenennt, gruppiert, sortiert und Zeilennummern zuweist. Berufung .acquire() Am Ende materialisiert sich das Ergebnis.

import polars as pl
google_gmail_emails = google_gmail_emails.rename({"from_user": "user_id"})
end result = (
    google_gmail_emails.lazy()
    .group_by("user_id")
    .agg(total_emails = pl.depend())
    .kind(
        by=("total_emails", "user_id"),
        descending=(True, False)
    )
    .with_row_count("activity_rank", offset=1)
    .choose((
        pl.col("user_id"),
        "total_emails",
        "activity_rank"
    ))
    .acquire()
)

// Leistungsvergleich

Polare gegen Pandas

Die Pandas-Lösung durchläuft die Daten nach der Gruppierung zweimal: einmal zur Berechnung der Größen und einmal zur Zuweisung von Rängen. Innen, rank(technique='first') weist ein Rangarray zu, löst Bindungen über argsort auf und schreibt zurück – was erheblich aufwendiger ist als die Suche nach einer einzelnen Spalte. Die Polaren group_by Die Funktion verteilt die Arbeitslast auf alle verfügbaren CPU-Kerne, was zu einer deutlich schnelleren Aggregation bei großen Tabellen führt. Und seit dem .with_row_count() Die Klausel ist ein einzelner sequenzieller O(n)-Durchlauf nach der Sortierung und ersetzt die Rangfunktion durch die kostengünstigste mögliche Operation. Bei einer Tabelle mit Millionen von E-Mail-Datensätzen kann die Verwendung der parallelen Aggregation ohne Rangfunktion zu einer 5- bis 10-fachen Verbesserung der Arbeitszeit im Vergleich zum Pandas-Ansatz führen.

Hier ist die Vorschau der Codeausgabe:

Benutzer-ID total_emails Aktivitätsrang
32ded68d89443e808 19 1
ef5fe98c6b9f313075 19 2
5b8754928306a18b68 18 3
55e60cfcc9dc49c17e 16 4
91f59516cb9dee1e88 16 5
e6088004caf0c8cc51 6 25

# Verwendung von cumcount() + Pivot() vs. over(): Suche nach Benutzerkäufen

In diese Fragewerden wir gebeten, wiederkehrende aktive Benutzer zu identifizieren – insbesondere diejenigen, die innerhalb von 1 und 7 Tagen nach ihrem ersten einen zweiten Kauf getätigt haben. Einkäufe, die am selben Tag getätigt wurden, sollten nicht berücksichtigt werden. Das Ergebnis ist lediglich eine Liste der qualifizierten Personen user_id Werte.

// Datenansicht

Der amazon_transactions Tabelle hat eine Zeile professional Kauf, mit user_id, merchandise, created_at Datum und income.

Hier ist eine Vorschau der Tabelle:

Ausweis Benutzer-ID Artikel erstellt_at Einnahmen
1 109 Milch 03.03.2020 123
2 139 Keks 18.03.2020 421
3 120 Milch 18.03.2020 176
100 117 brot 10.03.2020 209

Getreide (was eine Ausgabezeile bedeutet): eine Benutzer-ID, die innerhalb von 7 Tagen nach ihrem ersten Kauf einen qualifizierten Rückkauf getätigt hat.

// Randgehäuse

Einkäufe am selben Tag sollten ignoriert werden, d. h. die Lücke zwischen dem ersten und zweiten Einkauf darf mehr als 0 Tage betragen und darf höchstens 7 Tage betragen. Ein Kunde, der am selben Tag zweimal kauft, ist nicht qualifiziert.

// Lösungen

Beide Lösungen ermitteln das früheste Kaufdatum jedes Benutzers und filtern dann nach Folgekäufen innerhalb des Zeitrahmens von 1 bis 7 Tagen. Eine Sache, die Sie beachten sollten: Wenn created_at Hat Zeitstempel anstelle von einfachen Datumsangaben, müssen Sie vor dem Vergleich auf das Datum kürzen. Andernfalls würden zwei Käufe, die zu unterschiedlichen Zeiten am selben Tag getätigt würden, fälschlicherweise die strenge Ungleichung erfüllen.

1. Pandas-Lösung

Bei Pandas besteht die Lösung darin, eindeutige Kaufdaten professional Benutzer zu isolieren und diese in eine Rangfolge zu bringen cumcount()Drehen, um das erste und zweite Datum nebeneinander anzuzeigen, und Berechnen der Tagesdifferenz.

import pandas as pd
amazon_transactions("purchase_date") = pd.to_datetime(amazon_transactions("created_at")).dt.date
day by day = amazon_transactions(("user_id", "purchase_date")).drop_duplicates()
ranked = day by day.sort_values(("user_id", "purchase_date"))
ranked("rn") = ranked.groupby("user_id").cumcount() + 1
first_two = (ranked(ranked("rn") <= 2)
             .pivot(index="user_id", columns="rn", values="purchase_date")
             .reset_index()
             .rename(columns={1: "first_date", 2: "second_date"}))
first_two = first_two.dropna(subset=("second_date"))
first_two("diff") = (pd.to_datetime(first_two("second_date")) - pd.to_datetime(first_two("first_date"))).dt.days
end result = first_two((first_two("diff") >= 1) & (first_two("diff") <= 7))(("user_id"))

2. Polare Lösung

Die Polars-Lösung beinhaltet die Berechnung des ersten Kaufdatums professional Benutzer als Fensterausdruck mit .over("user_id")Filterung nach Käufen, die in das Zeitfenster passen, und Rückgabe einer deduplizierten Datei user_id Liste.

import polars as pl
# returning energetic customers: 2nd buy 1–7 days after the primary (ignore same-day)
returning_users = (
    amazon_transactions
    .lazy()
    # first buy date per consumer (window so we keep away from .groupby on LazyFrame)
    .with_columns(
        pl.col("created_at").min().over("user_id").alias("first_purchase_date")
    )
    # maintain transactions strictly 1-7 days after that first buy
    .filter(
        (pl.col("created_at") > pl.col("first_purchase_date")) &
        (pl.col("created_at") <= pl.col("first_purchase_date") + pl.period(days=7))
    )
    # distinct consumer checklist
    .choose("user_id")
    .distinctive()
    .kind("user_id", descending=(False))
)

// Leistungsvergleich

Polare gegen Pandas

Beachten Sie die Anzahl unterschiedlicher DataFrame-Zuweisungen in der Pandas-Lösung: die deduplizierte Tagestabelle, die sortierte Rangtabelle, der Pivot-Body usw dropna Ergebnis und die gefilterte Ausgabe. Diese bestehen aus fünf separaten Objekten, von denen jedes Daten in einen neuen Speicherblock kopiert. Bei einer großen Transaktionstabelle kann allein der Pivot-Schritt die Speichernutzung erheblich erhöhen, da er den gesamten Datensatz in ein breites Format umformt.

Die Lazy-Chain von Polars reserviert bis dahin keinen Speicher .acquire(). Der .over("user_id") Der Fensterausdruck berechnet das früheste Kaufdatum jedes Benutzers in einem Durchgang .filter() gilt sofort im selben Schritt, und .distinctive() läuft gleichzeitig auf allen CPU-Kernen. Es gibt keinen Pivot, keine sortierte Zwischenkopie und keinen separaten Datumsumwandlungsschritt – Polars verarbeitet die Datumsarithmetik nativ innerhalb der Ausdrucks-Engine. Dieser Ansatz verbraucht weniger Speicher und wird schneller ausgeführt, selbst bei mittelgroßen Datensätzen.

Hier ist die Vorschau der Codeausgabe:

Benutzer-ID
100
103
105
143

# Verwendung von develop().imply() vs. cum_mean(): Gleitender monatlicher Umsatzdurchschnitt

In diese Fragewerden wir gebeten, einen kumulativen Durchschnitt für die monatlichen Buchverkäufe im Jahr 2022 zu ermitteln. Der Durchschnitt wächst jeden Monat unter Verwendung aller vorangegangenen Monate: Februar bildet den Durchschnitt für Januar und Februar, März den Durchschnitt für alle drei und so weiter. Die Ausgabe sollte den Monat, den Gesamtumsatz dieses Monats und den kumulierten Durchschnitt, gerundet auf die nächste ganze Zahl, umfassen.

// Datenansicht

Der amazon_books Die Tabelle hat eine Zeile professional Buch und ihren Einheitspreis. Der book_orders Die Tabelle enthält eine Zeile professional Bestellung, die eine Buch-ID mit einer Menge und einem Bestelldatum verknüpft. Hier ist eine Vorschau der Tabelle:

book_id Buchtitel Einheitspreis
B001 Die Tribute von Panem 25
B002 Die Außenseiter 50
B003 Eine Spottdrossel töten 100
B020 Die Säulen der Erde 60

Der book_orders Die Tabelle enthält eine Zeile professional Buchbestellung, die jede Bestell-ID mit einem Bestelldatum, einer Buch-ID und der bestellten Menge verknüpft:

order_id Bestelldatum book_id Menge
1001 10.01.2022 B001 1
1002 10.01.2022 B009 1
1003 15.01.2022 B012 2
1084 01.02.2023 B009 1

Getreide (was eine Ausgabezeile bedeutet): ein Monat im Jahr 2022, mit Gesamtverkäufen für diesen Monat und einem kumulierten Durchschnitt aller monatlichen Verkäufe bis einschließlich dieses Monats.

// Kompromisse

Mit Pandas, dem .increasing().imply() Die Klausel ist praktisch, arbeitet aber intern mit einer Schleife auf Python-Ebene über wachsende Fensterabschnitte. Bei einer monatlichen Zusammenfassung mit 12 Zeilen sind diese Kosten vernachlässigbar. Bei täglichen oder stündlichen Daten im großen Maßstab (z. B. stündliche Transaktionen über drei Jahre) erhöht jedes expandierende Fenstersegment den Overhead, der Zeile für Zeile ansteigt.

Polaren cum_mean() führt einen einzelnen Durchgang in Rust aus und ist im Maßstab von Natur aus schneller. Es gibt einen Haken: Die Frage muss auf die nächste ganze Zahl gerundet werden, und Pandas verwendet standardmäßig die Banker-Rundung (halbe auf gerade runden). Die Polars-Lösung verwendet NumPys Cumsum mit einem expliziten ground(x + 0.5) Formel zur Durchsetzung des Spherical-Half-Up-Verhaltens. Wenn Sie eine genaue Übereinstimmung mit der erwarteten Ausgabe benötigen, ist die NumPy-Methode zuverlässiger als die integrierte Rundung in beiden Bibliotheken.

// Lösungen

1. Pandas-Lösung

Wir führen Bücher mit Bestellungen zusammen, filtern nach 2022, aggregieren die monatlichen Verkäufe und bewerben uns .increasing().imply() um den kumulativen Durchschnitt zu berechnen.

import pandas as pd
import numpy as np
import datetime as dt
merged = pd.merge(book_orders, amazon_books, on="book_id", how="inside")
merged("order_date") = pd.to_datetime(merged("order_date"))
merged("order_month") = merged("order_date").dt.month
merged("yr") = merged("order_date").dt.yr
merged("gross sales") = merged("unit_price") * merged("amount")
merged = merged.loc((merged("yr") == 2022), :)
end result = (
    merged.groupby("order_month")("gross sales")
    .sum()
    .to_frame("monthly_sales")
    .sort_values(by="order_month")
    .reset_index()
)
end result("rolling_average") = end result("monthly_sales").increasing().imply().spherical(0)
end result

2. Polare: Aufbau der Lazy Pipeline und Sammeln

Wir verbinden die beiden Tabellen innerhalb einer Lazy Chain und berechnen den Umsatz als unit_price * amountnach 2022 filtern, nach Monat aggregieren und aufrufen .acquire() um vor dem NumPy-Rolling-Schritt in den Keen-Modus zu wechseln.

import polars as pl
import numpy as np
# Step 1: Put together month-to-month gross sales (LazyFrame)
monthly_sales_lazy = (
    book_orders.lazy()
    .be part of(amazon_books.lazy(), on="book_id", how="inside")
    .with_columns((
        (pl.col("unit_price") * pl.col("amount")).alias("gross sales"),
        pl.col("order_date").forged(pl.Datetime),
        pl.col("order_date").dt.yr().alias("yr"),
        pl.col("order_date").dt.month().alias("order_month")
    ))
    .filter(pl.col("yr") == 2022)
    .group_by("order_month")
    .agg(pl.col("gross sales").sum().alias("monthly_sales"))
    .kind("order_month")
)
# Step 2: Change to keen mode for rolling computation
monthly_sales = monthly_sales_lazy.acquire()

3. Berechnen des gleitenden Durchschnitts und Abschluss

Mit den monatlichen Verkäufen als NumPy-Array wenden wir eine Rundung zur Hälfte an, fügen das Ergebnis wieder zum Polars DataFrame hinzu und wählen die Ausgabespalten aus.

# Step 3: Rolling common with round-half-up
sales_np = monthly_sales("monthly_sales").to_numpy()
cumsum = np.cumsum(sales_np)
rolling_avg = np.ground(cumsum / np.arange(1, len(cumsum)+1) + 0.5).astype(int)
# Step 4: Add again to Polars DataFrame
monthly_sales = monthly_sales.with_columns((
    pl.Sequence("rolling_average", rolling_avg)
))
# Step 5: Closing end result with appropriate column names
end result = monthly_sales.choose(("order_month", "monthly_sales", "rolling_average"))

// Leistungsvergleich

Polare gegen Pandas

Bei dieser Frage wirken sich zwei Vorgänge am stärksten auf die Leistung aus: der Be a part of und das kumulative Fenster. Bei Pandas, pd.merge Verbindet alle Zeilen aus beiden Tabellen, bevor nach 2022 gefiltert wird. Dies bedeutet, dass die Bestellungen jedes Jahres verarbeitet werden, bevor Zeilen außerhalb des Zielzeitraums verworfen werden. Polars erstellt einen Lazy-Question-Plan und pusht den filter(yr == 2022) Bedingung, bevor der Be a part of ausgeführt wird, sodass von Anfang an ein kleinerer Datensatz verknüpft wird. Dieser Prädikat-Pushdown erfolgt automatisch, ohne dass zusätzliches Schreiben erforderlich ist.

Der auffälligste Unterschied ist die Lücke im gleitenden Durchschnitt. Pandas .increasing().imply() Vergrößert sein Fenster zeilenweise, ruft für jedes Section C auf und bleibt dabei von einer Python-Schleife gesteuert. Polaren cum_mean() berechnet die gesamte Spalte in einer einzigen Rust-Schleife ohne Python-Overhead. Während der Unterschied bei monatlichen Daten möglicherweise nicht wahrnehmbar ist, wenn Sie dieselbe Abfrage drei Jahre lang (ungefähr 1.000 Zeilen) für tägliche Daten ausführen, wird die Polars-Model in Mikrosekunden abgeschlossen, während Pandas aufgrund des erweiterten Fensters eine messbare Latenz aufweist.

Hier ist die Vorschau der Codeausgabe:

order_month Verkäufe rollender_Durchschnitt
1 145 145
2 250 198
3 315 237
12 710 402

# Abschluss

Bei allen drei Problemen folgen die Polars-Lösungen demselben Muster: Erstellen Sie einen Lazy-Question-Plan, übertragen Sie so viele Berechnungen wie möglich in den Optimierer und rufen Sie ihn auf .acquire() nur, wenn Sie ein konkretes Ergebnis benötigen.

Die Syntax erfordert einige Anpassungen, wenn Sie, wie die meisten Analysten, seit Jahren Pandas-Gewohnheiten haben, die Vorgänge jedoch eng übereinstimmen. .groupby() wird .group_by(), .rename() nimmt ein einfaches Diktat anstelle eines columns= Schlüsselwort, und das Rating wird zu einer Sortierung, gefolgt von .with_row_count().

Der wahre Unterschied zeigt sich im Maßstab. Bei kleinen Datensätzen liefern beide Bibliotheken Ergebnisse schnell genug, sodass der Unterschied nicht spürbar ist. Wenn die Anzahl der Zeilen Millionen erreicht, sind die Parallelitäts- und Single-Cross-Algorithmen auf Rust-Ebene von Polars deutlich überlegen. Wenn bei Pandas Leistungsprobleme auftreten, sind diese drei Herausforderungen ein guter Ausgangspunkt für die Migration.

Nate Rosidi ist Datenwissenschaftler und in der Produktstrategie tätig. Er ist außerdem außerordentlicher Professor für Analytik und Gründer von StrataScratch, einer Plattform, die Datenwissenschaftlern hilft, sich mit echten Interviewfragen von Prime-Unternehmen auf ihre Interviews vorzubereiten. Nate schreibt über die neuesten Tendencies auf dem Karrieremarkt, gibt Ratschläge zu Vorstellungsgesprächen, stellt Information-Science-Projekte vor und behandelt alles rund um SQL.



Von admin

Schreibe einen Kommentar

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