EasyMLOps

ETL Deduplication

MLOps

Easy

Problem

Deduplicate records using the values in key_columns as a composite key. Select one record per key according to strategy:

  • "first": select the first occurrence
  • "last": select the last occurrence
  • "most_complete": select the record with the fewest None values, breaking ties by first occurrence

Regardless of strategy, order the output by the first appearance of each unique key. Return the selected records as a list of dictionaries without changing the input records.

Theory

Duplicate records are one of the most common and persistent data quality issues in data pipelines. They arise from many sources:

At-Least-Once Delivery: Message queues (Kafka, RabbitMQ, SQS) typically guarantee at-least-once delivery, meaning the same message may be delivered multiple times if acknowledgments fail.

API Retries: When an API call times out, clients often retry. If the original request actually succeeded, you now have the same data submitted twice.

Merge Operations: Combining data from multiple sources (e.g., merging acquisitions, consolidating regional databases) often introduces overlapping records.

Backfill Operations: Re-processing historical data may load records that already exist in the destination.

User Actions: Users may accidentally click a submit button twice or refresh a page that re-sends a form.

If duplicates are not handled, they cause severe downstream problems:

Deduplication is the process of identifying and removing duplicate records, keeping only one representative record per unique entity.


Defining Uniqueness: Key Columns

To deduplicate, you must first define what makes two records "the same." This is done through key columns (also called deduplication keys or natural keys).

Two records are duplicates if they have identical values in ALL key columns.

Example with single key:

Example with composite key:

Choosing the right key columns requires domain knowledge:


Deduplication Strategies

When duplicates exist, you must choose which record to keep. Different strategies serve different use cases:

Strategy 1: "first"

Keep the first occurrence of each unique key (by position in the input).

Use case: When the first arrival is most reliable, such as:

Algorithm:

Strategy 2: "last"

Keep the last occurrence of each unique key (by position in the input).

Use case: When the most recent arrival is most current, such as:

Algorithm:

Strategy 3: "most_complete"

Keep the record with the fewest null values across ALL fields (not just key columns).

Use case: When different duplicates may have different missing fields, such as:

Algorithm:


Preserving Output Order

Regardless of which deduplication strategy is used, the output must preserve the first-appearance order of unique keys.

This means:

This requirement ensures deterministic, reproducible output and maintains intuitive ordering that reflects when each unique entity was first seen.


The Deduplication Algorithm

Step 1: Initialize Tracking Structures

Step 2: Group Records by Key

Step 3: Select One Record Per Key

Step 4: Build Output


Computing Null Counts

For the "most_complete" strategy, you need to count null values:

\text{null\_count}(record) = \sum_{field \in record} \mathbf{1}[\text{value is None}]

Count nulls across ALL fields in the record, not just key columns. This ensures you select the most informative record overall.

When multiple records have the same null count, the tie-breaker is position: keep the record that appeared first in the input.


A Detailed Worked Example

Input Records:

Key Columns: [user_id, product_id]

Grouping by Key:

First-appearance order: [("A", 1), ("B", 2)]

Strategy: "first"

Strategy: "last"

Strategy: "most_complete"

Note: Output order follows first-appearance of keys (A,1 before B,2), even though the selected records may differ.


Handling Composite Keys

When multiple columns form the key, the key is a tuple of values:

key = tuple(record[col] for col in key_columns)

Example:

Two records are duplicates if their key tuples are equal (element-wise comparison).

Important: None values in key columns are valid and participate in comparison. Two records both having user_id=None are considered duplicates if that is the only key column.


Edge Cases

No Duplicates: If every record has a unique key, all records are returned in original order.

All Duplicates: If all records share the same key, exactly one record is returned (determined by strategy).

Empty Input: Return an empty list.

Single Record: Return that record.

Nulls in Key Columns: Null values are compared using standard equality. Two nulls are considered equal.


Computational Complexity

Deduplication scales linearly with input size:

O(n \cdot k)

where n is the number of records and k is the number of key columns.

For "most_complete" strategy, add O(n \cdot c) where c is the total number of columns (for null counting).

Hash-based key lookup ensures constant-time duplicate detection.


Where Deduplication Shows Up

Data Warehousing: ETL pipelines deduplicate before loading to maintain data integrity in fact and dimension tables.

Event Processing: Stream processors deduplicate events to ensure exactly-once semantics.

Data Integration: When merging data from multiple sources, deduplication resolves overlapping records.

CRM Systems: Customer records from different touchpoints are deduplicated to create a unified customer view.

Log Aggregation: Duplicate log entries from retries or multiple servers are consolidated for accurate analysis.

Machine Learning: Training data is deduplicated to prevent models from overfitting to repeated examples.

Examples

Example 1

Input
records = [{"id": 1, "name": "Alice", "email": "alice@test.com"}, {"id": 2, "name": "Bob", "email": "bob@test.com"}], key_columns = ["id"], strategy = "first"
Output
[{"id": 1, "name": "Alice", "email": "alice@test.com"}, {"id": 2, "name": "Bob", "email": "bob@test.com"}]
Explanation
The two records have different keys, so both remain.

Example 2

Input
records = [{"id": 1, "name": "Alice", "email": "alice@test.com"}, {"id": 2, "name": "Bob", "email": "bob@test.com"}, {"id": 1, "name": "Alice Smith", "email": "alice.s@test.com"}], key_columns = ["id"], strategy = "first"
Output
[{"id": 1, "name": "Alice", "email": "alice@test.com"}, {"id": 2, "name": "Bob", "email": "bob@test.com"}]

Hints

  1. Use tuple(record[column] for column in key_columns) as the composite key.
  2. Store key order separately from grouped records, then select from each group.

Requirements

Constraints

Starter Code

def deduplicate(records: list, key_columns: list, strategy: str) -> list:
    """
    Returns a list of selected records.
    """
    # Write code here
    pass

Test Cases

CaseMatches
no duplicatespublic
keep firstpublic