Approach: exploit the existing partitioning by event_date, do local (map-side) pre-aggregation within each partition, then perform a single keyed shuffle across user_id+event_date using combineByKey (or reduceByKey). Persist minimal columns and write output partitioned by event_date to avoid downstream shuffles.python
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
from pyspark import StorageLevel
spark = SparkSession.builder.getOrCreate()
# Read events already partitioned by event_date on storage (e.g., /data/events/event_date=YYYY-MM-DD/)
events = spark.read.parquet("/data/events/") \
.select("event_date", "user_id", "event_type", "value") # prune cols early
# Convert to RDD of ((event_date, user_id), (count, sum)) and do map-side combiner
rdd = events.rdd.map(lambda r: ((r.event_date, r.user_id), (1, float(r.value))))
# combineByKey does local aggregation (map-side) then a single shuffle per key
def create_combiner(v): return v
def merge_value(acc, v): return (acc[0] + v[0], acc[1] + v[1])
def merge_combiners(a, b): return (a[0] + b[0], a[1] + b[1])
agg_rdd = rdd.combineByKey(create_combiner, merge_value, merge_combiners)
# Convert back to DataFrame and write partitioned by event_date (no shuffle on write if using partitioning and bucketed sink)
agg_df = agg_rdd.map(lambda kv: (kv[0][0], kv[0][1], kv[1][0], kv[1][1])) \
.toDF(["event_date", "user_id", "event_count", "event_value_sum"])
# Repartition by event_date only (cheap if data already on disk partitioned by date) and write
agg_df.repartition(col("event_date")).write.mode("overwrite").partitionBy("event_date").parquet("/data/agg/user_daily/")
Key points:- Early column pruning reduces data shuffled.- Using RDD.combineByKey (map-side combiner) aggregates within each mapper partition before the shuffle, reducing network traffic.- Keys include event_date so final shuffle groups per user per day; writing partitioned by event_date avoids future shuffles.- Persist/partition awareness: if reading many dates, process per-date slices (filter by event_date) to limit working set.Complexity: single shuffle across unique (date,user) keys; network I/O proportional to number of distinct keys. Edge cases: skewed users (hot keys) — handle with salting; nulls in value; very large single partitions — use larger parallelism or split by date ranges. Alternatives: use DataFrame aggregations with shuffle partitions tuned (spark.sql.shuffle.partitions) or use mapInPandas for vectorized map-side aggregation.