Pandas explode: Change the Record Grain Without Inflating Totals
Expand nested records while preserving parent identity, separating empty and missing states, and reconciling totals at the correct grain.
Reviewed October 9, 2026. Examples target pandas 2.2.3 and use synthetic orders.
An order table contains one row per order and a list of purchased products. Expanding that list creates a convenient product table, but it also repeats every order-level field. Summing the repeated order total produces an inflated number. The expansion succeeded technically while changing the meaning of a row in a way the report ignored.
explode turns list-like elements into separate rows. The important question is not simply how to flatten a column, but what each resulting record represents. This guide builds an explicit parent-to-child contract, demonstrates empty-list behavior, and shows how paired columns differ from independent nested dimensions.
1. Name the input and output grain
Begin with identity. The input might contain one row per order, identified by order_id; the output might contain one row per product position within an order. Position matters when a product appears twice or the original sequence is significant. A product name by itself is rarely a reliable child identifier.
Classify every column before expansion. Parent attributes such as customer or order total will be repeated. Child attributes such as product and quantity belong to individual positions. A field that appears constant in a small sample may still have a different intended grain. The schema, not accidental equality, should decide where it belongs.
An exploded table is often useful for product counts, tag frequencies, and item-level features. It is less suitable for summing unchanged parent-level amounts without an explicit reconstruction step. Keeping separate parent and child tables makes this difference visible and reduces the temptation to use one table for incompatible questions.
2. Observe empty lists before filtering them away
An empty list produces a placeholder row with a missing exploded value. A scalar remains a single value. Consequently, output row count is not necessarily the sum of list lengths: empty parents remain represented unless explicitly excluded. This behavior is useful for preserving parent coverage, but it must be accounted for when counting children.
import pandas as pd
orders = pd.DataFrame({"order_id": ["A", "B", "C"],
"items": [["pen", "book"], [], ["bag"]],
"order_total": [30, 0, 40]})
orders["item_count"] = orders["items"].map(len)
expanded = orders.explode("items", ignore_index=True)
children = expanded.loc[expanded["items"].notna()].copy()
assert len(expanded) == 4
assert len(children) == 3
assert expanded.loc[expanded["order_id"].eq("B"), "items"].isna().all()
assert expanded["order_total"].sum() == 100
assert orders["order_total"].sum() == 70
counts = children.groupby("order_id").size().reindex(orders["order_id"], fill_value=0)
assert counts.tolist() == orders["item_count"].tolist()
print(expanded)
The two totals intentionally differ. The expanded sum is not a valid revenue result because order A's amount appears twice. Reconciliation uses the parent table for parent totals and the child table for child counts. It does not attempt to repair the inflated number by dividing every order by a guessed number of products.
3. Distinguish empty, unknown, and malformed values
A known empty list means the record has no children under the chosen contract. A missing list can mean the children were not supplied or not yet known. A string that looks like a list is still a string unless deliberately parsed. Treating these three states as interchangeable can erase a useful completeness signal.
Validate nested values before expansion. For a strict list contract, check that every nonmissing value is a list and that each element has an allowed type. Avoid generic truth tests for arbitrary arrays or nested objects, and avoid evaluating string representations as Python code. A JSON parser may be appropriate for a documented JSON field, followed by shape validation.
| Input state | Suggested interpretation | Reporting consequence |
|---|---|---|
| Nonempty list | Known child records | Count validated child positions |
| Empty list | Known absence of children | Preserve parent; child count is zero |
| Missing field | Unknown or incomplete children | Report completeness separately |
| String resembling a list | Unparsed text | Parse only under a documented format |
| Set | Unordered values | Do not infer stable child positions |
Sets are list-like for expansion, but their ordering is not a dependable sequence contract. If the input represents ordered line items, reject sets. If it represents an unordered collection, a deterministic sort can be a reporting convention, provided the elements can be consistently ordered. That convention does not recover an original position that never existed.
4. Expand paired columns together
Products and quantities often describe parallel positions. Multi-column explosion requires matching element counts within each row. Validate the pairing first, then expand both together. Exploding each column independently can create a Cartesian multiplication rather than the intended product-quantity pairs.
import pandas as pd
orders = pd.DataFrame({"order_id": ["A", "B"],
"products": [["pen", "book"], ["bag"]],
"quantities": [[2, 1], [3]]})
assert orders["products"].map(len).equals(orders["quantities"].map(len))
items = orders.explode(["products", "quantities"], ignore_index=True)
items["line_number"] = items.groupby("order_id", sort=False).cumcount() + 1
items["quantities"] = pd.to_numeric(items["quantities"]).astype("int64")
assert list(zip(items["products"], items["quantities"])) == [
("pen", 2), ("book", 1), ("bag", 3)]
assert not items.duplicated(["order_id", "line_number"]).any()
bad = pd.DataFrame({"products": [["pen", "book"]], "quantities": [[2]]})
try:
bad.explode(["products", "quantities"])
except ValueError:
pass
else:
raise AssertionError("Mismatched paired lists must be rejected")
print(items)
The assertions make the intended relationship visible: two positions in order A become two pairs, not four combinations. The line number is derived from the preserved list order. After filtering, sorting, or deduplicating items, a newly computed line number might no longer describe the original source position, so assign it at the appropriate stage.
5. Restore types and preserve parent identity
Exploded values commonly have object dtype. Converting quantities afterward is a separate validation step, not a cosmetic cleanup. Check integral values, missingness, and permitted ranges before imposing an integer dtype. A conversion that silently truncates an invalid fractional quantity changes the data instead of validating it.
Using ignore_index=True provides a simple new row index; it does not create a semantic identifier. Keep the explicit parent key and child position. Repeated original index labels can be useful for tracing, but an index inherited from a file load might have no stable meaning across runs. Do not use that incidental index as the only audit link.
If child values can repeat, decide whether repetitions are meaningful. Two occurrences of the same product may represent two source lines with different options or prices. Dropping duplicate product names would lose information. Deduplication requires an identity rule and a reason, rather than a desire to make the output row count smaller.
6. Reconcile the transformation against source expectations
For validated lists, calculate expected child counts before expansion. Compare those counts with the resulting valid child rows per parent, including zero-child parents. Verify parent key coverage separately from child row count. A transformation can preserve every child while accidentally losing parents with no children, or preserve every parent while counting placeholder rows as children.
For item-level amounts, compare the sum of quantities or line amounts against source item totals when such totals exist. For parent-level amounts, use the parent table or select one validated parent record per identity. Merely calling drop_duplicates on the amount column is unsafe because different orders can legitimately have the same total.
Include a fixture with a repeated child value, an empty list, a missing list, a malformed scalar, and unequal paired lengths. Define whether each case is accepted, rejected, or quarantined. These checks establish the schema boundary and prevent a future upstream format change from appearing as an unexplained reporting shift.
7. State what flattening does not prove
The examples demonstrate row semantics on tiny artificial records. They do not establish performance on deeply nested production data or confirm that any real source is complete. Repeated parent totals are a structural issue rather than a benchmark. Measure actual memory and execution time on a representative dataset if scale becomes a decision factor.
Flattening also does not normalize every nested structure. A list of dictionaries may require further extraction, and independent nested collections may need separate child tables rather than one multiplied table. Retain the original record or a recoverable source reference so an analyst can inspect unexpected combinations. A trustworthy flattened table explains both its rows and the parent records from which they came.
Sources and further reading
- pandas 2.2.3 DataFrame.explode: empty lists, scalar values, multi-column expansion, object dtype, and set ordering.
- pandas 2.2.3 DataFrame.groupby: grouping child records for reconciliation.
- pandas 2.2.3 DataFrame.isna: missing-value detection. Grain and identity recommendations are explicit workflow design choices.