Handling Null Values

Spark tutorial · PySpark.in

Handling Null Values in PySpark

Handling null (missing) values is a crucial part of data cleaning in PySpark. Apache Spark’s DataFrame API provides several functions to identify, replace, or remove null values to ensure data quality. This guide covers five key topics for null handling, with both beginner-friendly explanations and intermediate insights, along with code examples and practical tips.

1. Using isNull() and isNotNull()

Explanation (Basics): PySpark uses None (or NULL in Spark SQL) to represent missing data. The DataFrame Column class provides isNull() and isNotNull() methods to check for null values in a column. These return a Column of boolean values indicating which entries are null or not. Specifically, col.isNull() yields True for rows where the value is null and False otherwise, while col.isNotNull() does the opposite (True for non-null, False for null). Using these in DataFrame operations allows you to filter or flag missing data easily.

Intermediate Insights: isNull/isNotNull create boolean expressions that can be used in DataFrame transformations. For example, you might use df.filter(df["column"].isNotNull()) to keep only rows with non-missing values. These methods are preferred over direct Python None checks because Spark SQL handles null semantics differently (e.g., using == None may not behave as expected in filters). Under the hood, isNull() corresponds to SQL IS NULL (returning true for null inputs). You can also use these to create indicator columns marking nulls for analysis or debugging.

Syntax & Examples:

```

from pyspark.sql import SparkSession

from pyspark.sql.functions import col

spark = SparkSession.builder.getOrCreate()

# Sample DataFrame with a null value

data = [("James", None), ("Anna", 30), ("Julia", 25)]

df = spark.createDataFrame(data, ["Name", "Age"])

# Filter rows where Age is not null

non_null_df = df.filter(col("Age").isNotNull())

non_null_df.show()

```

In this example, the row with Name="James" (which had Age=None) is filtered out, as isNotNull() keeps only rows with valid age. You could similarly use df.filter(col("Age").isNull()) to do the opposite (select only rows with Age missing). Another use case is adding a boolean column: for instance, df.withColumn("Age_is_null", col("Age").isNull()) would append a column marking which rows have Age as null.

Practical Scenarios & Tips:

2. Using fillna(), dropna(), and fill

Explanation (Basics): When dealing with missing values, you typically have two strategies: fill them with a default/value or drop the rows containing them. PySpark provides DataFrame.fillna() (and its alias DataFrame.na.fill(), sometimes called just fill) to replace nulls with specified values, and DataFrame.dropna() (alias DataFrame.na.drop()) to remove rows with nulls. These methods help ensure your DataFrame has no nulls before further processing.

Intermediate Insights: The fillna and dropna functions are flexible. For filling, PySpark infers the type – e.g., replacing a string column’s nulls with "Unknown" or a numeric column’s nulls with 0. If you provide a value that doesn’t match the column type, that column is left unchanged. The dropna method can be fine-tuned: using df.dropna(how="all") will only drop rows that are completely empty (all columns null), and df.dropna(thresh=k) will drop rows with fewer than k non-null values, which is useful if you want to keep rows that have at least a certain amount of valid data. You can also limit dropping to specific columns via the subset parameter.

Syntax & Examples:

```

from pyspark.sql import Row

from pyspark.sql import SparkSession

spark = SparkSession.builder.getOrCreate()

# Sample DataFrame with multiple nulls and NaNs

df2 = spark.createDataFrame([

Row(age=10, score=80.0, name="Alice"),

Row(age=5, score=float("nan"), name="Bob"),

Row(age=None, score=None, name="Tom"),

Row(age=None, score=float("nan"), name=None)

])

df2.show()

# Fill null age with 0 and null name with "Unknown"

filled_df = df2.fillna({"age": 0, "name": "Unknown"})

filled_df.show()

# Drop any row that has a null or NaN in any column

dropped_df = df2.dropna()

dropped_df.show()

```

Here:

You can adjust dropna() behavior: for example, df2.na.drop(how="all") would drop only rows where all columns are null/NaN (in our sample, none of the rows have all values null, so all except possibly the one with name None would remain), and df2.na.drop(subset=["name"]) would drop rows where the name is null, regardless of other columns.

Practical Scenarios & Tips:

3. Replacing Values in DataFrames

Explanation (Basics): Beyond null-specific handling, PySpark DataFrames have a more general method DataFrame.replace() (also accessible via df.na.replace()) to substitute one set of values with another. This is useful for standardizing or cleaning data – for example, replacing placeholder strings like "N/A" with actual nulls, or converting codes to meaningful values. The replace function can operate on any values (not just nulls) of type string, number, or boolean. You can replace a single value, multiple values, or even use a dictionary for mappings.

Insights: df.replace(old_value, new_value, subset=None) returns a new DataFrame where all occurrences of old_value are replaced with new_value in the specified columns (or all columns if no subset provided). If you pass a list of values and a list of replacements, it will perform the mapping pairwise. You can also supply a dict to map multiple old values to new values in one call. Notably, replace can handle None as either the value to replace or the replacement value. For instance, you might replace a special marker (like "unknown") with actual null (None), or vice versa. Keep in mind that type matters: the replacement value should be the same type as the values being replaced (Spark will ignore columns where types don’t match the replacement).

Syntax & Examples:

```

from pyspark.sql import SparkSession

spark = SparkSession.builder.getOrCreate()

# Using replace to substitute values

df3 = spark.createDataFrame([

(10, 80, "Alice"),

(5, None, "Bob"),

(None, 10, "Tom"),

(None, None, None)

], schema=["age", "height", "name"])

# 1) Replace all occurrences of 10 with 20 across the DataFrame

df3.replace(10, 20).show()

# 2) Replace 'Alice' with None (null) in all columns

df3.replace("Alice", None).show()

# 3) Replace multiple names in the 'name' column

df3.replace(["Alice", "Bob"], ["A", "B"], subset="name").show()

```

Explanation of the above code:

Practical Scenarios & Tips:

4. Removing Null Rows

Explanation (Basics): Removing rows that contain null values is a common operation when those rows are incomplete or not useful for analysis. We have already introduced DataFrame.dropna() in section 2, which is the primary method to drop null-containing rows. Additionally, you can remove nulls by filtering using isNotNull() on required columns. The goal is to exclude any records that don't meet your completeness criteria.

Insights: Deciding to remove rows with nulls depends on context. You may choose to drop only if a certain column (or set of columns) is null, or if too many columns are null. PySpark’s dropna() supports these choices via its parameters:

Using isNotNull() filters gives you manual control for complex conditions (e.g., drop if colA is null and colB is null, etc.). Under the hood, filtering nulls with isNotNull or using dropna(subset=...) are similar; dropna is just a convenient wrapper.

Syntax & Examples:

Dropping nulls using dropna:

```

from pyspark.sql import SparkSession

spark = SparkSession.builder.getOrCreate()

# Using replace to substitute values

df3 = spark.createDataFrame([

(10, 80, "Alice"),

(5, None, "Bob"),

(None, 10, "Tom"),

(None, None, None)

], schema=["age", "height", "name"])

# Drop rows with any nulls in any column

clean_df = df3.na.drop() # same as df3.dropna()

clean_df.show()

# Drop rows where both age and name are null (subset with how='any')

subset_drop_df = df3.dropna(subset=["age", "name"])

subset_drop_df.show()

```

In the first call, df3.na.drop() will remove any row that has a null in any column. In our sample df3 (from section 3), that would drop every row except those where all columns were filled (likely only the row with Alice, if any). The second call dropna(subset=["age","name"]) drops rows where either age or name is null. This focuses the null-check on those two columns only – for example, a row with age=None or name=None will be dropped, but a row with score null would not be dropped if age and name are present.

Dropping nulls using filter:

```

from pyspark.sql import SparkSession

spark = SparkSession.builder.getOrCreate()

# Using replace to substitute values

df3 = spark.createDataFrame([

(10, 80, "Alice"),

(5, None, "Bob"),

(None, 10, "Tom"),

(None, None, None)

], schema=["age", "height", "name"])

# Using filter to drop rows with null in specific column(s)

no_null_name_df = df3.filter(col("name").isNotNull())

no_null_name_df.show()

no_null_multi_df = df3.filter( (col("age").isNotNull()) & (col("height").isNotNull()) )

no_null_multi_df.show()

```

Here, no_null_name_df retains only rows where the name is not null (dropping any row with name = null) – this is equivalent to dropna(subset=["name"]). The no_null_multi_df example shows how to drop rows that have null in either of two columns by combining conditions: we keep rows where both age and height are non-null. Using filters with multiple conditions gives fine-grained control (you could also drop where one column is null but another is not, etc., by adjusting the boolean logic).

Practical Scenarios:

5. Handling Nulls in Join Conditions

Explanation (Basics): Joining DataFrames in Spark while handling nulls can be tricky. In SQL-style semantics (which Spark follows), a join condition like df1.key == df2.key will not match rows where key is null on either side – because null is not considered equal to null by default. This means if both DataFrames have records with a null join key, those records won't join to each other in an inner join. They would be dropped from an inner join and appear with nulls only on one side in an outer join. To properly handle nulls in join conditions, you have a few approaches:

Insights: Spark provides a null-safe equals operator, accessible in DataFrame API via Column.eqNullSafe() method. This corresponds to the SQL operator <=>. Using df1.col.eqNullSafe(df2.col) in the join condition will treat NULL == NULL as true. For example, df1.join(df2, df1["id"].eqNullSafe(df2["id"])) will match rows even when id is null in both DataFrames – something that a normal == join would not do. The difference can be seen in join results: an inner join on a null key with the normal == yields 0 matches, whereas using eqNullSafe yields a match for the pair of null keys.

Another approach is using coalesce to replace nulls with a default before comparison (e.g., coalesce(df1.key, lit("##")) == coalesce(df2.key, lit("##")) treating “##” as a placeholder). This can achieve a similar effect, but you must choose a placeholder that doesn’t occur in real data, and it adds a bit of complexity. Generally, eqNullSafe is cleaner and explicitly built for this scenario.

Syntax & Example:

```

from pyspark.sql import SparkSession

spark = SparkSession.builder.getOrCreate()

# Prepare two DataFrames to join

data1 = [(1, "Alice"), (2, None)]

data2 = [(1, "Engineer"), (2, None)]

dfA = spark.createDataFrame(data1, ["id", "value"])

dfB = spark.createDataFrame(data2, ["id", "value"])

# Inner join on id using standard equality (null won't match null)

inner_join_std = dfA.join(dfB, dfA["id"] == dfB["id"], "inner")

print("Inner join rows (standard):", inner_join_std.count())

# Inner join on id using null-safe equality

inner_join_nullsafe = dfA.join(dfB, dfA["id"].eqNullSafe(dfB["id"]), "inner")

print("Inner join rows (null-safe):", inner_join_nullsafe.count())

inner_join_nullsafe.show()

```

In this example, dfA and dfB each have an id 1 and an id 2 (with the id 2 being null in both). Using the regular join condition (== on id), the row with id null does not join – so the inner join count will be 1 (only the id=1 matched). Using the null-safe equality with eqNullSafe, the null id in dfA is considered equal to the null id in dfB, so that pair joins, resulting in 2 matched rows. The output of inner_join_nullsafe.show() would include the null id row joined, something you wouldn’t get with a normal join condition.

More Spark tutorials

All tutorials · Try the free PySpark compiler · Practice challenges