# The Column Transformer

> Each column gets its function.

Canonical URL: <https://datadriven.io/problems/the_column_transformer>

Domain: Python · Difficulty: easy · Seniority: L3 · Asked in: HelloFresh

## Problem

An ETL step ships its per-column transforms as configuration: `transforms` maps a column name to the name of an operation, while `rows` is a list of same-keyed dicts. An operation is one of `'upper'` or `'lower'` to change a string's case, `'increment'` to add one to a number, or `'double'` to multiply a number by two. Apply each column's operation to its value in every row and return the updated rows without modifying the input.

## Example 1

Input:

```
rows = [{"age":30,"name":"alice"},{"age":25,"name":"bob"}], transforms = {"name":"upper"}
```

Output:

```
[{"age":30,"name":"ALICE"},{"age":25,"name":"BOB"}]
```

## Example 2

Input:

```
rows = [{"age":30,"name":"alice"}], transforms = {"age":"increment","name":"upper"}
```

Output:

```
[{"age":31,"name":"ALICE"}]
```

## Worked solution and explanation

### What you're really being asked

Strip the ETL costume and this is a config-driven dispatch problem: each column names an operation, so you look up what to do per column and run it. The real work is not the operations, it is rebuilding every row as a NEW dict. Walk the row's OWN `items()` so untransformed columns pass through and key order survives for free, then overwrite only the columns that name a transform. Get greedy and mutate the row in place and you silently rewrite the list the caller still holds; build the row off the `transforms` dict instead and you drop every pass-through column and scramble the order.

---

### Break down the requirements

#### Step 1: Rebuild each row as a fresh dict

Process rows one at a time and start each with an empty `new_row`. Walking the current row's `items()` (not the `transforms` dict) is what keeps every column and its original order, and it means you never touch the input.

#### Step 2: Decide per column whether to transform

For each column, ask `if col in transforms`. If it names no operation, copy the value straight across. This one check is what makes untransformed columns pass through unchanged.

#### Step 3: Dispatch on the operation name

When a column does name an operation, dispatch on the name with `if`/`elif`: `'upper'` and `'lower'` change a string's case, `'increment'` adds one, `'double'` multiplies by two. Because you iterate the row's columns, a transform naming a column the row lacks is simply never reached, so a missing column can never raise.

---

### The solution

**Copy each row, dispatch per column**

```python
def apply_transforms(rows: list, transforms: dict) -> list:
    result = []
    for row in rows:
        # Copy by walking the row's own items so order and pass-through columns are free.
        new_row = {}
        for col, value in row.items():
            if col in transforms:
                op = transforms[col]
                if op == "upper":
                    new_row[col] = value.upper()
                elif op == "lower":
                    new_row[col] = value.lower()
                elif op == "increment":
                    new_row[col] = value + 1
                elif op == "double":
                    new_row[col] = value * 2
                else:
                    new_row[col] = value
            else:
                new_row[col] = value
        result.append(new_row)
    return result
```

> **Time and Space Complexity**
>
> **Time:** O(n * c) where n is the number of rows and c is the columns per row. Every value is visited once and the `col in transforms` lookup is O(1).
> 
> Space: O(n * c) for the copied output rows.

> **Row-driven, not transform-driven**
>
> Whether you drive the copy off the ROW (`for col, value in row.items()`) or off the `transforms` dict. Iterating `transforms` is the tell that a candidate will drop untransformed columns and lose the input's column order. Also whether each `new_row` is a fresh dict so the caller's input is never mutated.

> **The shared dict bites twice**
>
> Editing `row` in place (`row[col] = ...`) rewrites the caller's data because the dict is shared. And building the result by looping `transforms` first, then trying `row[col]`, raises `KeyError` on any row missing that column and silently discards every column no transform names.

---

### How would this grow on the job?

## Common follow-up questions

- How would you let callers add new operations without editing this function? _(Tests dispatch-table design: replace the if/elif chain with a dict mapping operation names to functions so new operations are a one-line registration.)_
- What if a transform needs to read several columns of the same row? _(Tests API design: an operation like a ratio needs more than one column, so the transform would receive the whole row rather than a single value.)_
- How would you apply these transforms lazily over a very large dataset? _(Tests generator-based streaming: yield each transformed row instead of collecting the whole list, so 100,000 rows don't all sit in memory.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/the_column_transformer)
- [Python Interview Questions](https://datadriven.io/python-interview-questions)
- [Data Engineering Interview Prep Guide](https://datadriven.io/data-engineer-interview-prep)
- [Daily Challenge](https://datadriven.io/daily)

---

Source: DataDriven (https://datadriven.io). DataDriven is the data engineering interview community. Live code execution in SQL, Python, and Spark sandboxes. Every feature is open to every member.