I know Python + pandas: how much effort for PySpark?

You already carry two thirds of the bag. Panda cooks alone in one kitchen; Sparky runs a restaurant chain on many EC2 machines. Real pandas vs PySpark code and outputs on the CareWell file.

1

๐Ÿ“Š Effort: what you already know (teacher's estimate)

If you know Python and pandas well, about 60โ€“70% of PySpark is already yours. The new 30โ€“40% is mostly how Spark runs on many EC2 machines. These percentages are the teacher's estimate, not a measurement.

TopicAlready knownNew effort
Python language100%0%
Data thinking (rows, columns, joins, aggregations)~90%~10%
DataFrame API syntax~60%~40%
Spark SQL (if you know SQL)~70%~30%
How Spark runs: lazy plans, partitions, shuffle, jobs / stages / tasks0%100%
Performance tuning: joins, skew, caching, AQE, file sizes0%100%
Cluster & cloud: EMR, Glue, Databricks, S3, spark-submit, Spark UI0%100%
Overall~60โ€“70%~30โ€“40%

๐Ÿณ pandas vs PySpark

๐Ÿผ pandasโšก PySpark
Where it runsone machine, data must fit in RAM (GBs)driver + executors on many EC2 machines (TBs)
When work happenseager: every line runs nowlazy: builds a plan; an action (count, show, write, collect) runs it
Changing datamutable; inplace=True changes the same DataFrameimmutable; every step returns a new DataFrame
Index / row orderhas an index ([0, 1, 2]), order is keptno index (hasattr(df, "index") = False), order only after orderBy
Missing valueNaN (real: nan)null (Python None)
Outputone file (out_pandas.csv)a folder of part files + _SUCCESS (real: 3 part files)
Custom Pythonapply() is finePython UDFs are slow (rows go to a Python worker); prefer F. functions
Small data speedfast: 18 ms (read + groupby, 1,000 rows)slower: 579 ms, plus 7,313 ms SparkSession start-up (sandbox)
Optimizernone: runs what you wroteCatalyst + AQE: pushdown, pruning, join choice

Timings are from one small run in a sandbox (1,000 rows, PySpark 4.2.0 local[3], pandas 2.3.3). On small data pandas wins; Spark pays off when data does not fit one machine.

๐Ÿ” Syntax translation (pandas โ†’ PySpark)

TaskpandasPySpark (from pyspark.sql import functions as F)
look at rowspdf.head(3)df.show(3)
new columnpdf["c"] = exprdf.withColumn("c", expr)
filterpdf[pdf["age"] >= 60]df.filter(F.col("age") >= 60)
group + aggregatepdf.groupby("d")["x"].mean()df.groupBy("d").agg(F.avg("x"))
joinpdf.merge(o, on="k")df.join(o, on="k")
missingpdf["c"].isna() / fillna({...})F.col("c").isNull() / fillna({...})
sortsort_values("x", ascending=False)orderBy(F.desc("x"))
row countlen(pdf)df.count()
writepdf.to_csv("out.csv")df.write.csv("out_folder")
drop columnpdf.drop(columns=["c"])df.drop("c")

โญ Only in PySpark: 11 things to learn

  • ๐Ÿ’คLazy evaluation: transformations build a plan; actions run it
  • ๐ŸงฉPartitions: data split into pieces; one task per partition
  • ๐Ÿ”€Shuffle (narrow / wide): wide steps (groupBy, join, orderBy) move rows between EC2 machines
  • ๐Ÿง Driver, executors, jobs, stages, tasks: one action = a job; stages split at shuffles; tasks run on executors
  • ๐Ÿ“กJoin strategies: broadcast a small table, or sort-merge two big ones
  • โš–๏ธData skew: one huge key makes one slow task
  • ๐Ÿ”AQE & optimizer: explain(), predicate pushdown, column pruning, run-time plan changes
  • ๐ŸงŠcache / persist: keep a reused DataFrame in memory
  • ๐ŸขF functions, not UDFs: built-in functions stay in the JVM; Python UDFs are slow
  • ๐Ÿ“ฆFile formats & schema: Parquet, partitionBy, explicit StructType schema
  • โ˜๏ธRunning on a cluster: spark-submit, Spark UI, EMR / Glue / Databricks, S3

๐Ÿ›‘ Habits to break

  • ๐Ÿ”No Python loops, no collect() on big data: use column expressions and write results to S3
  • โœ๏ธNo inplace: always reassign: df = df.withColumn(...)
  • ๐Ÿ”ขDo not rely on row order or an index: sort with orderBy and use a key column

๐Ÿ—บ๏ธ 4-week plan

WhenWhat
Weeks 1โ€“2DataFrame API + Spark SQL: translate your pandas code (fast, mostly known)
Weeks 3โ€“4Internals + tuning: lazy plans, partitions, shuffle, joins, skew, cache, AQE, Spark UI (the real effort)
ThenA project: S3 raw โ†’ Glue or EMR PySpark job โ†’ Parquet curated layer, run with spark-submit

๐ŸŽค Interview Q&A

Real code and real outputs

Each pair ran in one Python process: pandas 2.3.3 and PySpark 4.2.0 (local[3], driver memory 2g), on the CareWell file healthcare_dataset_1000.csv (1,000 rows).

Setup ยท pandas

import pandas as pd
PATH = "healthcare_dataset_1000.csv"          # CareWell, 1,000 rows
pdf = pd.read_csv(PATH)

Setup ยท PySpark

import os, sys, time
os.environ["PYSPARK_PYTHON"] = sys.executable
from pyspark.sql import SparkSession, functions as F
t = time.time()
spark = (SparkSession.builder.master("local[3]").appName("pandas-to-pyspark")
         .config("spark.driver.memory", "2g").config("spark.ui.port", "4074")
         .config("spark.ui.showConsoleProgress", "false").getOrCreate())
spark.sparkContext.setLogLevel("ERROR")
START_MS = (time.time() - t) * 1000
df = spark.read.csv(PATH, header=True, inferSchema=True)

head ยท pandas

print(pdf[["patient_id", "age", "city"]]
      .head(3))
  patient_id   age           city
0      P2000  53.0        Chennai
1      P2001  69.0  Visakhapatnam
2      P2002  34.0        Kurnool

head ยท PySpark

(df.select("patient_id", "age", "city")
   .show(3))
+----------+---+-------------+
|patient_id|age|         city|
+----------+---+-------------+
|     P2000| 53|      Chennai|
|     P2001| 69|Visakhapatnam|
|     P2002| 34|      Kurnool|
+----------+---+-------------+
only showing top 3 rows

newcol ยท pandas

pdf["cost_lakh"] = (pdf["treatment_cost"]
                    / 1e5).round(2)
print(pdf["cost_lakh"].head(3).tolist())
[0.92, 0.08, 0.1]

newcol ยท PySpark

df2 = df.withColumn("cost_lakh",
    F.round(F.col("treatment_cost") / 1e5, 2))
print([r[0] for r in
       df2.select("cost_lakh").take(3)])
[0.92, 0.08, 0.1]

filter ยท pandas

old = pdf[pdf["age"] >= 60]
print("rows:", len(old))
rows: 219

filter ยท PySpark

old = df.filter(F.col("age") >= 60)
print("rows:", old.count())
rows: 219

group ยท pandas

g = (pdf.groupby("department")
       ["treatment_cost"].mean()
       .round(0).sort_values(ascending=False)
       .head(3))
print(g)
department
Orthopedics      73180.0
ENT              66957.0
Ophthalmology    63465.0
Name: treatment_cost, dtype: float64

group ยท PySpark

(df.groupBy("department")
   .agg(F.round(F.avg("treatment_cost"), 0)
        .alias("avg"))
   .orderBy(F.desc("avg"))
   .show(3))
+-------------+-------+
|   department|    avg|
+-------------+-------+
|  Orthopedics|73180.0|
|          ENT|66957.0|
|Ophthalmology|63465.0|
+-------------+-------+
only showing top 3 rows

join ยท pandas

floors = pd.DataFrame({
  "department": ["Cardiology", "Oncology"],
  "floor": [3, 5]})
j = pdf.merge(floors, on="department")
print(len(j), j["floor"].unique().tolist())
123 [3, 5]

join ยท PySpark

floors = spark.createDataFrame(
  [("Cardiology", 3), ("Oncology", 5)],
  ["department", "floor"])
j = df.join(floors, on="department")
print(j.count(), sorted(r[0] for r in
      j.select("floor").distinct().collect()))
123 [3, 5]

fillna ยท pandas

print("missing:", pdf["blood_group"]
      .isna().sum())
f = pdf.fillna({"blood_group": "Unknown"})
print("Unknown:", (f["blood_group"]
      == "Unknown").sum())
missing: 17
Unknown: 17

fillna ยท PySpark

print("missing:", df.filter(
  F.col("blood_group").isNull()).count())
f = df.fillna({"blood_group": "Unknown"})
print("Unknown:", f.filter(
  F.col("blood_group") == "Unknown").count())
missing: 17
Unknown: 17

sort ยท pandas

print(pdf.sort_values("treatment_cost",
      ascending=False)
      [["patient_id", "treatment_cost"]]
      .head(3).to_string(index=False))
patient_id  treatment_cost
     P2411        231757.0
     P2262        231384.0
     P2442        230752.0

sort ยท PySpark

(df.orderBy(F.desc("treatment_cost"))
   .select("patient_id", "treatment_cost")
   .show(3))
+----------+--------------+
|patient_id|treatment_cost|
+----------+--------------+
|     P2411|        231757|
|     P2262|        231384|
|     P2442|        230752|
+----------+--------------+
only showing top 3 rows

write ยท pandas

pdf.to_csv("out_pandas.csv", index=False)
print([f for f in os.listdir(".")
       if f.startswith("out_pandas")])
['out_pandas.csv']

write ยท PySpark

df.repartition(3).write.mode("overwrite").csv(
    "out_spark", header=True)
for f in sorted(os.listdir("out_spark")):
    if not f.startswith("."):
        print(f[:24] + ("โ€ฆ" if len(f) > 24 else ""))
_SUCCESS
part-00000-a368f357-b32aโ€ฆ
part-00001-a368f357-b32aโ€ฆ
part-00002-a368f357-b32aโ€ฆ

lazy ยท pandas

t = time.time()
old = pdf[pdf["age"] >= 60]       # runs NOW
print(type(old).__name__, len(old), "rows")
print(f"{(time.time()-t)*1000:.1f} ms")
DataFrame 219 rows
0.8 ms

lazy ยท PySpark

t = time.time()
old = df.filter(F.col("age") >= 60)  # plan
print(type(old).__name__, "no rows yet")
print(f"{(time.time()-t)*1000:.1f} ms")
t = time.time(); n = old.count()     # action
print(n, f"rows {(time.time()-t)*1000:.0f} ms")
DataFrame no rows yet
25.2 ms
219 rows 204 ms

mutable ยท pandas

pdf.drop(columns=["name"], inplace=True)
print("columns:", pdf.shape[1])
columns: 15

mutable ยท PySpark

df3 = df.drop("name")             # NEW df
print("df :", len(df.columns), "columns")
print("df3:", len(df3.columns), "columns")
df : 16 columns
df3: 15 columns

nan ยท pandas

print(pdf["blood_group"].iloc[1])
print(pdf.index[:3].tolist())
nan
[0, 1, 2]

nan ยท PySpark

print(df.collect()[1]["blood_group"])
print("index? ", hasattr(df, "index"))
None
index?  False

timing ยท pandas

t = time.time()
r = pd.read_csv(PATH).groupby("city")\
      ["treatment_cost"].sum()
print("pandas :", f"{(time.time()-t)*1000:.0f} ms")
pandas : 18 ms

timing ยท PySpark

t = time.time()
r = (spark.read.csv(PATH, header=True,
      inferSchema=True).groupBy("city")
      .agg(F.sum("treatment_cost")).collect())
print("spark  :", f"{(time.time()-t)*1000:.0f} ms")
print("start-up:", f"{START_MS:,.0f} ms")
spark  : 579 ms
start-up: 7,313 ms

parts ยท pandas

# pandas: one machine, one block of RAM
print("pandas: 1 piece, no shuffle")
pandas: 1 piece, no shuffle

parts ยท PySpark

print("partitions:",
      df.rdd.getNumPartitions())
print("after repartition(3):",
      df.repartition(3).rdd.getNumPartitions())
plan = (df.groupBy("city").count()
        ._jdf.queryExecution()
        .executedPlan().toString())
print("shuffle (Exchange):", "Exchange" in plan)
partitions: 1
after repartition(3): 3
shuffle (Exchange): True

plan ยท pandas

# pandas has no optimizer: it runs
# each line exactly as you wrote it
print("pandas: no plan, no explain()")
pandas: no plan, no explain()

plan ยท PySpark

small = spark.createDataFrame(
  [("Cardiology", 3)], ["department", "floor"])
p = (df.join(small, "department")
       ._jdf.queryExecution()
       .executedPlan().toString())
print("BroadcastHashJoin:", "BroadcastHashJoin" in p)
BroadcastHashJoin: True

The full script

pandas_vs_pyspark_demo.py

# pandas_vs_pyspark_demo.py : setup + every pair below, in the order they ran
# (the pandas side re-reads the CSV before each pair so inplace changes do not leak)
import pandas as pd
PATH = "healthcare_dataset_1000.csv"          # CareWell, 1,000 rows
pdf = pd.read_csv(PATH)
import os, sys, time
os.environ["PYSPARK_PYTHON"] = sys.executable
from pyspark.sql import SparkSession, functions as F
t = time.time()
spark = (SparkSession.builder.master("local[3]").appName("pandas-to-pyspark")
         .config("spark.driver.memory", "2g").config("spark.ui.port", "4074")
         .config("spark.ui.showConsoleProgress", "false").getOrCreate())
spark.sparkContext.setLogLevel("ERROR")
START_MS = (time.time() - t) * 1000
df = spark.read.csv(PATH, header=True, inferSchema=True)

# ---- head : pandas
pdf = pd.read_csv(PATH)
print(pdf[["patient_id", "age", "city"]]
      .head(3))
# ---- head : PySpark
(df.select("patient_id", "age", "city")
   .show(3))

# ---- newcol : pandas
pdf = pd.read_csv(PATH)
pdf["cost_lakh"] = (pdf["treatment_cost"]
                    / 1e5).round(2)
print(pdf["cost_lakh"].head(3).tolist())
# ---- newcol : PySpark
df2 = df.withColumn("cost_lakh",
    F.round(F.col("treatment_cost") / 1e5, 2))
print([r[0] for r in
       df2.select("cost_lakh").take(3)])

# ---- filter : pandas
pdf = pd.read_csv(PATH)
old = pdf[pdf["age"] >= 60]
print("rows:", len(old))
# ---- filter : PySpark
old = df.filter(F.col("age") >= 60)
print("rows:", old.count())

# ---- group : pandas
pdf = pd.read_csv(PATH)
g = (pdf.groupby("department")
       ["treatment_cost"].mean()
       .round(0).sort_values(ascending=False)
       .head(3))
print(g)
# ---- group : PySpark
(df.groupBy("department")
   .agg(F.round(F.avg("treatment_cost"), 0)
        .alias("avg"))
   .orderBy(F.desc("avg"))
   .show(3))

# ---- join : pandas
pdf = pd.read_csv(PATH)
floors = pd.DataFrame({
  "department": ["Cardiology", "Oncology"],
  "floor": [3, 5]})
j = pdf.merge(floors, on="department")
print(len(j), j["floor"].unique().tolist())
# ---- join : PySpark
floors = spark.createDataFrame(
  [("Cardiology", 3), ("Oncology", 5)],
  ["department", "floor"])
j = df.join(floors, on="department")
print(j.count(), sorted(r[0] for r in
      j.select("floor").distinct().collect()))

# ---- fillna : pandas
pdf = pd.read_csv(PATH)
print("missing:", pdf["blood_group"]
      .isna().sum())
f = pdf.fillna({"blood_group": "Unknown"})
print("Unknown:", (f["blood_group"]
      == "Unknown").sum())
# ---- fillna : PySpark
print("missing:", df.filter(
  F.col("blood_group").isNull()).count())
f = df.fillna({"blood_group": "Unknown"})
print("Unknown:", f.filter(
  F.col("blood_group") == "Unknown").count())

# ---- sort : pandas
pdf = pd.read_csv(PATH)
print(pdf.sort_values("treatment_cost",
      ascending=False)
      [["patient_id", "treatment_cost"]]
      .head(3).to_string(index=False))
# ---- sort : PySpark
(df.orderBy(F.desc("treatment_cost"))
   .select("patient_id", "treatment_cost")
   .show(3))

# ---- write : pandas
pdf = pd.read_csv(PATH)
pdf.to_csv("out_pandas.csv", index=False)
print([f for f in os.listdir(".")
       if f.startswith("out_pandas")])
# ---- write : PySpark
df.repartition(3).write.mode("overwrite").csv(
    "out_spark", header=True)
for f in sorted(os.listdir("out_spark")):
    if not f.startswith("."):
        print(f[:24] + ("โ€ฆ" if len(f) > 24 else ""))

# ---- lazy : pandas
pdf = pd.read_csv(PATH)
t = time.time()
old = pdf[pdf["age"] >= 60]       # runs NOW
print(type(old).__name__, len(old), "rows")
print(f"{(time.time()-t)*1000:.1f} ms")
# ---- lazy : PySpark
t = time.time()
old = df.filter(F.col("age") >= 60)  # plan
print(type(old).__name__, "no rows yet")
print(f"{(time.time()-t)*1000:.1f} ms")
t = time.time(); n = old.count()     # action
print(n, f"rows {(time.time()-t)*1000:.0f} ms")

# ---- mutable : pandas
pdf = pd.read_csv(PATH)
pdf.drop(columns=["name"], inplace=True)
print("columns:", pdf.shape[1])
# ---- mutable : PySpark
df3 = df.drop("name")             # NEW df
print("df :", len(df.columns), "columns")
print("df3:", len(df3.columns), "columns")

# ---- nan : pandas
pdf = pd.read_csv(PATH)
print(pdf["blood_group"].iloc[1])
print(pdf.index[:3].tolist())
# ---- nan : PySpark
print(df.collect()[1]["blood_group"])
print("index? ", hasattr(df, "index"))

# ---- timing : pandas
pdf = pd.read_csv(PATH)
t = time.time()
r = pd.read_csv(PATH).groupby("city")\
      ["treatment_cost"].sum()
print("pandas :", f"{(time.time()-t)*1000:.0f} ms")
# ---- timing : PySpark
t = time.time()
r = (spark.read.csv(PATH, header=True,
      inferSchema=True).groupBy("city")
      .agg(F.sum("treatment_cost")).collect())
print("spark  :", f"{(time.time()-t)*1000:.0f} ms")
print("start-up:", f"{START_MS:,.0f} ms")

# ---- parts : pandas
pdf = pd.read_csv(PATH)
# pandas: one machine, one block of RAM
print("pandas: 1 piece, no shuffle")
# ---- parts : PySpark
print("partitions:",
      df.rdd.getNumPartitions())
print("after repartition(3):",
      df.repartition(3).rdd.getNumPartitions())
plan = (df.groupBy("city").count()
        ._jdf.queryExecution()
        .executedPlan().toString())
print("shuffle (Exchange):", "Exchange" in plan)

# ---- plan : pandas
pdf = pd.read_csv(PATH)
# pandas has no optimizer: it runs
# each line exactly as you wrote it
print("pandas: no plan, no explain()")
# ---- plan : PySpark
small = spark.createDataFrame(
  [("Cardiology", 3)], ["department", "floor"])
p = (df.join(small, "department")
       ._jdf.queryExecution()
       .executedPlan().toString())
print("BroadcastHashJoin:", "BroadcastHashJoin" in p)

spark.stop()

The steps in words