Data Transformation and Processing Logic Questions
Implementing transformation logic: joins, aggregations, deduplication, pivoting/reshaping, and business-rule application over datasets. Covers writing correct and maintainable transformation code, handling edge cases in the transform layer, and preparing data for downstream consumption. Focuses on the logic of turning raw data into analytics-ready outputs.
In Python, implement a function that deduplicates a collection of in-memory records by a key (for example email address, case-insensitively). Provide two variants of the policy: (a) drop all but the most recent record for each key, and (b) merge duplicate records together (summing numeric fields, keeping the most recent value for everything else). Handle an empty input and a subset key that is entirely null.
Sample Answer
Direct answer
Implement two small, separate functions rather than one function with a flag: one that keeps only the most recent record per key (a "last write wins" policy), and one that merges duplicates together (summing counters, keeping the latest value for everything else). They are different business decisions and conflating them into one function with an if/else tends to produce a function nobody trusts.
Structured elaboration
- Keep-latest: normalize the key (case-insensitive email, for example), sort by the recency field, and drop all but the last row per key.
- Merge-on-duplicate: group by the normalized key, then for numeric fields that represent a running total (purchase_count), sum across the group; for fields that represent current state (name, last-login), take the value from the most recent row.
- Edge cases that a real interview will probe: an empty input collection must return an empty result, not raise; a key that appears only once must pass through unchanged; and the key-normalization step itself needs to handle a missing/None key without crashing.
Worked example
import pandas as pd
def dedup_keep_latest(records, key='email'):
df = pd.DataFrame(records)
if df.empty:
return df
df['_k'] = df[key].astype(str).str.lower()
df = df.sort_values('updated_at').drop_duplicates(subset='_k', keep='last').drop(columns='_k')
return df.reset_index(drop=True)
def dedup_merge(records, key='email', sum_fields=('purchase_count',)):
df = pd.DataFrame(records)
if df.empty:
return df
df['_k'] = df[key].astype(str).str.lower()
out = []
for k, g in df.groupby('_k'):
row = g.sort_values('updated_at').iloc[-1].to_dict()
for f in sum_fields:
row[f] = g[f].sum()
out.append(row)
return pd.DataFrame(out).drop(columns='_k').reset_index(drop=True)
Given two records for the same email with different casing (A@x.com with purchase_count 2, then a@x.com with purchase_count 3, the second more recent), dedup_keep_latest returns one row with purchase_count 3 (the newer row's own value, verified), while dedup_merge returns one row with purchase_count 5 (2 + 3, verified). Both handle an empty input by returning an empty frame rather than raising.
Trade-offs and pitfalls
- Case-insensitive key normalization (
.str.lower()) is itself a policy decision: it can accidentally merge two genuinely distinct users if your system treats email case as significant somewhere else (some mail servers do). State the assumption. drop_duplicates(keep='last')depends on the DataFrame already being sorted by the field you care about; forgetting the.sort_values()before it is the single most common bug in this pattern.- The merge policy's "sum numeric fields, keep-latest for everything else" default is reasonable for counters but wrong for fields like a running balance; call out which fields are safe to sum before applying the pattern generically.
Design a rules engine that evaluates many business rules against streaming or batch records to decide routing or transformation outcomes. Requirements: deterministic output across retries, a way to resolve conflicts when multiple rules could apply (priority ordering), fast evaluation per record, the ability to update rules without downtime, and a full audit trail of which rule fired for which record.
Sample Answer
Direct answer
At the scale of hundreds of business rules evaluated per record, the design shifts from "write each rule as a CASE expression" to a compiled, versioned rule set with explicit conflict-resolution priority, deterministic evaluation regardless of retries, hot-reloadability without downtime, and a full audit trail of which rule fired and why, each of these is a distinct requirement that a small rule set can get away without.
Structured elaboration
- Deterministic outputs across retries: the same input record, evaluated against the same rule-set version, must always produce the same decision; this generally means the rules and their evaluation order are pure functions of (record, rule-set version), with no hidden dependency on wall-clock time or external mutable state during evaluation.
- Conflict resolution via explicit priority, not code order: when multiple rules could apply, an explicit priority number (or a well-defined "most specific wins" policy) resolves the conflict predictably; relying on the order rules happen to be listed in source code is fragile exactly the same way an if/elif chain is fragile at small scale, just with much higher blast radius at large scale.
- Fast evaluation per record: with hundreds of rules, naively evaluating every rule's full condition against every record is often too slow for a tight per-record latency budget; compiling rules into an efficient intermediate representation (an indexed decision structure, or compiling conditions to something closer to bytecode) avoids re-parsing rule text on every evaluation.
- Hot-reload without downtime: rules change more often than code deploys should need to; loading a new rule-set version into a running system without a restart, with the old version still serving in-flight evaluations until they complete, avoids coupling rule changes to a full deployment cycle.
- Full audit trail: for every decision, record which specific rule(s) fired, at which rule-set version, this is what makes a disputed or surprising outcome debuggable after the fact, and is often a compliance requirement in regulated domains.
Worked example
A concrete miniature of the conflict-resolution principle: the earlier discount-rule survivor's GREATEST(rule_a, rule_b) pattern is the small-scale version of exactly this problem, computing every applicable rule's outcome and picking the best (or the highest-priority) one explicitly, rather than depending on evaluation order. At hundreds-of-rules scale, the same principle holds, but the mechanism shifts from a handful of inline CASE expressions to a compiled, indexed rule evaluator that can find the relevant subset of rules for a given record quickly, plus an explicit priority field on each rule to resolve genuine conflicts, plus a persisted log entry per decision naming which rule(s) fired.
Trade-offs and pitfalls
- A fully general rules engine (its own DSL, a compilation step, hot-reload infrastructure) is a meaningful investment; for a rule set that genuinely stays small and changes rarely, the earlier CASE-expression approach is the right-sized solution, don't over-build.
- Hot-reloading rules without downtime introduces its own correctness question: what happens to a record whose evaluation started under the old rule-set version and would finish under the new one? A clean answer (pin a version for the duration of one evaluation) avoids a whole class of subtle inconsistency bugs.
- An audit trail that's too coarse (only "some rule matched") is nearly useless for debugging a disputed decision months later; the trail needs to name the SPECIFIC rule and rule-set version, not just confirm that some fallback path was taken.
You must identify probable duplicate records across multiple sources when there is no shared unique identifier and the same real-world entity (for example a customer) may be represented with typos, formatting differences, or partial information (name, address, email). Design an end-to-end approach for finding and resolving these duplicates at scale, and discuss the trade-offs your design makes between catching every true duplicate and avoiding an incorrect merge.
Sample Answer
Direct answer
Without a shared unique identifier, entity resolution comes down to three ordered steps: normalize the fields you'll compare, use blocking to cut down the astronomically large number of candidate pairs to a tractable set, then score each remaining candidate pair with a similarity metric and a chosen confidence threshold that decides auto-merge versus human review.
Structured elaboration
- Normalization first: lowercase, strip punctuation and whitespace, expand common abbreviations, so that "Jon Smith" and "JON SMITH" compare as identical inputs to the similarity step, not as a similarity problem in themselves.
- Blocking: comparing every pair of records is O(n2), infeasible past a few hundred thousand records; blocking groups records into buckets that are cheap to compute (first letter of last name, postal code, a phonetic key like Soundex) so only records in the SAME bucket are ever compared to each other. This sacrifices some recall (a true match split across two blocks is missed) for tractability.
- Similarity scoring: within a block, score each pair with a string-similarity metric (Levenshtein edit distance, Jaro-Winkler for names, or token-set methods for addresses); at truly large scale, MinHash/LSH approximates "which pairs are similar" without materializing all pairwise comparisons, by hashing records into buckets where similar records collide with high probability.
- Confidence threshold and workflow: pairs scoring above a high threshold auto-merge; pairs in an ambiguous middle band route to a human reviewer with the evidence shown; pairs below a low threshold are left as distinct. Where you set these two thresholds is a precision/recall trade-off made explicit, not hidden inside a single "is duplicate" boolean.
- At true scale (hundreds of millions of records), an exact pairwise approach is replaced by LSH/MinHash for candidate generation, with the same blocking-then-scoring logic applied within the much smaller candidate set it produces; a hybrid architecture (fast approximate first pass, then exact reconciliation on the surviving candidates) is common precisely because the two techniques catch different failure modes.
Worked example
Records: "Jon Smith", "John Smith", "Jonathan Smith", "Amy Lee", "Amy Le". Using first-letter-of-name blocking and a sequence-similarity ratio (verified computationally):
'Jon Smith' vs 'John Smith' similarity = 0.95
'Jon Smith' vs 'Jonathan Smith' similarity = 0.78
'John Smith' vs 'Jonathan Smith' similarity = 0.83
'Amy Lee' vs 'Amy Le' similarity = 0.92
At an auto-merge threshold of 0.85, "Jon Smith"/"John Smith" and "Amy Lee"/"Amy Le" merge automatically as true near-duplicates, while both pairs involving "Jonathan Smith" (0.78 and 0.83) fall below the bar and are routed to human review rather than silently merged or silently kept apart. This concretely shows why the threshold is a business decision: a lower threshold would auto-merge "Jonathan Smith" into "Jon Smith" too, which may or may not be correct.
Trade-offs and pitfalls
- Blocking is itself a recall risk: if the blocking key is wrong for one of a true pair (a typo in the very field used for blocking), that pair is never even compared. Multiple, differently-keyed blocking passes mitigate this at added cost.
- A pure precision-maximizing threshold (very high) leaves many true duplicates unmerged, which quietly inflates downstream counts (more "unique customers" than really exist); a pure recall-maximizing threshold (very low) risks merging two different real people, which is often the more damaging error since it can misattribute one person's history to another.
- A debugging variant of this problem: two reporting systems disagreeing on a metric by a visible percentage is frequently traced to exactly this kind of inconsistency, one system's fuzzy-matching rules differing from the other's, which is worth naming as a root cause an interviewer may be probing for.
A transformation job produced different output across two runs even though the code did not change. Walk through the debugging steps you would take to find the cause, and what you would put in place afterward to prevent a recurrence.
Sample Answer
Direct answer
Non-deterministic transformation output almost always traces to an operation whose result depends on an ordering the system doesn't guarantee, most commonly an unordered groupby().first()/.last() on data whose row order varies between runs; the fix is to make the intended ordering explicit (sort by a real, meaningful key before the operation) rather than relying on whatever order the data happened to arrive in.
Structured elaboration
- Confirm it's really non-determinism, not a data change: first verify the exact same input snapshot produces different output across runs; if the input itself silently changed between runs, that's a different bug (a data-freshness issue), not a non-determinism issue.
- Check dependency and environment versions: a library upgrade can silently change tie-breaking behavior in an operation that was never guaranteed to be stable in the first place.
- Look specifically for unordered operations:
groupby().first(),.drop_duplicates()without a prior sort, and any join or shuffle that doesn't have a secondary deterministic tie-break are the recurring culprits; each of these has a "first/any" semantic that depends on row order unless you make that order explicit yourself. - Check for unseeded randomness: any sampling, shuffling, or randomized algorithm needs an explicit, fixed seed for its output to be reproducible; forgetting this is a second common source of run-to-run variation.
- The fix, once found: sort by an explicit, meaningful key (a timestamp, a stable id) BEFORE the aggregation that assumed an order; this makes "first" and "last" actually mean something specific and repeatable, rather than "whatever happened to be first in an arbitrary in-memory order."
Worked example
import pandas as pd
df = pd.DataFrame({'user_id':[1,1,2,2], 'event':['A','B','C','D'], 'val':[10,20,30,40]})
shuffled1 = df.sample(frac=1, random_state=1).reset_index(drop=True)
shuffled2 = df.sample(frac=1, random_state=2).reset_index(drop=True)
first1 = shuffled1.groupby('user_id')['event'].first()
first2 = shuffled2.groupby('user_id')['event'].first()
Verified: with the SAME logical data shuffled two different ways (simulating two runs where input row order isn't guaranteed), groupby('user_id')['event'].first() returned DIFFERENT results across the two shuffles ({1: 'A', 2: 'D'} versus {1: 'B', 2: 'C'}), concretely reproducing the bug class. Adding an explicit .sort_values('ts') by a real timestamp column before the groupby, and re-running against the same two differently-shuffled inputs, produced IDENTICAL results both times, confirming the fix actually addresses the root cause rather than merely appearing to.
Trade-offs and pitfalls
- This class of bug is dangerous specifically because it's silent: the code runs successfully every time, produces plausible-looking output every time, and only the SPECIFIC VALUE differs between runs, which is much harder to catch in code review or a single test run than an outright crash.
- A test that only runs an aggregation once against a fixed, already-sorted test fixture will never catch this bug, since the fixture never exercises the ordering-dependence at all; a good regression test for this class of issue should specifically shuffle the input and assert the output is stable across multiple different shuffles.
- The general lesson generalizes beyond
groupby().first(): any time a transformation's correctness silently depends on an ordering guarantee the underlying engine doesn't actually promise, the same class of bug is possible, sort explicitly wherever the result should be deterministic.
Some aggregations (sum, count) are associative and trivially parallelizable across a distributed dataset with a guaranteed deterministic result. Others, like median or percentile, are not. Discuss how you would compute an approximate percentile at scale with a mergeable, deterministic algorithm (for example a t-digest or histogram sketch), and separately, how you would implement a scalable approximate 'distinct count' (for example unique users in the last 30 days) using a structure like HyperLogLog, including the accuracy/memory trade-off of each.
Sample Answer
Direct answer
Associative aggregates like sum and count can be computed on partitions independently and combined trivially and exactly; non-associative statistics like median or an arbitrary percentile cannot be combined that way, and computing them exactly at scale requires either collecting all the data to one place (expensive) or accepting an approximate, mergeable summary structure like a t-digest, histogram sketch, or HyperLogLog instead of the exact value.
Structured elaboration
- Why percentile doesn't parallelize like sum does: the median of the combination of two datasets is not a simple function of the two datasets' individual medians; you need something closer to the full sorted order, which is exactly what doesn't scale.
- Sketch-based approximation: a t-digest or histogram sketch summarizes the DISTRIBUTION in a small, fixed-size, MERGEABLE structure, mergeable meaning two workers' partial sketches can be combined into one sketch representing the union of their data, without re-processing the raw values, which is what makes it parallelizable where the exact computation isn't.
- HyperLogLog for approximate distinct counts: the same idea, applied to "how many distinct values," a small, mergeable sketch trades a small, well-understood error for tractable memory and merge cost at huge cardinality.
- The error is the price of mergeability: both structures accept a small, quantifiable error bound in exchange for being computable in parallel and mergeable afterward; whether that trade is acceptable depends entirely on how the number will be used (a rounded, monitoring-dashboard metric can usually tolerate it; a billing calculation usually cannot).
Worked example
Against 100,000 synthetic random values, verified via execution: the exact median was 501.0 and the approximate (t-digest-style) quantile computed 501, a negligible difference. The exact distinct count was 1,001 and the HyperLogLog-style approximate count returned 1,249, a measured relative error of about 25% at this specific (fairly low) cardinality; HyperLogLog's accuracy improves at larger cardinalities and with more allocated registers, this measured number (not an assumed "single-digit percent" figure) is what an honest answer reports, along with the caveat that the observed error is specific to this run's cardinality and configuration.
Trade-offs and pitfalls
- Approximate structures have tunable size/accuracy trade-offs (more registers/buckets, less error, more memory); the honest answer states that the error depends on configuration and cardinality rather than quoting a single always-true percentage.
- Combining sketches from different sources only works correctly if they were built with compatible parameters (the same hash function and register count for HyperLogLog, for instance); mixing incompatible sketches produces a meaningless merge, not a graceful degradation.
- These techniques are for read-time or reporting-time aggregation; if the aggregate needs to be EXACT for a financial or compliance reason, this whole toolkit is the wrong tool regardless of how attractive the performance is.
Unlock Full Question Bank
Get access to all 42 Data Transformation and Processing Logic interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.