Vai al contenuto principale

Padroneggiare groupBy di PySpark per aggregazioni di dati scalabili

Esplora il metodo groupBy di PySpark, che consente ai professionisti dei dati di eseguire funzioni di aggregazione sui propri dati. È un modo potente per partizionare e riassumere rapidamente i tuoi grandi dataset, sfruttando le potenti tecniche di Spark.
Aggiornato 29 set 2026  · 15 min leggi

Scopri con l'IA

ChatGPTClaudePerplexity

Nel mondo dei big data, raramente osserviamo i dati grezzi. Per rendere i dati più fruibili, raggrupparli e aggregarli è un’operazione comune e potente. Quando si lavora con sistemi distribuiti come Apache Spark e Python, disporre della funzione groupBy di PySpark ci permette di raccogliere e riassumere i dati distribuiti. Rispecchia la funzionalità della clausola GROUP BY di SQL, ma è progettata per gestire in modo efficiente l’elaborazione distribuita su dataset enormi.

Il groupBy di PySpark consente di partizionare i dati in base a diverse colonne, che possono poi essere aggregate in varie misure come somme, medie e così via. Per farlo in modo efficiente, PySpark segue il paradigma split-apply-combine:

  1. Dividere i dati in gruppi in base ai criteri,
  2. Applicare a ogni gruppo una logica di aggregazione o trasformazione,
  3. Combinare i risultati in un nuovo DataFrame.

Grafico che mostra come funziona il metodo groupBy di PySpark illustrando lo split per chiave, l'applicazione di sum a ciascuna chiave e la combinazione dei dati aggregati

Esempio del metodo groupBy() di PySpark che applica Sum

PySpark sfrutta la valutazione pigra (lazy evaluation), il che significa che operazioni come groupBy vengono calcolate solo quando vengono invocate azioni (come show() o collect()). Invece, PySpark costruisce prima un DAG (Directed Acyclic Graph) iniziale che viene affinato per le prestazioni. Questo aiuta Spark a ottimizzare il piano di query prima dell’esecuzione. 

Se non hai ancora avuto modo di lavorare con PySpark, ti consiglio vivamente di dare un’occhiata a Big Data Fundamentals with PySpark.

Creare un DataFrame PySpark

Per prima cosa, ci serve un DataFrame PySpark. Se ti serve un ripasso dei vari comandi PySpark, consulta questo ottimo cheat sheet sui DataFrame PySpark. 

Vedremo alcuni dei passaggi e dei comandi chiave che ci aiuteranno a iniziare con PySpark. Prima di cominciare, assicurati di avere PySpark installato nel tuo ambiente Python, oltre a Java e al Java JDK. Se ti serve assistenza, segui questa guida per iniziare con PySpark.

Avviare SparkSession

Il primo passo è far partire PySpark!

from pyspark.sql import SparkSession

# Start a SparkSession
spark = SparkSession.builder \
    .appName("GroupByExample") \
    .getOrCreate()

DataFrame di esempio per GroupBy

Costruiamo un DataFrame di esempio su cui lavorare.

from pyspark.sql import Row

data = [
    Row(department="Sales", employee="Alice", salary=5000),
    Row(department="Sales", employee="Bob", salary=4800),
    Row(department="HR", employee="Carol", salary=4000),
    Row(department="HR", employee="David", salary=3900),
    Row(department="IT", employee="Eve", salary=6000)
]

df = spark.createDataFrame(data)
df.show()

Output:

	+----------+--------+------+
	|department|employee|salary|
	+----------+--------+------+
	|     Sales|   Alice|  5000|
	|     Sales|     Bob|  4800|
	|        HR|   Carol|  4000|
	|        HR|   David|  3900|
	|        IT|     Eve|  6000|
	+----------+--------+------+

Che cos’è il GroupBy di PySpark?

Il groupBy di PySpark è una trasformazione usata per suddividere i dati in gruppi in base a una o più colonne, che poi possono essere aggregati o trasformati in modo indipendente.

Sintassi di base ed esempi

Usare il metodo groupBy è semplice; puoi richiamarlo sul dataframe di interesse. Puoi usarlo su qualsiasi tipo di colonna purché sia adatto alla partizione. Ad esempio, potresti voler evitare i float.

grouped = df.groupBy("department")

Questo crea un oggetto GroupedData, che non è un DataFrame in sé. Indica a Spark come iniziare a partizionare i dati. Una volta ottenuto questo oggetto GroupedData, puoi applicare aggregazioni .count() o .sum() per ottenere un nuovo DataFrame.

grouped.count().show()

Parametri e valori di ritorno

L’unico parametro del metodo è *cols, che accetta nomi di colonne, espressioni di colonne, indici di colonne (int) o una lista di colonne. Finché la colonna è qualcosa su cui vuoi partizionare, puoi usarla nell’operazione. Restituisce sempre un oggetto GroupedData.

# raggruppare per nome colonna come sopra
	df.groupBy("department")

# raggruppare per espressione di colonna
	df.groupBy(df.department)

# raggruppare per indice di colonna
	df.groupBy(1)

# raggruppare per lista di colonne, puoi mescolare i metodi!
	df.groupBy(["department", 2])

GroupBy su colonne singole e multiple

Come mostrato sopra, puoi raggruppare su una o più colonne. Una singola colonna è ottima quando ti interessa un solo asse, come reparti o anni. Più colonne sono utili se vuoi più livelli di dettaglio, ad esempio specifici venditori in reparti diversi, o mesi specifici all’interno di un anno.

# raggruppare per singola colonna
	df.groupBy("department").sum(“salary”).show()

# raggruppare per più colonne
	df.groupBy(["department", 2]).sum(“salary”).show()

Come avrai notato, il metodo groupBy funziona in modo molto simile alle istruzioni GROUP BY di SQL. Più avanti mostreremo anche un modo per usare quel linguaggio in PySpark.

Funzioni e tecniche di aggregazione

PySpark supporta un’ampia gamma di metodi di aggregazione integrati per lavorare con dati raggruppati. Se conosci SQL, molte di queste ti saranno familiari, come count(), sum(), avg() e così via. Per un ripasso, dai un’occhiata alla nostra guida alle funzioni di aggregazione in SQL. 

Funzioni di aggregazione integrate

Passiamo in rassegna le funzioni di aggregazione integrate:

  • count(): conta il numero di record in questa partizione
  • sum(): riassume il totale dei valori numerici
  • avg(): calcola la media dei valori numerici
  • min(): restituisce il valore più piccolo della partizione
  • max(): restituisce il valore più grande della partizione

Oltre alle funzioni di aggregazione, puoi aggiungere il metodo .alias() per rinominare le colonne in modo che siano più comprensibili. Mostreremo un esempio qui sotto.

Aggregazioni multiple con agg()

Invece di eseguire ogni aggregazione indipendentemente, puoi farne più di una alla volta usando il metodo agg(). Ogni aggregazione creerà una nuova colonna nel tuo dataframe. Riduce anche la necessità di chiamare più volte groupBy, migliorando le prestazioni. Non è necessario aggregare sulla stessa colonna con agg(): puoi definire una colonna diversa per ogni funzione di aggregazione.

# After already starting your session
from pyspark.sql import functions as sf

df.groupBy("department").agg(
    sf.count("employee").alias("employee_count"), 
    sf.avg("salary").alias("avg_salary"),
    sf.max("salary").alias("max_salary")
).show()

Output:

+----------+--------------+----------+----------+
|department|employee_count|avg_salary|max_salary|
+----------+--------------+----------+----------+
|     Sales|             2|    4900.0|      5000|
|        HR|             2|    3950.0|      4000|
|        IT|             1|    6000.0|      6000|
+----------+--------------+----------+----------+

Schemi di aggregazione avanzati

Il vantaggio di PySpark è la possibilità di usare schemi di aggregazione più avanzati, come pivotare i dati, eseguire rollup e creare data cube. Puoi anche creare grouping set. 

Pivot

Proprio come creare una tabella pivot in Excel, puoi pivotare i tuoi dati su colonne diverse. In questo caso, stiamo raggruppando per department e facendo pivot sulla colonna employee per vedere il salario totale di ciascun dipendente. 

Questo significa che ogni riga mostra il reparto e ogni colonna i dipendenti di quel reparto. Puoi intuire quanto sarebbe potente se avessimo dati per anno e volessimo vedere come si è comportato ogni reparto anno su anno.

df.groupBy("department").pivot("employee").sum("salary").show()

Rollup e cube

Due modi molto potenti per aggregare i dati sono rollup() e cube().  Mentre un semplice groupby() mostrerebbe i risultati per le aggregazioni esistenti, rollup() e cube() sono entrambe strutture gerarchiche. Significa che aggregano i dati a un livello più granulare.

Ad esempio, rollup() aggrega da sinistra a destra, mostrando ogni possibile permutazione se seguissimo le iterazioni passo dopo passo. Per ogni reparto mostriamo ogni persona nel reparto e anche il gruppo finale di chi non rientra.

D’altra parte, cube() mostra tutte le permutazioni possibili per tutte le colonne aggregate, quindi per ogni reparto separatamente, ogni dipendente separatamente e ogni combinazione di entrambi. 

In sintesi:

  • Rollup crea subtotali gerarchici seguendo l’ordine delle colonne.
  • Cube genera subtotali per ogni possibile combinazione delle colonne specificate.

Puoi vedere un esempio nel codice e nell’output qui sotto:

rollup() Codice:

# using rollup
df.rollup("department", “employee”).sum("salary").show()

rollup() Output:

+----------+--------+-----------+
|department|employee|sum(salary)|
+----------+--------+-----------+
|      NULL|    NULL|      23700|
|     Sales|   Alice|       5000|
|     Sales|    NULL|       9800|
|     Sales|     Bob|       4800|
|        HR|   Carol|       4000|
|        HR|    NULL|       7900|
|        HR|   David|       3900|
|        IT|     Eve|       6000|
|        IT|    NULL|       6000|
+----------+--------+-----------+

cube() Codice:

#using cube
df.cube("department", "employee").sum("salary").show()

cube() Output:

+----------+--------+-----------+
|department|employee|sum(salary)|
+----------+--------+-----------+
|      NULL|   Alice|       5000|
|      NULL|    NULL|      23700|
|     Sales|   Alice|       5000|
|     Sales|    NULL|       9800|
|     Sales|     Bob|       4800|
|      NULL|     Bob|       4800|
|      NULL|   Carol|       4000|
|        HR|   Carol|       4000|
|        HR|    NULL|       7900|
|        HR|   David|       3900|
|      NULL|   David|       3900|
|        IT|     Eve|       6000|
|        IT|    NULL|       6000|
|      NULL|     Eve|       6000|
+----------+--------+-----------+

Grouping set

I grouping set ti permettono di definire più livelli di aggregazione. In questo esempio, sto aggregando a livello di reparto e dipendente, a livello di reparto e a tutti i livelli per ottenere il salario totale.

Noterai che la sintassi è un po’ diversa per il metodo groupingSets(). Prima definisci la lista dei set [(“department”, “employee”), (“department”, ), ()] dove il primo set è reparto e dipendente, il secondo set sono solo i reparti e l’ultimo set di () significa tutti. 

Definisci poi le colonne di aggregazione all’interno dei set. Il resto si scrive come al solito usando .agg().

df.groupingSets([("department", "employee"), ("department",), ()], "department","employee").agg(sf.sum("salary")).sort("department","employee").show()

Output: 

+----------+--------+-----------+
|department|employee|sum(salary)|
+----------+--------+-----------+
|      NULL|    NULL|      23700|
|        HR|    NULL|       7900|
|        HR|   Carol|       4000|
|        HR|   David|       3900|
|        IT|    NULL|       6000|
|        IT|     Eve|       6000|
|     Sales|    NULL|       9800|
|     Sales|   Alice|       5000|
|     Sales|     Bob|       4800|
+----------+--------+-----------+

Funzioni di aggregazione personalizzate

Infine, possiamo creare funzioni di aggregazione personalizzate usando le User Defined Functions (UDF) in PySpark. Esistono due modi: udf e pandas_udf. Entrambi permettono di creare funzioni personalizzate, ciascuno con pro e contro.

La udf standard di Spark ti consente di creare funzioni native Spark. Ciò significa che la sintassi e i tipi di dato devono funzionare nativamente in Spark. Pur limitando alcune possibilità all’interno della UDF, ti permette di sfruttare appieno la potenza computazionale distribuita di Spark ed è migliore per dataset di grandi dimensioni.

from pyspark.sql.functions import udf
from pyspark.sql.types import IntegerType

@udf(returnType=IntegerType())
def bonus(salary):
    return int(salary * 0.1)

df.withColumn("bonus", bonus(df.salary)).show()

D’altra parte, pandas_udf ti consente di creare funzioni personalizzate più “pythoniche”. Ora sei vincolato solo a ciò che è possibile in Pandas e non solo in Spark. 

Tuttavia, questo significa che non puoi sfruttare la potenza computazionale distribuita di Spark e ti affidi al calcolo locale. È più adatto a dataset piccoli o medio-piccoli.

import pandas as pd
from pyspark.sql.functions import pandas_udf

@pandas_udf("double")
def salary_bonus(s: pd.Series) -> pd.Series:
    return s * 0.1

df.withColumn("bonus", salary_bonus(df.salary)).show()

In generale, conviene usare funzioni personalizzate solo se e solo se non esistono modi migliori per eseguire l’aggregazione. Non reinventare la ruota! 

Inoltre, se qualcosa può essere vettorizzato (come le funzioni sopra), scegli una versione vettorizzata della funzione invece di una UDF di aggregazione. Per ulteriori informazioni su groupBy(), leggi questo articolo che approfondisce il framework split-apply-combine con pandas.

Filtrare i dati aggregati

In PySpark, puoi filtrare i gruppi in base a metriche aggregate post-raggruppamento usando il metodo filter(). In questo metodo fornisci una condizione, in espressioni Python o SQL. 

A tal proposito, puoi anche usare where() se preferisci, poiché è semplicemente un alias di filter() e svolge la stessa operazione.

Puoi scegliere se filtrare prima o dopo l’aggregazione. Filtrare prima inciderà sull’aggregazione limitando i dati che vengono aggregati e può migliorare le prestazioni. 

Ad esempio, magari vogliamo contare solo il numero di dipendenti sopra una certa retribuzione per trovare il numero di dipendenti “ad alto salario”.

#filter per salari alti
filter_df = df.filter(df.salary > 4000)

#aggregare e trovare il totale dei dipendenti con salario alto
agg_filter_df = filter_df.groupBy("department").agg(sf.count("*").alias("high_salary_emp"))
agg_filter_df.show()

Tuttavia, filtrare dopo l’aggregazione non influenzerà l’aggregazione originale. Magari vogliamo contare tutti i dipendenti in un reparto e poi vedere solo i reparti sopra una certa dimensione. Questo aggiunge uno strato extra di elaborazione, ma non dovrebbe aumentare molto il carico computazionale.

# creare un dataframe che conta il numero di dipendenti
agg_df = df.groupBy("department").agg(sf.count("*").alias("num_employees"))

# filtrare per reparti con più di 1 dipendente
agg_df.where("num_employees > 1").show()

Scegli con attenzione quando filtrare, perché influirà sull’accuratezza e sul risultato finale. Filtrare prima può ridurre il totale finale, mentre filtrare dopo può lasciare troppi elementi nella risposta finale.

Strategie di ottimizzazione delle prestazioni di PySpark groupBy

Sebbene PySpark ottimizzi automaticamente l’operazione groupBy, esistono strategie che possono migliorare ulteriormente la velocità di elaborazione. Elementi come la minimizzazione dello shuffle dei dati, la mitigazione dello skew e l’ottimizzazione dell’esecuzione contribuiscono tutti a migliorare le prestazioni di groupBy.

Gestione dello shuffle

groupBy causa uno shuffle, che ridistribuisce i dati tra le partizioni per raggruppare insieme le chiavi simili. Gli shuffle sono intensivi per rete e disco, quindi ridurli o ottimizzarli è cruciale. Ecco alcune tecniche chiave di ottimizzazione:

  • Usa repartition() con criterio: puoi dire a PySpark in base a quale colonna partizionare i dati in modo che cerchi in meno punti i dati
  • Regola spark.sql.shuffle.partitions. Per impostazione predefinita, PySpark usa 200 partizioni di shuffle. Se il dataset è più piccolo, puoi ridurre questo numero.
  • Abilita la compressione dello shuffle. Comprimere gli shuffle ridurrà il sovraccarico di rete e disco.

Ecco alcuni esempi di come migliorare la gestione dello shuffle.

# ridurre il numero di partizioni di shuffle
spark.conf.set("spark.sql.shuffle.partitions", "64")  # Regola in base alla dimensione del cluster

# assicurarsi che spark comprima i dati
spark.conf.set("spark.shuffle.compress", "true") # comprime il trasferimento in rete
spark.conf.set("spark.shuffle.spill.compress", "true") # comprime gli spill su disco

# ripartiziona i dati prima del raggruppamento per ottimizzare l'aggregazione
df.repartition("department").groupBy("department").sum("salary").show()

Tecniche di mitigazione dello skew

Lo skew dei dati si verifica quando alcune chiavi compaiono in proporzione molto maggiore rispetto ad altre, causando carichi di partizione sbilanciati e task lenti. Questo mette più pressione su alcuni worker rispetto ad altri. Possiamo ridurre lo skew e distribuire il lavoro in modo più uniforme usando salting, minimizzando join sbilanciati e broadcast join per dimensioni più piccole.

  • Salting: aggiungi una colonna di numeri casuali che forza una distribuzione uniforme tra i worker
  • Repartitioning per minimizzare lo skew: forzare Spark a usare una colonna diversa per la partizione può portare a una distribuzione del carico migliore
# Esempio di salting:

df = df.withColumn("salted_key", sf.rand()) #Crea colonna di numeri casuali
df = df.repartition(2, 'salted_key') # usa questo per ripartizionare i dati
df.groupBy(sf.spark_partition_id()).count().show()

Ottimizzazione dell’esecuzione

I piani di esecuzione logici e fisici dei job PySpark possono essere ottimizzati con funzionalità integrate. Queste possono migliorare le prestazioni delle tue aggregazioni groupBy.

  • Optimizer Catalyst: Spark riscrive automaticamente i piani inefficienti, ma scrivere trasformazioni dichiarative (non loop procedurali) aiuta l’optimizer a fare il suo lavoro.
  • Caching: Il caching è utile quando lo stesso risultato di groupBy viene utilizzato più volte in una pipeline.
  • Broadcast join: Effettua il broadcast dei dataframe più piccoli per tenerli in memoria mentre i dataset più grandi vengono suddivisi e partizionati, minimizzando il sovraccarico di rete e disco sul cluster.
grouped_df = df.groupBy("department").sum("salary").cache()
grouped_df.show() # puoi mettere in cache i risultati intermedi

# usa il broadcasting (pseudocodice qui):
from pyspark.sql.functions import broadcast
df.join(broadcast(smaller_df), "department").show()

Per maggiori dettagli su come ottimizzare le operazioni di PySpark, leggi questo articolo sui join in PySpark per capire alcuni meccanismi interni.

Analisi comparativa con operazioni RDD

Sebbene la DataFrame API sia l’interfaccia consigliata per la maggior parte degli utenti PySpark grazie a un livello di astrazione più alto ed efficienza prestazionale, merito del Catalyst Optimizer, comprendere il livello Resilient Distributed Dataset (RDD) può essere utile, specialmente per chi vuole più controllo o sta migrando codice Spark legacy. In generale, però, usare la DataFrame o la SQL API offre prestazioni migliori.

L’RDD è il componente fondamentale di PySpark, su cui si basa tutto (inclusa la DataFrame API). Tende a lavorare in memoria e gestisce lo streaming dei dati un po’ meglio della DataFrame API. Tuttavia, i dati sono immutabili e quindi non possono essere modificati una volta creato l’RDD. 

Questa sezione confronta groupBy nella DataFrame API con i suoi equivalenti nella RDD API, analizzando sia le prestazioni sia l’adeguatezza ai casi d’uso.

Per prima cosa, trasformiamo il dataframe in un RDD.

# Convert DataFrame to RDD of (key, value)
rdd = df[['department','salary']].rdd
rdd.collect()

Ora possiamo eseguire operazioni come groupByKey() e reduceByKey() sull’RDD.  Vediamo prima groupByKey().

Il metodo groupByKey() invia tutte le valori con la stessa chiave allo stesso executor e poi li raggruppa. Per questo motivo, può volerci molto tempo affinché i dati vengano rimescolati verso la chiave giusta. Questo può anche portare a skew dei dati.

rdd.groupByKey().mapValues(list).collect()

Invece, possiamo provare il metodo reduceByKey(). Questo metodo aggrega prima su ogni partizione e poi sposta i dati sulla rete. Ciò minimizza la quantità di dati rimescolati e di conseguenza funziona più velocemente. 

Quindi, reduceByKey è fortemente preferito rispetto a groupByKey perché riduce in modo significativo le operazioni di shuffle.

In pratica, aggrega in ogni partizione, poi combina i dati e poi li aggrega insieme. È ideale per aggregazioni che riuniscono le chiavi, come sum() e max().

from operator import add

rdd.reduceByKey(add).collect()

Vantaggi prestazionali dei metodi di aggregazione DataFrame vs RDD

Ecco una tabella che riassume i vantaggi di ciascun metodo di aggregazione.

Caratteristica

DataFrame groupBy

RDD groupByKey()

RDD reduceByKey()

Livello di astrazione

Alto

Basso

Basso

Esecuzione ottimizzata

Sì (Catalyst & Tungsten)

No

No

Minimizzazione dello shuffle

Sì

❌ (Shuffle completo)

✅ (Con combiner)

Efficienza della memoria

Alta

Bassa

Media–Alta

Flessibilità

Moderata

Alta

Media

Prestazioni su Big Data

Eccellenti

Scarse

Buone

Uso consigliato

Nella maggior parte dei casi

Raro/specializzato

Aggregazioni personalizzate, basso livello

Il corso Big Data Fundamentals with PySpark copre in dettaglio la programmazione con gli RDD e descrive come costituiscano l’ossatura di PySpark.

Query PySpark SQL GROUP BY

Un altro modo per eseguire aggregazioni in PySpark è usare la SQL API per scrivere istruzioni in SQL. Può essere un’ottima strada se ti senti più a tuo agio a scrivere SQL.

Il primo passo è creare una vista temporanea usando il metodo createOrReplaceTempView() del DataFrame. Poi puoi usare spark.sql() per scrivere la tua istruzione.

# Create a temporary view using the DataFrame
df.createOrReplaceTempView("employees")

# Write a SQL-like statement
spark.sql("""
    SELECT department, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department
""").show()

Output:

+----------+----------+
|department|avg_salary|
+----------+----------+
|     Sales|    4900.0|
|        HR|    3950.0|
|        IT|    6000.0|
+----------+----------+

Come vedi, è facile quanto scrivere una normale query SQL. Il momento principale per usare la SQL API è per familiarità e per scrivere le query con più facilità. La DataFrame API spesso è migliore perché fornisce tipi di dato coerenti e di solito ottimizza meglio. 

Con essa puoi accedere a metodi dei DataFrame a cui non avresti accesso con la Spark SQL API. Per maggiori informazioni sul GROUP BY in SQL, puoi leggere questo articolo su GROUP BY e HAVING in SQL.

Applicazioni reali

Ci sono molte applicazioni reali per l’aggregazione dei dati. Anzi, è quasi certo che dovrai aggregare i dati in qualche modo per analizzarli e condividerli.

Di seguito forniremo alcuni esempi di diversi casi d’uso delle aggregazioni. Alcuni potrebbero essere pseudo-codice o non perfettamente applicabili al nostro dataset di test, ma servono a darti un’idea di come potresti scrivere queste aggregazioni.

Business intelligence

Un tema comune di cui abbiamo parlato è l’idea di usare groupBy come mezzo di analisi gerarchica. Immagina di avere una colonna “revenue” e fossi interessato a come si è comportato ciascun reparto. Possiamo raggruppare per reparto e valutare il contributo di ricavi di ogni reparto.

CopyEdit
df.groupBy("department").agg(sum("revenue").alias("total_revenue")).show()

Analisi delle serie temporali

Le aggregazioni ti danno accesso alle funzioni finestra. Queste funzioni considerano una finestra scorrevole di dati, in sequenza per righe o per periodi di tempo. 

Definiamo prima una finestra usando l’oggetto Window per indicare cosa vogliamo partitonBy e orderBy. Usiamo poi questo oggetto nel metodo over() della funzione di aggregazione scelta. Ad esempio, una media mobile sarebbe sf.avg().over(window).show().

Immagina di voler vedere una media mobile dei salari per ogni reparto. Raggrupperemmo per department e ordineremmo per una nuova colonna che possiamo chiamare employeeId. Poiché gli ID dipendente sono spesso sequenziali, possiamo vedere come è cambiato nel tempo il salario medio. 

from pyspark.sql.window import Window

#Define the window we wish to partition and order by
windowSpec = Window.partitionBy("department").orderBy("employeeId")

#Perform the sf.avg function on the “salary” column over the windowSpec.
df.withColumn("rolling_avg", sf.avg("salary").over(windowSpec)).show()

Questo è molto simile a come faremmo le funzioni finestra in SQL.

Analytics per i media

Magari vogliamo capire metriche utente diverse per la nostra media company. Vogliamo cose come tempo totale di visione e video unici. Possiamo fare groupBy() sulla colonna user_id seguito da .agg() con funzioni diverse come sum() e countDistinct() per metriche differenti.

df.groupBy("user_id").agg(
    sf.sum("watch_time").alias("total_watch"), #somma dei minuti visti
    sf.countDistinct("video_id").alias("unique_views") #conteggio dei video unici visti
).show()

Best practice e trappole da evitare con PySpark GroupBy

Quando aggreghi dati in PySpark, è facile cadere in alcune trappole che portano a prestazioni scarse e tempi di esecuzione lunghi. Seguire queste best practice aiuta a garantire prestazioni, correttezza e scalabilità. 

Errori comuni

Ecco alcuni modi comuni con cui si trasformano query semplici in programmi a lunga esecuzione.

  1. Uso eccessivo di groupByKey() negli RDD:
    • Evita groupByKey() negli RDD, poiché il suo meccanismo fa sì che PySpark effettui shuffle dei dati tra le partizioni. Questo può comportare molto overhead di rete e disco.
    • Invece, preferisci usare reduceByKey() o resta sulla DataFrame API.
  2. Non gestire i dati skewed:
    • Quando un gruppo domina (ad es. un reparto ha milioni di record), quel task può diventare un collo di bottiglia.
    • Usa salting o partizionamento personalizzato per mitigare, redistribuendo il carico di partizione con il metodo repartition().
  3. Forgettare di concatenare le funzioni di aggregazione:
    • Scrivere funzioni di aggregazione su righe separate, ad esempio avere sum() in un groupBy e poi count() sullo stesso dataset raggruppato in un’altra riga, porta a piani inefficienti rompendo la capacità di ottimizzazione di PySpark.
    • PySpark risolve i problemi con un modello di valutazione lazy, cioè genera l’intero piano prima di eseguire azioni. Concatenare trasformazioni (ad es. groupBy().agg().filter()) consente al Catalyst optimizer di pianificare in modo efficiente.
  4. Uso scorretto delle UDF:
    • Le UDF Python impediscono a Spark di ottimizzare pienamente le query, poiché Catalyst non riesce a ottimizzarle.
    • Se possibile, usa le funzioni integrate di Spark o le pandas UDF per prestazioni migliori.
  5. Gestione errata della memoria:
    • Le aggregazioni possono essere costose e occupare molta memoria. Questo può portare a esaurimento della memoria quando si riutilizzano più volte i risultati dell’aggregazione.
    • Monitora l’uso di memoria e valuta l’uso di persist() o cache() quando riutilizzi dati raggruppati.

Suggerimenti di ottimizzazione ed efficienza

Ecco alcuni modi per ottimizzare le tue aggregazioni PySpark e mantenerle fluide!

Gestione dei null

  • Aggregare su colonne con null può dare risultati inattesi.
  • Usa na.fill() o na.drop() prima dell’aggregazione.
df.na.fill({"salary": 0}).groupBy("department").sum("salary").show()

Evita shuffle eccessivi

  • Come discusso sopra, lo shuffle porta a operazioni lente. Ripartiziona logicamente i dati prima del raggruppamento per minimizzare la dimensione dello shuffle.
  • Sintonizza spark.sql.shuffle.partitions in base al volume di dati e verifica quale numero di partizioni funziona meglio per i tuoi dati.

Usa i piani explain

  • Monitora i piani di query con .explain() per capire l’esecuzione fisica.
  • Cerca segni di shuffle ampi, suggerimenti di broadcast e scan inefficienti
df.groupBy("department").sum("salary").explain(True)

Output:

	== Parsed Logical Plan ==
	'Aggregate ['department], ['department, unresolvedalias('sum(salary#600L))]
	+- LogicalRDD [department#598, employee#599, salary#600L], false

	== Analyzed Logical Plan ==
	department: string, sum(salary): bigint
	Aggregate [department#598], [department#598, sum(salary#600L) AS sum(salary)#679L]
	+- LogicalRDD [department#598, employee#599, salary#600L], false

	== Optimized Logical Plan ==
	Aggregate [department#598], [department#598, sum(salary#600L) AS sum(salary)#679L]
	+- Project [department#598, salary#600L]
	   +- LogicalRDD [department#598, employee#599, salary#600L], false

	== Physical Plan ==
	AdaptiveSparkPlan isFinalPlan=false
	+- HashAggregate(keys=[department#598], functions=[sum(salary#600L)], output=[department#598, sum(salary)#679L])
	   +- Exchange hashpartitioning(department#598, 200), ENSURE_REQUIREMENTS, [plan_id=1841]
	      +- HashAggregate(keys=[department#598], functions=[partial_sum(salary#600L)], output=[department#598, sum#681L])
	         +- Project [department#598, salary#600L]
	            +- Scan ExistingRDD[department#598,employee#599,salary#600L]
	

Monitora le prestazioni con Spark UI

  • Usa la Spark Web UI per monitorare gli stage, i task e identificare aggregazioni lente o skew.
  • Puoi saperne di più sulla Spark Web UI in questo corso Introduction to Spark SQL in Python .

Sfrutta il tuning della configurazione

Spark ha molte configurazioni che possono essere ottimizzate per carichi di lavoro groupBy ampi. Consulta la tabella qui sotto per alcune configurazioni chiave.

Configurazione

Descrizione

spark.sql.shuffle.partitions

Controlla il numero di partizioni per gli shuffle. Riduci per job piccoli, aumenta per quelli grandi. (Default: 200)

spark.sql.autoBroadcastJoinThreshold

Abilita il broadcast automatico delle tabelle piccole. Imposta a -1 per disabilitare o aumenta per supportare join più grandi. (Default: 10MB)

spark.executor.memory

Controlla la memoria disponibile per ogni executor. Aumentala per aggregazioni ampie. (Default: 4g)

spark.sql.adaptive.enabled

Abilita l’Adaptive Query Execution (AQE), che può ottimizzare dinamicamente shuffle, skew e join. (Default: true)

Note e dettagli di implementazione avanzati

Per utenti avanzati o per chi lavora con dataset molto grandi, capire come groupBy si comporta “sotto il cofano” è essenziale. Vediamo alcuni dettagli su come funziona questo metodo.

Valutazione lazy e piano di esecuzione

Tutte le operazioni sui DataFrame in PySpark, incluso groupBy, sono valutate in modo lazy. Significa che non avviene alcun calcolo finché non viene attivata un’azione (come show() o collect()). 

Questo permette al Catalyst optimizer di riordinare, combinare o rimuovere operazioni per migliorare le prestazioni. Sfrutta questo aspetto concatenando più aggregazioni e metodi, così Spark può creare un piano ottimizzato.

Tipo di ritorno e collisioni di nomi

Quando usi groupBy() viene restituito un oggetto GroupedData. Eseguire una funzione di aggregazione, come sum(), restituisce poi un oggetto simile a un DataFrame. Se usi show() ne vedrai i risultati.

Ricorda che groupBy().agg() restituisce un nuovo DataFrame con colonne rinominate. Usa sempre alias con il metodo alias() per evitare collisioni di nome e rendere gli output più chiari. Se lasci colonne senza alias, potresti incorrere in collisioni di nomi o problemi con i join più avanti.

from pyspark.sql import functions as F

df.groupBy("department") \

  .agg(

      sum("salary").alias("total_salary"),

      count("*").alias("employee_count")

  )

Formato del risultato dell’aggregazione

Da notare che in PySpark l’aggregazione restituita non preserva l’ordine delle righe. Usa orderBy() per output prevedibili e un ordinamento coerente delle righe.

df.groupBy("department").sum("salary").orderBy("department").show()

Comportamenti specifici di versione e limitazioni

Esistono alcuni cambiamenti e limitazioni specifici per ciascuna versione di PySpark. Catalyst è stato introdotto nella 1.3 e notevolmente migliorato nella 2.0. Alcuni metodi sono stati introdotti solo in versioni successive di PySpark, come groupingSets(). 

Ecco una tabella che copre alcuni dei cambiamenti funzionali più importanti per PySpark. Assicurati di usare la versione giusta di Spark e PySpark per le funzionalità di cui hai bisogno! 

Caratteristica

Versione Spark

Descrizione

cube() / rollup()

1.4+

Aggregazioni gerarchiche utili in analytics stile OLAP (3.4+ supporta Spark Connect)

pandas_udf

2.3+

UDF vettorizzate con Apache Arrow per esecuzione più rapida (3.4+ supporta Spark Connect, 4.0+ supporta SCALAR)

Adaptive Query Execution (AQE)

3.0+

Regola dinamicamente join, shuffle e gestione dello skew a runtime

Modalità di compatibilità ANSI SQL

3.0+

Segnalazione errori e comportamento delle espressioni più accurati

groupingSets()

4.0+

Consente più raggruppamenti in un’unica aggregazione (ad es., per subtotali)

Conclusione

La funzione groupBy di PySpark è uno strumento essenziale per l’aggregazione dei dati in ambienti distribuiti. Che si tratti di sintetizzare dati per regione, calcolare metriche medie o eseguire analisi multilivello complesse, groupBy offre un’API scalabile e flessibile per gestire carichi di lavoro su big data.

Ricorda solo queste best practice per minimizzare tempi e costi:

  • Usa dove possibile le funzioni integrate
  • Evita trappole prestazionali come shuffle eccessivi e skew dei dati.
  • Sfrutta le ottimizzazioni e gli strumenti di profilazione di Spark per mettere a punto job di grandi dimensioni.

Man mano che PySpark evolve, possiamo aspettarci ottimizzazioni ancora più intelligenti, supporto nativo per aggregazioni complesse e una migliore integrazione con Pandas e la sintassi in stile SQL. Potrebbe anche offrire un’integrazione migliore per modelli su larga scala. Se vuoi saperne di più su PySpark, dai un’occhiata a queste risorse dettagliate di DataCamp:

PySpark groupBy - Domande frequenti

Cosa restituisce effettivamente il metodo groupBy() di PySpark?

Restituisce un oggetto GroupedData, non un DataFrame. Questo oggetto deve essere seguito da un metodo di aggregazione come .count(), .sum() o .agg() per restituire un nuovo DataFrame con i risultati raggruppati.

Cos’è il data skew e come può influire su groupBy()?

Lo skew dei dati si verifica quando una o poche chiavi hanno una quantità di dati sproporzionata, sovraccaricando alcuni executor mentre altri restano inattivi. Può portare a prestazioni lente o al fallimento del job. Puoi mitigare lo skew usando salting, partizionamento personalizzato o broadcast join per dimensioni piccole.

Posso usare groupBy() su più colonne in PySpark?

Sì, puoi raggruppare con groupBy() su una lista di colonne.

Quando dovrei usare funzioni di aggregazione personalizzate (UDF o pandas_udf)?

Usale solo quando le funzioni integrate non bastano. Le funzioni integrate sono più veloci e beneficiano delle ottimizzazioni di Spark. Le UDF impediscono a Catalyst di ottimizzare il piano, mentre le pandas_udf scambiano scalabilità con flessibilità e andrebbero usate solo per dataset di dimensioni medio-piccole.

Come ottimizzo le operazioni groupBy() per le prestazioni in PySpark?

Minimizza gli shuffle usando .repartition() e regolando spark.sql.shuffle.partitions. Ricorri a .cache() quando riutilizzi risultati aggregati. Controlla come Spark ottimizza e le sue raccomandazioni usando .explain().


Tim Lu's photo
Author
Tim Lu
LinkedIn

Sono una data scientist con esperienza in analisi spaziale, machine learning e pipeline dei dati. Ho lavorato con GCP, Hadoop, Hive, Snowflake, Airflow e altri processi di data science/engineering.

Argomenti
PySpark
Python

I migliori corsi su PySpark

Corso

Fondamenti di PySpark

4 ore
157.9K
Impara a usare la gestione distribuita dei dati e l'apprendimento automatico in Spark con il pacchetto PySpark.
Vedi i dettagliRight Arrow
Inizia Il Corso
Mostra altroRight Arrow
Correlato

blog

Che cos'è Snowflake? Guida per principianti alla piattaforma dati cloud

Esplora le basi di Snowflake, la piattaforma dati cloud. Scopri la sua architettura, le sue funzionalità e come integrarla nelle tue pipeline di dati.
Tim Lu's photo

Tim Lu

12 min

blog

Tokenizzazione nel NLP: come funziona, sfide e casi d'uso

Guida al preprocessing NLP nel machine learning. Copriamo spaCy, i transformer di Hugging Face e come funziona la tokenizzazione in casi d'uso reali.
Abid Ali Awan's photo

Abid Ali Awan

10 min

blog

I 15 migliori server MCP remoti che ogni AI builder dovrebbe conoscere nel 2026

Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Abid Ali Awan's photo

Abid Ali Awan

15 min

Mostra AltroMostra Altro