Spark User Defined Functions

A user defined function, or UDF, lets you run your own Python logic on DataFrame columns. Built-in functions cover most needs. A UDF fills the gaps when a business rule has no ready-made function, such as a custom grading formula or a special text clean-up routine.

The Layman View

Spark is a large kitchen with a standard set of tools. A UDF is a hand-made tool that you bring from home. The kitchen accepts it, but every ingredient must travel to your tool and back. That trip costs time.

Creating a Simple UDF

Write a normal Python function, wrap it with udf, and declare the return type.

from pyspark.sql import functions as F
from pyspark.sql.types import StringType

def grade(score):
    if score is None:
        return "Unknown"
    if score >= 80:
        return "A"
    if score >= 60:
        return "B"
    return "C"

grade_udf = F.udf(grade, StringType())

df = spark.createDataFrame([("Asha", 91), ("Ben", 67), ("Chitra", 45)], ["name", "score"])
df.withColumn("grade", grade_udf("score")).show()
name    score  grade
Asha     91     A
Ben      67     B
Chitra   45     C

Decorator Style

@F.udf(returnType=StringType())
def initials(name):
    return "".join(part[0].upper() for part in name.split())

df.withColumn("initials", initials("name")).show()

Using a UDF in SQL

Register the function by name and call it inside any SQL query.

spark.udf.register("grade_sql", grade, StringType())
df.createOrReplaceTempView("students")
spark.sql("SELECT name, grade_sql(score) AS grade FROM students").show()

Why Plain UDFs Run Slowly

Spark stores data in the JVM. A plain Python UDF forces Spark to serialize each row, send it to a Python worker, run the function, and send the answer back.

JVM executor                    Python worker
-------------                   -------------
row 1 --serialize-->             run grade(row 1)
row 2 --serialize-->             run grade(row 2)
row 3 --serialize-->             run grade(row 3)
<--serialize-- results ---------

The optimizer also treats a UDF as a black box. It cannot push filters through it or reorder the work around it.

Pandas UDFs: The Faster Option

A pandas UDF processes a whole batch of values at once through Apache Arrow. Batches remove most of the per-row cost. Use them for numeric work and for logic that already exists in pandas.

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

@pandas_udf("double")
def to_fahrenheit(celsius: pd.Series) -> pd.Series:
    return celsius * 9 / 5 + 32

temps = spark.createDataFrame([(0.0,), (25.0,), (100.0,)], ["celsius"])
temps.withColumn("fahrenheit", to_fahrenheit("celsius")).show()
Plain UDF   : 1 row  per call  --> many small calls
Pandas UDF  : 10,000 rows per call --> few large calls

Handling Null Values

Spark passes null as None to your function. A function that calls a method on None raises an error and stops the job. Test for None at the top of every UDF.

Replace a UDF with Built-ins When Possible

# UDF version
upper_udf = F.udf(lambda s: s.upper() if s else None, StringType())
df.withColumn("n", upper_udf("name"))

# Built-in version: faster and optimizable
df.withColumn("n", F.upper("name"))

The when and otherwise functions replace the grade function above. Try them before you write a UDF.

df.withColumn("grade",
    F.when(F.col("score") >= 80, "A")
     .when(F.col("score") >= 60, "B")
     .otherwise("C")).show()

Decision Guide

  • Start with a built-in function. It runs fastest.
  • Use a pandas UDF when the logic needs Python libraries or vector math.
  • Use a plain UDF for small data or rare, complex rules.
  • Declare an exact return type. A wrong type produces null values without a warning.
  • Keep UDFs free of side effects such as writing files. Spark may run a function more than once.