Zum Hauptinhalt springen

PySpark-Tutorial: RDDs, DataFrames und Spark SQL in Python

Lerne PySpark mit ausführbarem Code: Richte PySpark 4.3 ein, arbeite mit DataFrames (select, filter, group, join), stelle Abfragen mit Spark SQL und segmentiere Kunden mit pyspark.ml.
· 15 Min. lesen

Entdecke KI

PySpark ist die Python-API für Apache Spark. Du schreibst DataFrame-Code oder SQL in Python, und Spark verteilt die Arbeit auf alle CPU-Kerne deines Laptops oder auf alle Maschinen in einem Cluster. Ich setze es ein, sobald ein Datensatz nicht mehr bequem in pandas passt oder wenn derselbe Job später ohne Umschreiben in einem Cluster laufen soll.

In diesem PySpark-Tutorial installiere ich PySpark 4.3, erstelle eine SparkSession und erkläre die zwei Konzepte, über die Anfänger am häufigsten stolpern: RDDs versus DataFrames und Lazy Evaluation. Danach gehen wir die Alltagsoperationen (select, filter, groupBy, join und Spark SQL) an einem E‑Commerce-Datensatz mit 541.909 Zeilen durch und schließen mit einem K-Means-Kundensegmentierungsmodell über die pyspark.ml Pipeline-API ab.

Wenn du zu diesem Tutorial begleitete Übungen möchtest, deckt unser Kurs Introduction to PySpark dasselbe Terrain ab. Für einen längeren Lernplan sieh dir unseren Guide How to Learn PySpark From Scratch an.

TL;DR

  • PySpark ist die Python-API für Apache Spark und für Daten gedacht, die über den Speicher einer einzelnen Maschine hinauswachsen.
  • Installiere es mit pip install "pyspark[sql]"; PySpark 4.3 benötigt Python 3.11+ und Java 17+.
  • Jedes Programm startet mit einer SparkSession, und DataFrames (nicht RDDs) sind die Standard-API.
  • Transformationen sind lazy: Nichts läuft, bis eine Action wie count() oder show() ausgelöst wird.
  • Die DataFrame-API und Spark SQL kompilieren in denselben Plan – nutze, was sich besser liest.
  • pyspark.ml Pipelines verketten Feature-Aufbereitung und Modelle wie K-Means zu einem Objekt.

Was ist Apache Spark?

Apache Spark ist eine Open-Source-Engine für verteilte Datenverarbeitung. Sie teilt einen Datensatz in Partitionen, verarbeitet sie parallel im Cluster und hält Zwischenergebnisse im Speicher, statt sie zwischen den Schritten auf die Festplatte zu schreiben.

Dieses In‑Memory-Design ist der Hauptgrund, warum Spark Hadoop MapReduce für die meisten Batch-Workloads abgelöst hat (den Vergleich findest du in Hadoop vs Spark). Spark startete 2009 im AMPLab der UC Berkeley, wurde 2014 ein Top-Level-Apache-Projekt und erreichte Version 4.3 am 30. Oktober 2026.

Eine Engine deckt über eingebaute Bibliotheken mehrere Workloads ab:

  • Spark SQL und DataFrames für tabellarische Daten und SQL-Abfragen
  • Structured Streaming für kontinuierlich eintreffende Daten
  • MLlib (pyspark.ml) für verteiltes Machine Learning
  • pandas API on Spark um pandas‑ähnlichen Code auf einem Cluster auszuführen

Für einen genaueren Blick darauf, wie Driver, Cluster Manager und Executor die Arbeit aufteilen, lies unseren Leitfaden zur Apache-Spark-Architektur.

Was ist PySpark?

PySpark ist die Python-API für Apache Spark. Dein Python-Code beschreibt die gewünschten Transformationen, und die Engine von Spark, die auf der Java Virtual Machine (JVM) läuft, plant und führt sie im Cluster aus. Du bekommst die verteilte Ausführung von Spark, ohne Scala oder Java schreiben zu müssen.

Data Scientists nutzen PySpark zum Bereinigen und Aggregieren großer Datensätze, Data Engineers für ETL-Pipelines, und beide verwenden pyspark.ml, um Modelle auf Daten zu trainieren, die nicht auf eine einzelne Maschine passen. Wenn du heute vor allem mit pandas arbeitest, wird dir die DataFrame-API vertraut vorkommen.

Wann PySpark statt pandas verwenden

PySpark zahlt sich aus, wenn Daten größer als eine einzelne Maschine werden; darunter ist pandas in der Regel schneller, weil kein JVM-Start und keine Aufgabenplanung anfallen.

FunktionpandasPySpark
DatengrößeBegrenzt durch den RAM einer MaschineBegrenzt durch den kombinierten Speicher und die Festplatten im Cluster
AusführungSofort, meist einkernigLazy, parallel über Kerne oder Maschinen
Start-OverheadKeinerEinige Sekunden für JVM und SparkSession
Streaming-DatenNicht unterstütztStructured Streaming
Machine Learningscikit-learn-Ökosystempyspark.ml (verteilt)
FehlertoleranzKeine; ein Absturz verliert die ArbeitVerlorene Partitionen werden aus ihrer Abstammung neu berechnet

Meine Faustregel: Wenn die Daten locker in den Speicher passen und der Job auf einem Laptop läuft, bleib bei pandas. Wechsel zu PySpark, wenn du mit Speicherfehlern kämpfst oder wenn der Job später auf einem Cluster oder einer Plattform wie Databricks laufen soll.

So installierst du PySpark

PySpark wird mit einem pip-Befehl von PyPI installiert und bringt die Spark-Laufzeit mit, sodass du für lokale Arbeit keine separate Spark- oder Hadoop-Installation brauchst. Prüfe vorher die zwei Voraussetzungen.

Voraussetzungen

Laut offiziellem Installationsleitfaden benötigt PySpark 4.3:

  • Python 3.11 oder neuer. PySpark 4.3 hat Python 3.10 gestrichen; sieh dir unseren Python-Setup-Guide an, wenn du eine neuere Version brauchst.
  • Java 17 oder neuer (Spark 4.3 läuft auf Java 17, 21 und 25) mit der Umgebungsvariable JAVA_HOME, die darauf zeigt. Unser Guide zur Java-Installation führt durch jedes Betriebssystem.

Beides kannst du im Terminal prüfen:

python3 --version  # Python 3.11 oder neuer
java -version      # openjdk version "17" oder neuer

Wenn du in DataLab oder Databricks arbeitest, ist PySpark bereits installiert – spring direkt zum Datensatz.

PySpark mit pip installieren

Ich empfehle eine virtuelle Umgebung, damit PySpark-Abhängigkeiten nicht mit anderen Projekten kollidieren:

python3 -m venv pyspark-env
source pyspark-env/bin/activate
pip install "pyspark[sql]" "pandas<3"
python3 -c "import pyspark; print(pyspark.__version__)"
4.3.0

Unter Windows verwende python statt python3 und aktiviere die Umgebung mit pyspark-env\Scripts\activate. Das Extra [sql] bringt pandas, PyArrow und NumPy mit, die PySpark für toPandas() und pandas-UDFs nutzt. Ich pinne pandas<3, weil PySpark 4.3 aktuell warnt, dass pandas 3.0 noch nicht vollständig unterstützt wird.

Zwei Fehler verursachen die meisten gescheiterten ersten Läufe:

  • JAVA_GATEWAY_EXITED oder "Unable to locate a Java Runtime": Java fehlt oder JAVA_HOME ist nicht gesetzt.
  • PYTHON_VERSION_MISMATCH: Die Worker-Prozesse von Spark haben ein anderes Python als dein Skript gefunden. Setze PYSPARK_PYTHON auf den Interpreter deiner Umgebung, z. B. export PYSPARK_PYTHON=$(which python).

Für betriebssystemspezifische Schritte, inklusive Windows, lies unseren PySpark-Installationsguide.

Den Datensatz beschaffen

Die Beispiele nutzen einen E‑Commerce-Datensatz mit 541.909 Transaktionen eines britischen Online-Händlers, von Dezember 2010 bis Dezember 2011. Jede Zeile ist eine Produktposition auf einer Rechnung mit 8 Spalten:

  • InvoiceNo: Rechnungsnummer (Nummern mit "C" vorne sind Stornos)
  • StockCode und Description: Produktcode und Name
  • Quantity und UnitPrice: Gekaufte Einheiten und Preis pro Einheit in Pfund Sterling
  • InvoiceDate: Datum und Uhrzeit des Kaufs
  • CustomerID: Kundenkennung (in einigen Zeilen fehlend)
  • Country: Land des Kunden

Du kannst ihn als datacamp_ecommerce.csv von der DataLab-Dataset-Seite herunterladen. Es ist das Online Retail Dataset aus dem UCI Machine Learning Repository, daher kannst du dieselbe Datei auch aus der öffentlichen Quelle bauen (benötigt pip install openpyxl):

import io
import urllib.request
import zipfile

import pandas as pd

url = "https://archive.ics.uci.edu/static/public/352/online+retail.zip"
with urllib.request.urlopen(url) as response:
    archive = zipfile.ZipFile(io.BytesIO(response.read()))

retail = pd.read_excel(
    archive.open("Online Retail.xlsx"),
    dtype={"InvoiceNo": str, "StockCode": str, "CustomerID": "Int64"},
)
# Datumsformat der DataLab-Datei anpassen, z. B. 12/1/10 8:26
retail["InvoiceDate"] = retail["InvoiceDate"].map(
    lambda ts: f"{ts.month}/{ts.day}/{ts:%y} {ts.hour}:{ts:%M}"
)
retail.to_csv("datacamp_ecommerce.csv", index=False)

Das Einlesen der 23‑MB-Excel-Datei dauert etwa eine Minute. Speichere die CSV in dem Ordner, aus dem du den restlichen Code ausführst.

Eine SparkSession erstellen

Eine SparkSession ist der Einstiegspunkt in jedes PySpark-Programm: Sie verbindet deinen Python-Code mit der Spark-Engine und erstellt DataFrames. Du erstellst pro Anwendung eine Session mit dem Builder-Pattern:

from pyspark.sql import SparkSession

spark = SparkSession.builder.appName("PySparkTutorial").getOrCreate()
print(spark.version)
4.3.0

getOrCreate() gibt die laufende Session zurück, wenn bereits eine existiert – in Notebooks kannst du es also gefahrlos zweimal aufrufen. Aus einem normalen Python-Prozess gestartet, läuft Spark im lokalen Modus (local[*]), nutzt alle CPU-Kerne deiner Maschine als Worker und stellt das Spark-UI unter http://localhost:4040 bereit, um Jobs zu beobachten. Mit spark.stop() gibst du die Ressourcen wieder frei.

Den E‑Commerce-Datensatz laden

Mit laufender Session können wir die CSV in ein DataFrame einlesen. Ich übergebe ein explizites Schema statt inferSchema=True, das die Datei nur zum Raten der Spaltentypen ein zweites Mal scannen würde:

import pyspark.sql.functions as F

schema = """
    InvoiceNo STRING, StockCode STRING, Description STRING, Quantity INT,
    InvoiceDate STRING, UnitPrice DOUBLE, CustomerID INT, Country STRING
"""

orders = spark.read.csv(
    "datacamp_ecommerce.csv", header=True, schema=schema, escape='"'
)
orders = orders.withColumn("InvoiceDate", F.to_timestamp("InvoiceDate", "M/d/yy H:mm"))

orders.printSchema()
root
 |-- InvoiceNo: string (nullable = true)
 |-- StockCode: string (nullable = true)
 |-- Description: string (nullable = true)
 |-- Quantity: integer (nullable = true)
 |-- InvoiceDate: timestamp (nullable = true)
 |-- UnitPrice: double (nullable = true)
 |-- CustomerID: integer (nullable = true)
 |-- Country: string (nullable = true)

Zwei Details in diesem Snippet sind wichtig:

  • escape='"' handhabt Produktnamen mit doppelten Anführungszeichen wie RECORD FRAME 7"" SINGLE SIZE. Der Standard-Escape-Charakter von Spark ist ein Backslash.
  • Das Datumsformat M/d/yy H:mm passt zu Werten wie 12/1/10 8:26. Seit Spark 4.0 ist der ANSI-SQL-Modus standardmäßig aktiv, daher wirft to_timestamp() bei einem Wert, der nicht zum Muster passt, einen CANNOT_PARSE_TIMESTAMP-Fehler. Verwende F.try_to_timestamp(), wenn du für fehlerhafte Zeilen lieber NULL erhalten möchtest.

So siehst du dir die ersten Zeilen an und zählst sie:

orders.show(5, truncate=False)
print(orders.count())
+---------+---------+-----------------------------------+--------+-------------------+---------+----------+--------------+
|InvoiceNo|StockCode|Description                        |Quantity|InvoiceDate        |UnitPrice|CustomerID|Country       |
+---------+---------+-----------------------------------+--------+-------------------+---------+----------+--------------+
|536365   |85123A   |WHITE HANGING HEART T-LIGHT HOLDER |6       |2010-12-01 08:26:00|2.55     |17850     |United Kingdom|
|536365   |71053    |WHITE METAL LANTERN                |6       |2010-12-01 08:26:00|3.39     |17850     |United Kingdom|
|536365   |84406B   |CREAM CUPID HEARTS COAT HANGER     |8       |2010-12-01 08:26:00|2.75     |17850     |United Kingdom|
|536365   |84029G   |KNITTED UNION FLAG HOT WATER BOTTLE|6       |2010-12-01 08:26:00|3.39     |17850     |United Kingdom|
|536365   |84029E   |RED WOOLLY HOTTIE WHITE HEART.     |6       |2010-12-01 08:26:00|3.39     |17850     |United Kingdom|
+---------+---------+-----------------------------------+--------+-------------------+---------+----------+--------------+
only showing top 5 rows

541909

Weitere Leseoptionen, etwa mehrere Dateien, andere Trennzeichen und den Umgang mit defekten Zeilen, findest du in unserem PySpark-CSV-Lese-Tutorial.

RDDs vs. DataFrames

RDDs und DataFrames sind PySparks zwei Repräsentationen verteilter Daten – und für fast allen neuen Code sind DataFrames die richtige Wahl.

AbstraktionWas es istWann verwenden
RDD (Resilient Distributed Dataset)Niedrigstufige, unveränderliche Sammlung beliebiger Python-Objekte, in Partitionen aufgeteiltUnstrukturierte Daten oder wenn du feinkörnige Kontrolle über die Partitionierung brauchst
DataFrameVerteilte Tabelle mit benannten, typisierten Spalten, ähnlich einem pandas DataFrameAnalyse, SQL-Abfragen, ETL und Machine Learning

Hier dieselbe Aggregation – Gesamtmenge pro Land – in beiden Varianten:

sales = [("United Kingdom", 6), ("France", 24), ("United Kingdom", 8)]

# RDD: Du schreibst die Logik mit Python-Funktionen
rdd = spark.sparkContext.parallelize(sales)
print(rdd.reduceByKey(lambda a, b: a + b).collect())

# DataFrame: Du deklarierst das Ergebnis, und Spark plant die Berechnung
sales_df = spark.createDataFrame(sales, ["country", "quantity"])
sales_df.groupBy("country").sum("quantity").show()
[('France', 24), ('United Kingdom', 14)]
+--------------+-------------+
|       country|sum(quantity)|
+--------------+-------------+
|United Kingdom|           14|
|        France|           24|
+--------------+-------------+

Die RDD-Version führt deine Lambda in Python-Worker-Prozessen aus. Die DataFrame-Version läuft durch den Catalyst-Optimizer von Spark, der die Abfrage in einen effizienten Plan umschreibt, der innerhalb der JVM läuft – DataFrame-Code ist daher meist ohne Tuning schneller. Der Rest dieses Tutorials nutzt DataFrames.

Lazy Evaluation: Transformationen vs. Actions

Lazy Evaluation bedeutet, dass Spark eine Transformation beim Aufruf nicht ausführt, sondern den Schritt im Plan vormerkt und den gesamten Plan erst ausführt, wenn eine Action ein Ergebnis verlangt.

 TransformationenActions
Was sie tunBeschreiben ein neues DataFrameLiefern ein Ergebnis zurück oder schreiben Daten
Wann sie laufenNiemals alleinSofort, und lösen alle ausstehenden Transformationen aus
Beispieleselect(), filter(), withColumn(), groupBy(), join()show(), count(), collect(), toPandas(), write

Der folgende Filter kehrt sofort zurück, obwohl der Datensatz über eine halbe Million Zeilen hat, weil noch nichts gelesen wurde. Erst count() startet einen Job:

uk_orders = orders.filter(F.col("Country") == "United Kingdom")  # Transformation: noch kein Job
print(uk_orders.count())                                          # Action: Spark liest jetzt die Datei
495478

Durch das Warten bis zur Action kann Spark die gesamte Kette auf einmal optimieren. explain() druckt den gewählten physischen Plan (hier gekürzt):

uk_orders.explain()
== Physical Plan ==
*(1) Project [InvoiceNo#0, StockCode#1, ...]
+- *(1) Filter (isnotnull(Country#7) AND (Country#7 = United Kingdom))
   +- FileScan csv [...] PushedFilters: [IsNotNull(Country), EqualTo(Country,United Kingdom)], ...

Der Eintrag PushedFilters zeigt, dass Spark den Länderfilter in den Dateiscan hineinschiebt, sodass Zeilen anderer Länder bereits beim Lesen der CSV verworfen werden. Eine praktische Folge: Ein Datenfehler innerhalb einer Transformation, etwa ein Datum, das im ANSI-Modus nicht geparst werden kann, zeigt sich erst bei der nächsten Action – ein paar Zeilen später.

Spark führt nichts aus, bis eine Action ein Ergebnis anfordert

Häufige DataFrame-Operationen

Der Großteil von PySpark-Code ist eine Kette aus fünf DataFrame-Operationen: select(), filter(), withColumn(), groupBy() und join(). Jede gibt ein neues DataFrame zurück, daher kannst du sie beliebig verketten.

Spalten mit select() auswählen

select() gibt ein DataFrame nur mit den benannten Spalten zurück:

orders.select("InvoiceNo", "Description", "Quantity", "UnitPrice").show(3, truncate=False)
+---------+----------------------------------+--------+---------+
|InvoiceNo|Description                       |Quantity|UnitPrice|
+---------+----------------------------------+--------+---------+
|536365   |WHITE HANGING HEART T-LIGHT HOLDER|6       |2.55     |
|536365   |WHITE METAL LANTERN               |6       |3.39     |
|536365   |CREAM CUPID HEARTS COAT HANGER    |8       |2.75     |
+---------+----------------------------------+--------+---------+
only showing top 3 rows

Zeilen mit filter() filtern

filter() behält Zeilen, die eine Bedingung erfüllen. Dieser Datensatz mischt Verkäufe mit Stornos (Rechnungsnummern beginnend mit "C"), negativen Mengen und Zeilen ohne Kunde – ich behalte nur gültige Verkäufe:

sales_only = orders.filter(
    (F.col("Quantity") > 0)
    & (F.col("UnitPrice") > 0)
    & F.col("CustomerID").isNotNull()
    & ~F.col("InvoiceNo").startswith("C")
)
print(sales_only.count())
397884

Kombiniere Bedingungen mit & (und), | (oder) und ~ (nicht) und klammere jede ein, da Python-Operatorpräzedenz sonst & vor > auswertet. Unser PySpark-Filter-Tutorial zeigt weitere Muster.

Spalten mit withColumn() hinzufügen

withColumn() fügt eine Spalte hinzu oder ersetzt eine gleichnamige. Hier berechnet es den Zeilenwert:

sales_only = sales_only.withColumn("TotalAmount", F.round(F.col("Quantity") * F.col("UnitPrice"), 2))
sales_only.select("InvoiceNo", "Quantity", "UnitPrice", "TotalAmount").show(3)
+---------+--------+---------+-----------+
|InvoiceNo|Quantity|UnitPrice|TotalAmount|
+---------+--------+---------+-----------+
|   536365|       6|     2.55|       15.3|
|   536365|       6|     3.39|      20.34|
|   536365|       8|     2.75|       22.0|
+---------+--------+---------+-----------+
only showing top 3 rows

Sieh dir für bedingte Spalten mit F.when() unser PySpark-withColumn()-Tutorial an.

Aggregieren mit groupBy() und agg()

groupBy() gruppiert Zeilen nach einer oder mehreren Spalten, und agg() berechnet eine oder mehrere Kennzahlen pro Gruppe. Welche Länder bringen den meisten Umsatz?

country_revenue = (
    sales_only.groupBy("Country")
    .agg(
        F.count_distinct("CustomerID").alias("customers"),
        F.round(F.sum("TotalAmount"), 2).alias("revenue"),
    )
    .orderBy(F.desc("revenue"))
)
country_revenue.show(5)
+--------------+---------+----------+
|       Country|customers|   revenue|
+--------------+---------+----------+
|United Kingdom|     3920|7308391.55|
|   Netherlands|        9| 285446.34|
|          EIRE|        3|  265545.9|
|       Germany|       94| 228867.14|
|        France|       87| 209024.05|
+--------------+---------+----------+
only showing top 5 rows

Das Vereinigte Königreich dominiert – erwartbar bei einem britischen Händler. Interessanter sind die Niederlande und Irland (als EIRE gelistet): 9 bzw. 3 Kunden geben mehr aus als Deutschlands 94 – das deutet auf Großabnehmer hin.

Beachte, dass ich das Functions-Modul als F importiere statt from pyspark.sql.functions import *, was Pythons eingebaute sum(), min() und max() überschattet. Seit PySpark 4.0 zeigt der Wildcard-Import zudem die alten CamelCase-Namen wie countDistinct nicht mehr an.

Unser PySpark-groupBy-Tutorial geht tiefer auf Aggregationen ein.

DataFrames mit join() verknüpfen

join() kombiniert zwei DataFrames über einen gemeinsamen Schlüssel. Um jedes Land mit einer Region zu labeln, verknüpfe ich eine kleine Lookup-Tabelle, die Australien absichtlich auslässt:

regions = spark.createDataFrame(
    [("United Kingdom", "UK"), ("Netherlands", "EU"), ("EIRE", "EU"),
     ("Germany", "EU"), ("France", "EU")],
    ["Country", "region"],
)
country_revenue.join(regions, on="Country", how="left").orderBy(F.desc("revenue")).show(6)
+--------------+---------+----------+------+
|       Country|customers|   revenue|region|
+--------------+---------+----------+------+
|United Kingdom|     3920|7308391.55|    UK|
|   Netherlands|        9| 285446.34|    EU|
|          EIRE|        3|  265545.9|    EU|
|       Germany|       94| 228867.14|    EU|
|        France|       87| 209024.05|    EU|
|     Australia|        9| 138521.31|  NULL|
+--------------+---------+----------+------+
only showing top 6 rows

Ein Left Join behält jede Zeile aus dem linken DataFrame und füllt fehlende Treffer mit NULL – deshalb bleibt Australien im Ergebnis. Ein Inner Join (how="inner", Standard) würde es entfernen. Unser PySpark-Joins-Tutorial vergleicht alle Join-Typen und ihre Performance.

Spark SQL in PySpark

Mit Spark SQL kannst du ein DataFrame per Standard-SQL abfragen, indem du es als temporäre View registrierst. Diese Abfrage reproduziert die Ländertabelle aus dem groupBy-Abschnitt:

sales_only.createOrReplaceTempView("sales")

spark.sql("""
    SELECT Country,
           COUNT(DISTINCT CustomerID) AS customers,
           ROUND(SUM(TotalAmount), 2) AS revenue
    FROM sales
    GROUP BY Country
    ORDER BY revenue DESC
    LIMIT 5
""").show()
+--------------+---------+----------+
|       Country|customers|   revenue|
+--------------+---------+----------+
|United Kingdom|     3920|7308391.55|
|   Netherlands|        9| 285446.34|
|          EIRE|        3|  265545.9|
|       Germany|       94| 228867.14|
|        France|       87| 209024.05|
+--------------+---------+----------+

Beide Varianten kompilieren in denselben optimierten Plan – keine ist schneller. Ich nutze SQL für lange Aggregationen, die Analysten lesen, und die DataFrame-API, wenn ich Python-Logik um die Abfrage brauche, etwa Filter in einer Schleife. spark.sql() gibt ein DataFrame zurück, sodass du beide frei mischen kannst.

Die temporäre View lebt bis zum Stopp der SparkSession.

Machine Learning mit PySpark: Ein K‑Means-Beispiel

PySparks Modul pyspark.ml trainiert Modelle auf DataFrames, und eine Pipeline verknüpft die Feature-Aufbereitung und das Modell zu einem Objekt, das du fitten und wiederverwenden kannst. Als Beispiel segmentiere ich die Kundschaft des Händlers mit K‑Means‑Clustering anhand von Recency, Frequency und Monetary Value (RFM) – ein Standard im Marketing:

  • Recency: Tage seit dem letzten Kauf
  • Frequency: Anzahl unterschiedlicher Rechnungen
  • Monetary: Gesamtumsatz

RFM-Features mit groupBy() bauen

Ein groupBy() über das bereinigte sales_only-DataFrame erzeugt alle drei Features. Recency messen wir ab dem Tag nach der letzten Rechnung im Datensatz:

snapshot = sales_only.agg(F.date_add(F.max("InvoiceDate"), 1)).first()[0]

rfm = sales_only.groupBy("CustomerID").agg(
    F.datediff(F.lit(snapshot), F.max("InvoiceDate")).alias("recency"),
    F.count_distinct("InvoiceNo").alias("frequency"),
    F.round(F.sum("TotalAmount"), 2).alias("monetary"),
)
rfm.describe("recency", "frequency", "monetary").show()
+-------+------------------+-----------------+------------------+
|summary|           recency|        frequency|          monetary|
+-------+------------------+-----------------+------------------+
|  count|              4338|             4338|              4338|
|   mean| 93.05947441217151|4.272014753342554|2054.2664591977896|
| stddev|100.01226424171826|7.697997826095038| 8989.230441421178|
|    min|                 1|                1|              3.75|
|    max|               374|              209|         280206.02|
+-------+------------------+-----------------+------------------+

Sieh auf die Spalte Monetary: ein Mittelwert von rund £2.054, eine Standardabweichung über dem Vierfachen davon und ein Maximum über £280.000. K‑Means misst euklidische Distanzen, daher würden wenige Großkunden die Cluster dominieren. Ein Log auf jedem Feature komprimiert diesen langen Schwanz.

Skalieren und Clustern in einer Pipeline

Die Pipeline hat drei Stufen: VectorAssembler packt die Features in die einzelne Vektorspalte, die pyspark.ml-Modelle erwarten, StandardScaler bringt sie auf dieselbe Skala, und KMeans clustert sie. Ich fitte k = 2 bis 6 und bewerte jedes Modell mit dem Silhouetten-Score, der von -1 bis 1 reicht – je höher, desto klarer getrennte Cluster:

from pyspark.ml import Pipeline
from pyspark.ml.clustering import KMeans
from pyspark.ml.evaluation import ClusteringEvaluator
from pyspark.ml.feature import StandardScaler, VectorAssembler

rfm_log = rfm.select(
    "CustomerID",
    "recency", "frequency", "monetary",
    F.log1p("recency").alias("log_recency"),
    F.log1p("frequency").alias("log_frequency"),
    F.log1p("monetary").alias("log_monetary"),
).cache()

assembler = VectorAssembler(
    inputCols=["log_recency", "log_frequency", "log_monetary"], outputCol="features_raw"
)
scaler = StandardScaler(
    inputCol="features_raw", outputCol="features", withMean=True, withStd=True
)
evaluator = ClusteringEvaluator(featuresCol="features", metricName="silhouette")

for k in range(2, 7):
    kmeans = KMeans(featuresCol="features", k=k, seed=42)
    model = Pipeline(stages=[assembler, scaler, kmeans]).fit(rfm_log)
    score = evaluator.evaluate(model.transform(rfm_log))
    print(f"k={k}  silhouette={score:.3f}")
k=2  silhouette=0.624
k=3  silhouette=0.502
k=4  silhouette=0.502
k=5  silhouette=0.478
k=6  silhouette=0.470

.cache() hält rfm_log im Speicher, da jeder Fit und jede Bewertung in der Schleife es erneut liest; ohne Caching würde Spark die RFM-Tabelle bei jedem Fit erneut aus der CSV berechnen.

k wählen und Segmente lesen

Der Silhouetten-Score ist bei k = 2 am höchsten, aber zwei Segmente trennen meist nur aktive von inaktiven Kundinnen und Kunden – damit kann ein Marketingteam wenig anfangen. k = 4 erzielt denselben Wert wie k = 3 und splittet die aktiven in Großabnehmer und Newcomer, daher nutze ich es und profile jedes Cluster:

kmeans = KMeans(featuresCol="features", k=4, seed=42)
model = Pipeline(stages=[assembler, scaler, kmeans]).fit(rfm_log)
segments = model.transform(rfm_log)

(segments.groupBy("prediction")
    .agg(
        F.count("*").alias("customers"),
        F.round(F.avg("recency"), 1).alias("avg_recency"),
        F.round(F.avg("frequency"), 1).alias("avg_frequency"),
        F.round(F.avg("monetary"), 1).alias("avg_monetary"),
    )
    .orderBy("prediction")
    .show())
+----------+---------+-----------+-------------+------------+
|prediction|customers|avg_recency|avg_frequency|avg_monetary|
+----------+---------+-----------+-------------+------------+
|         0|     1598|      181.2|          1.3|       326.2|
|         1|      720|       13.2|         13.7|      8072.7|
|         2|     1162|       77.1|          3.9|      1765.2|
|         3|      858|       17.4|          2.3|       613.7|
+----------+---------+-----------+-------------+------------+

Die vier Segmente lassen sich klar lesen:

  • Cluster 1, beste Kundschaft (720): Im Schnitt innerhalb der letzten zwei Wochen gekauft, rund 14 Bestellungen und über £8.000 ausgegeben. Hier greifen Retention- und Loyalty-Angebote.
  • Cluster 3, frisch und leicht (858): Aktiv in den letzten drei Wochen, aber nur etwa zwei Bestellungen. Viele sind vermutlich Neukunden, die sich Richtung zweite und dritte Bestellung entwickeln lassen.
  • Cluster 2, am Abkühlen (1.162): Mittleres Ausgabenniveau, aber rund elf Wochen seit dem letzten Kauf. Eine Win‑Back‑Kampagne hat hier die besten Chancen.
  • Cluster 0, inaktive Einmalkäufer (1.598): Meist eine einzelne Bestellung, etwa £326, vor gut sechs Monaten. Die größte Gruppe und die mit dem geringsten Wert.

720 beste Kundinnen und Kunden stechen heraus; 1.598 sind inaktive Einmalkäufer

Cluster-Nummern sind willkürliche Labels, und ein anderer Seed kann sie umsortieren – profile die Cluster also immer, bevor du sie benennst. Wie K‑Means seine Zentren findet, erklärt unsere Einführung in k‑means clustering, und die pyspark.ml API-Referenz listet alle Modelle und Transformer. Unser Kurs Machine Learning with PySpark behandelt Klassifikation, Regression und Modell-Tuning mit Pipelines.

Fazit

PySpark lässt sich auf wenige Ideen herunterbrechen: eine SparkSession zum Start, DataFrames für fast alles, lazy Transformationen, die erst bei einer Action laufen, und die Wahl, jede Abfrage in Python oder SQL zu schreiben. Alles in diesem Tutorial lief im lokalen Modus auf einem Laptop – derselbe Code läuft unverändert im Cluster.

Eine Datei mit 541.909 Zeilen braucht keinen Cluster, und pandas hätte das geschafft. Das war Absicht, denn die Gewohnheiten, die bei großen Daten zählen (explizite Schemas, früh filtern, Wiederverwendetes cachen und explain() prüfen), baut man leichter auf Daten auf, die man inspizieren kann.

Von hier aus sind das je nach Ziel gute nächste Schritte:

Wenn du weiter einsteigen willst, empfehle ich unseren Lernpfad Big Data with PySpark. Er behandelt Datenmanagement, Feature Engineering, Machine-Learning-Pipelines und Empfehlungssysteme Schritt für Schritt. Für Teams bietet DataCamp for Business Lernpfade für Data-Engineering-Rollen.

PySpark FAQs

Welche Voraussetzungen brauche ich, um PySpark zu lernen?

PySpark 4.3 benötigt Python 3.11 oder neuer und Java 17 oder neuer (Spark 4.3 läuft auf Java 17, 21 und 25), mit JAVA_HOME, das auf deine Java-Installation zeigt. Du solltest dich mit grundlegender Python-Programmierung wohlfühlen; Erfahrung mit pandas DataFrames hilft, da die PySpark-DataFrame-API ähnliche Konzepte nutzt. Vorkenntnisse in verteilten Systemen brauchst du nicht.

Ist PySpark schneller als pandas?

Nicht bei kleinen Daten. pandas läuft im selben Prozess ohne Startkosten, während PySpark ein paar Sekunden für JVM-Start und Task-Scheduling braucht – daher gewinnt pandas meist bei Daten, die bequem in den Speicher passen. PySpark ist schneller, sobald die Arbeit den Speicher oder die CPU einer einzelnen Maschine übersteigt, weil Partitionen parallel über Kerne oder Clusterknoten verarbeitet werden.

Was ist der Unterschied zwischen RDDs und DataFrames in PySpark?

Ein RDD (Resilient Distributed Dataset) ist PySparks niedrigstufige Struktur: eine verteilte Sammlung beliebiger Python-Objekte, die du mit eigenen Funktionen verarbeitest. Ein DataFrame organisiert Daten in benannte, typisierte Spalten wie ein pandas DataFrame, und Sparks Catalyst-Optimizer übersetzt DataFrame-Abfragen in effiziente Ausführungspläne. Nutze DataFrames für nahezu allen neuen Code und RDDs nur für unstrukturierte Daten oder feinkörnige Kontrolle über die Partitionierung.

Wie starte ich PySpark in einem Jupyter Notebook?

Installiere Java 17 oder neuer und führe dann pip install "pyspark[sql]" in der Umgebung aus, die dein Notebook nutzt. Erstelle die SparkSession in der ersten Zelle:

from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("MyNotebook").getOrCreate()

Wenn du die lokale Einrichtung überspringen willst: DataLab und Databricks-Notebooks bringen PySpark vorinstalliert mit.

Wann sollte ich PySpark statt pandas verwenden?

Nutze PySpark, wenn deine Daten nicht in den Speicher einer Maschine passen, wenn ein Job auf einem Cluster oder einer Plattform wie Databricks laufen soll oder wenn du Pipelines baust, die über einen Laptop hinauswachsen. Für explorative Analysen auf Daten, die in den Speicher passen, ist pandas einfacher und startet sofort. Lokal geschriebener PySpark-Code läuft unverändert im Cluster – es kann also sinnvoll sein, direkt damit zu starten, wenn Skalierung absehbar ist.

Was ist Lazy Evaluation in PySpark?

Lazy Evaluation bedeutet, dass PySpark Transformationen wie filter() oder withColumn() nicht beim Aufruf ausführt. Es protokolliert sie im Plan und führt diesen erst aus, wenn eine Action wie count(), show() oder write ein Ergebnis benötigt. Das Warten erlaubt Spark, die gesamte Kette zu optimieren – etwa, indem Filter in den Dateiscan geschoben werden.

Wie führe ich SQL-Abfragen in PySpark aus?

Registriere ein DataFrame als temporäre View mit df.createOrReplaceTempView("sales") und frage es mit spark.sql("SELECT ... FROM sales") ab. Das Ergebnis ist ein reguläres DataFrame, sodass du SQL und DataFrame-Code frei mischen kannst. Beide kompilieren in denselben optimierten Plan – keiner ist schneller.

Brauche ich Hadoop, um PySpark auszuführen?

Nein. pip install pyspark bringt die Spark-Laufzeit mit, die im lokalen Modus ohne Hadoop läuft. Hadoop-Komponenten brauchst du nur, wenn du aus HDFS liest oder Spark auf einem YARN-Cluster betreibst.

Natassha Selvaraj

Autor

Natassha Selvaraj

Natassha ist eine Datenberaterin, die an der Schnittstelle von Datenwissenschaft und Marketing arbeitet. Sie ist davon überzeugt, dass Daten, wenn sie klug genutzt werden, Einzelpersonen und Organisationen zu enormem Wachstum inspirieren können. Als Autodidaktin liebt Natassha es, Artikel zu schreiben, die anderen Data Science-Anwärtern den Einstieg in die Branche erleichtern. Ihre Artikel auf ihrem persönlichen Blog und in externen Publikationen werden durchschnittlich 200.000 Mal pro Monat aufgerufen.

Themen

Ähnlich

Tutorial

Python Datenstrukturen Tutorial

Tutorial

Loop-Schleifen in Python-Tutorial

Tutorial

Python-Arrays