Copyright (c) 2026 MindMesh Academy. All rights reserved. This content is proprietary and may not be reproduced or distributed without permission.

3.3.4. Handling Data Quality Issues

💡 First Principle: Real-world data contains duplicates, nulls, and late arrivals. Robust data engineering handles these systematically rather than hoping they don't exist.

Handling Duplicates

# PySpark: Remove duplicates
df_deduped = df.dropDuplicates(["OrderID"])  # subset list; with no list every column must match, and distinct() takes no subset

# Keep most recent duplicate
from pyspark.sql.window import Window
window = Window.partitionBy("OrderID").orderBy(col("UpdatedAt").desc())
df_latest = df.withColumn("row_num", row_number().over(window)) \
              .filter(col("row_num") == 1) \
              .drop("row_num")

⚠️ Silent duplicates from a broken upsert: if a MERGE/upsert match condition stops matching (for example, the source system changes its key format), every incoming row takes the INSERT branch. New duplicate rows pile up while the original rows keep their old values. When duplicates appear with no code change, check the MERGE ON condition against the current source data.

Handling Missing Data

# Fill nulls with default values (df.na.fill() and df.fillna() are the same call;
# F.coalesce(col, lit(default)) does it per column)
df_filled = df.na.fill({
    "Quantity": 0,
    "Discount": 0.0,
    "ShipDate": "1900-01-01"
})

# Drop rows with nulls in critical columns
df_complete = df.na.drop(subset=["CustomerID", "ProductID"])

Handling Late-Arriving Data

  • Challenge: Data arrives after the batch window closes
  • Solutions:
    • Watermarks: Track latest processed timestamp, and re-read a lookback window (for example, last watermark minus 2 days). A strict > last max timestamp filter skips rows that arrive late with older timestamps; MERGE or dedupe the overlap
    • Reprocessing windows: Reprocess recent data periodically
    • Delta Lake merge: Update existing records with late data

Validation and lineage: Validate and standardize data types (cast, trim, consistent date formats) for every source, including "trusted" ones, and keep typed columns rather than casting everything to string; failing rows go to quarantine, not deletion. For audit lineage, stamp each row with the source name, pipeline run ID and load timestamp, passed in as pipeline parameters.

Validation Patterns (PySpark)
NeedPatternWhy not the alternative
Default a null to a computed value (e.g., today)withColumn("OrderDate", coalesce(col("OrderDate"), current_date())) or when(col(...).isNull(), ...).otherwise(...)na.fill/fillna accept only literal values, not Column expressions like current_date()
Standardize mixed date formatscoalesce(to_date(col("d"), "yyyy-MM-dd"), to_date(col("d"), "MM/dd/yyyy"), to_date(col("d"), "MMM d, yyyy"))A single cast or pattern returns null for every other format
Flag bad values but keep the rowswithColumn("is_valid_email", col("email").rlike(pattern))filter() removes the rows instead of flagging them
Count nulls in a columndf.filter(col("c").isNull()).count()dropna/fillna change the data rather than measure it
Compare timestamps from different systemsConvert both to UTC first, e.g. to_utc_timestamp(col("ShipDate"), "America/New_York")Comparing UTC with local time shifts one side by hours and creates false "shipped before ordered" failures

Reject, don't drop (the dead-letter pattern, batch or streaming): send records that fail parsing or validation to a quarantine (reject, dead-letter) table or error folder with the failure reason, keep loading the good rows, and log the reject count, so the failures can be fixed and reprocessed later. Silently dropping rows hides data loss and removes the evidence needed for root-cause analysis and for reconciling with the source.

Data Quality Dimensions
DimensionQuestion it answersExample check
CompletenessAre all expected records and required values present?Row count vs. source; nulls in CustomerID
AccuracyAre the present values correct?Amount matches the source ledger
ValidityDo values conform to format or range rules?Email pattern; Quantity > 0
TimelinessDid the data arrive when expected?Load finished before the SLA
ConsistencyDo related datasets agree?Every FactSales customer exists in DimCustomer (load dimensions before facts, 3.1.2)

A dataset can be complete (no missing rows or nulls) and still inaccurate (wrong values).

See how it connects
Alvin Varughese
Written byAlvin Varughese
Founder•20 professional certifications