Data Analysis with Python (pandas, polars, DuckDB) Module 7 – Reshaping: tidy data, melt, pivot and MultiIndex
Lists, dummy columns and bins
Split list cells into rows with explode, build and reverse indicator columns with get_dummies and from_dummies, and group numbers with cut and qcut.
What you will learn
- Split list-like cells into rows and account for their observation count
- Create and reverse indicator columns for a single label
- Choose fixed-range or quantile groups for numeric values
- Track row labels and missing values through the three reshapes
Before you start
What you’ll learn · Before you start
What you will learn
- Split list-like cells into rows and account for their observation count
- Create and reverse indicator columns for a single label
- Choose fixed-range or quantile groups for numeric values
- Track row labels and missing values through the three reshapes
Before you start
On this page
Three kinds of column hold several facts in one place, and pandas has a tool for each. Cells that hold lists, such as
python;sql;excel, become one row per item: the pandas explode: A reshape that gives each item of a list-like cell its own row and repeats the other fields. The declared input policy determines how empty and missing lists contribute rows. method repeats the rest of the row for every item. A
column of labels becomes dummy columns, one true-or-false indicator column: A column that marks whether each row has a specified label, often with Boolean values or zeros and ones. Several labels per observation can produce several marked columns. per label, with get_dummies, and
from_dummies turns them back into one column. A column of numbers becomes bins, labelled ranges such as 18–25:
cut uses edges you choose, while qcut puts the edges at quantile: A value selected or interpolated from ordered usable observations at a stated probability. Different conventions can return different values. More in “Descriptive statistics that do not mislead”, so that every bin holds about the same number of
rows. These three reshapes come up in nearly every survey, job-board and customer analysis, for counting skills,
preparing model inputs, or grouping ages and spending into bands.
This lesson works through twelve job applications, invented for it, with every run recorded: it explodes their skill lists and counts them per city, turns a label column into dummy columns and back, and bins the applicants’ ages both ways.
Explode preserves each item and repeats its row
Read A01’s three skills, then compare the twelve application rows with the number of listed items.
id,city,age,source,skills
A01,Pune,22,portal,python;sql;excel
A02,Pune,29,referral,sql;excel
A03,Indore,18,portal,excel
A04,Indore,35,walk-in,python;sql
A05,Pune,41,portal,python;power bi
A06,Nagpur,26,referral,sql
A07,Nagpur,31,portal,python;sql;tableau
A08,Pune,24,walk-in,excel;power bi
A09,Indore,47,referral,sql;excel
A10,Nagpur,19,portal,python
A11,Pune,58,referral,excel;sql
A12,Indore,25,portal,python;excel str.split(";") turns each skills cell into a Python list, and the pandas explode method then gives every list item
its own row, copying the other columns:
Follow A01’s three skill items into three rows, then check the same expansion over all twelve applications.
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Keep the originating identifier. A01 has one application row with a skills cell python;sql;excel.
- Split the cell. str.split(";") produces the three list items python, sql and excel.
- Give each item a row. explode repeats A01 and Pune on three item rows. The original row label is repeated unless a fresh index is requested.
- Reconcile the intended unit. The whole complete file has twelve applications and 23 listed items, so it becomes 23 skill rows without becoming 23 applicants.
The 23 skill rows remain twelve applications. Compare the repeated row labels with the fresh-index crosstab.
import pandas as pd
# Twelve invented job applications; each lists its skills in one cell, separated by semicolons.
apps = pd.read_csv("applications.csv")
skills = apps.assign(skill=apps["skills"].str.split(";")).explode("skill")
print(skills[["id", "city", "skill"]].head(6)) # the index labels repeat: one row per list item
print(f"{len(apps)} applications -> {len(skills)} rows, one per skill")
print()
print(skills["skill"].value_counts())
print()
# Repeated index labels trip up anything that aligns on the index.
try:
pd.crosstab(skills["city"], skills["skill"])
except ValueError as err:
print(f"crosstab failed: {err}")
skills = apps.assign(skill=apps["skills"].str.split(";")).explode("skill", ignore_index=True)
print(pd.crosstab(skills["city"], skills["skill"])) # applicants per city and skill Output
id city skill 0 A01 Pune python 0 A01 Pune sql 0 A01 Pune excel 1 A02 Pune sql 1 A02 Pune excel 2 A03 Indore excel 12 applications -> 23 rows, one per skill skill sql 7 excel 7 python 6 power bi 2 tableau 1 Name: count, dtype: int64 crosstab failed: cannot reindex on an axis with duplicate labels skill excel power bi python sql tableau city Indore 3 0 2 2 0 Nagpur 0 0 2 2 1 Pune 4 2 2 3 0
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 explode_skills.py
Lists of 3, 2, 1 … items gave 23 rows for 12 applications, and after the explode, counting skills is an ordinary
value_counts. Look at the index of the first rows, though: 0, 0, 0, 1, 1. Explode keeps each row’s original label on all of its copies. Repeated labels can break an operation that requires unambiguous index alignment; here pd.crosstab refused with “cannot reindex on an axis with duplicate labels”. Passing ignore_index=True, or calling
reset_index(drop=True) afterwards, gives the rows fresh labels, and the crosstab then shows the applicants per city
and skill.
Two more behaviours matter once real data arrives. In pandas, an empty list explodes into one row holding NaN, so it is not lost silently. Several list columns explode together, item by item, when their lists have equal lengths in every row. Lists typed by people bring spaces, capitals and repeats as well, and each of those changes the counts.
Text to Columns Split a delimited column into separate columns or rows in the browser, to see what a split will produce.Checkpoint
Check: List items and row labels
Indicator columns mark labels without adding observations
A model or a statistics routine usually needs numbers, not labels. get_dummies turns one label column into dummy columns, one per distinct label, each true where the row has that label:
Follow A01’s portal label into its marked source column, then recover the label from that row.
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Read the single label. A01 source is portal; A02 source is referral. Each remains one application observation.
- Mark its source column. get_dummies marks source_portal true on A01 and its other two source indicators false.
- Reverse the indicators. from_dummies reads A01's single marked source column and restores portal in a source column.
- Distinguish multi-label flags. A01's skill indicators mark python, sql and excel in the same row; they do not create three application rows.
The source indicators still have one row per application. The skill indicators can mark several labels in that same row.
import pandas as pd
apps = pd.read_csv("applications.csv")
# One label per row: one indicator column per label.
flags = pd.get_dummies(apps["source"], prefix="source")
print(apps[["id", "source"]].join(flags).head(4))
print(f"dtype of the indicators: {flags.dtypes.unique()[0]}")
print()
# And back: from_dummies finds the column that is True in each row.
print(pd.from_dummies(flags, sep="_").head(4))
print()
# Several labels per cell: str.get_dummies splits on the separator and marks every label it finds.
skill_flags = apps["skills"].str.get_dummies(sep=";")
print(skill_flags.head(4))
print()
print(skill_flags.sum().sort_values(ascending=False)) # the same counts as explode and value_counts Output
id source source_portal source_referral source_walk-in
0 A01 portal True False False
1 A02 referral False True False
2 A03 portal True False False
3 A04 walk-in False False True
dtype of the indicators: bool
source
0 portal
1 referral
2 portal
3 walk-in
excel power bi python sql tableau
0 1 0 1 1 0
1 1 0 0 1 0
2 1 0 0 0 0
3 0 0 1 1 0
excel 7
sql 7
python 6
power bi 2
tableau 1
dtype: int64
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dummy_columns.py
pd.get_dummies(apps["source"], prefix="source")made three dummy columns,source_portal,source_referralandsource_walk-in, all of thembool.pd.from_dummies(flags, sep="_")reversed them: in each row it found the one column that is true and wrote its label back into asourcecolumn.- For cells holding several labels,
str.get_dummies(sep=";")splits on the separator and marks every label found, so one row can have several ones. Its column sums are the skill counts that explode andvalue_countsgave.
Two options are worth knowing by name. drop_first=True leaves out the first label’s column, because in a linear
model the dropped label becomes the baseline that the others are compared with. dummy_na=True adds a column that
marks missing values, which are otherwise all false.
Checkpoint
Check: Labels expressed as marked columns
Choose fixed ranges or ranks for numeric values
With specified edges, cut gives fixed ranges; qcut derives them from ranks in the input. Both turn numbers into categories:
Compare the same twelve sorted ages in the overview: the fixed ranges have 5, 4 and 3 rows, while these distinct-valued quartiles have four groups of 3.
Chosen ranges contain 5, 4 and 3 of these ages; their four distinct-valued quartile groups contain 3 each
Text description of the diagram
The top box lists the twelve applicants' ages in order: 18, 19, 22, 24 and 25; then 26, 29, 31 and 35; then 41, 47 and 58.
Two arrows lead down from it. The first, labelled "sizes vary", leads to cut with the edges 17, 25, 35 and 60. Its bands are (17, 25] with 5 applicants, (25, 35] with 4 and (35, 60] with 3.
The second arrow, labelled "edges move", leads to qcut with q=4. Its edges are the data's quartiles, 18, 23.5, 27.5, 36.5 and 58, and each of its four bands holds 3 applicants.
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Keep the input unchanged. The twelve sorted ages are 18, 19, 22, 24, 25, 26, 29, 31, 35, 41, 47 and 58.
- Use chosen ranges. cut uses edges 17, 25, 35 and 60. Its right-closed bands contain five, four and three applicants.
- Use the observed quartiles. qcut computes the inner quartiles 23.5, 27.5 and 36.5. These distinct-valued samples give four groups of three.
- Compare the definitions. Age 25 is at the first fixed band's included right edge, but lies above the first quantile edge 23.5, so its rank group is second.
Follow age 25 into the right-closed interval: An interval that includes its upper endpoint. In (a, b], values equal to b are included and values equal to a are excluded. More in “Nearest-key and range joins: merge_asof” first fixed band. The quartile boundary of 23.5 puts it in the second rank group instead.
import pandas as pd
ages = pd.read_csv("applications.csv")["age"]
print(sorted(ages.tolist()))
print()
# cut: you choose the edges; each band includes its right edge, so (17, 25] holds the ages 18 to 25.
bands = pd.cut(ages, bins=[17, 25, 35, 60], labels=["18-25", "26-35", "36-60"])
print(bands.value_counts(sort=False))
print()
# qcut: four groups of (nearly) equal size; the edges come from the data's quartiles.
quartiles, edges = pd.qcut(ages, q=4, retbins=True)
print(quartiles.value_counts(sort=False))
print(f"quartile edges: {edges.tolist()}") Output
[18, 19, 22, 24, 25, 26, 29, 31, 35, 41, 47, 58] age 18-25 5 26-35 4 36-60 3 Name: count, dtype: int64 age (17.999, 23.5] 3 (23.5, 27.5] 3 (27.5, 36.5] 3 (36.5, 58.0] 3 Name: count, dtype: int64 quartile edges: [18.0, 23.5, 27.5, 36.5, 58.0]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 age_bands.py
pd.cut(ages, bins=[17, 25, 35, 60], labels=[…]) used the bin edge: A boundary used to place a numeric value in a range. The interval convention states which endpoint belongs to each adjacent bin and how out-of-range or missing values are handled. as given. Each band includes its right edge, so
(17, 25] holds the ages 18 to 25, and the bands hold 5, 4 and 3 applicants. pd.qcut(ages, q=4) computed the
quartiles of these ages, 23.5, 27.5 and 36.5, and used them as edges, so each of the four groups holds exactly 3
applicants. retbins=True returned those edges, which you need if the same groups must be applied to other data
later.
| Criterion | cut | qcut |
|---|---|---|
| Edges | Yours, or equal widths | Quantiles of the data |
| Sizes | Vary | About equal |
| Labels | Fixed when edges are fixed | Ranks in this data |
| Known values | NaN outside chosen edges | The observed range is covered |
| When to choose | Age, price or grade bands | Quartiles, deciles |
The details that decide which band a value lands in also have names. include_lowest=True makes the first band
include its left edge. right=False builds bands such as [18, 26), closed on the left instead of the right.
duplicates="drop" lets qcut merge edges that coincide when many values are tied. Ranking orders values before
forming groups. Polars offers these reshapes too; Polars 2 changes the empty-list default.
bin_intervals uses specified breaks. bin_quantiles uses value quantiles. bin_ranks uses ranks.
Checkpoint
Check: Fixed ranges and quantile groups
Messy list cells: counts that lie, and how to fix them Pro: opens with a Pro, Premium or Ultimate pass
Lists typed by people carry spaces, capitals, repeats and empty items, and each of them changes the counts without an error. This part reproduces every one on the same applications, measures its effect, and ends with an exercise.
In this part:
- Count applicants after normalising skill items
- Match paired list lengths before exploding
- Practise: count skills per city
2 diagrams · 5 runnable examples (CSV, Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Binning without losing rows: edges, ties and moving quantiles Pro: opens with a Pro, Premium or Ultimate pass
An out-of-range cut value can become missing without an error, while recalculating quantile edges changes the numerical meaning of a label. This part reproduces both, with the counts, and shows how to keep bins comparable over time.
In this part:
- Check boundary ages before trusting band counts
- State which promise changes when quantiles tie
- Freeze reference edges when labels compare periods
3 diagrams · 3 runnable examples (Python) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Explode and bins in polars, stable across versions Pro: opens with a Pro, Premium or Ultimate pass
Polars 2 changes how empty lists and binning are handled. Compare the pinned behaviour with the documented compatibility cases, then state a row and interval policy before moving between versions.
In this part:
- Separate empty and null lists before row counting
- Translate outer edges and interval closure deliberately
- Keep the row policy explicit at version boundaries
3 diagrams · 2 runnable examples (Python) · about 8 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Practise: identify the reshape and its row unit
Use the list, label and numeric examples to distinguish observation rows from derived columns.
Practice that adapts to you
Practise this lesson
8 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
-
Fresher to mid · Warm-up
What is the difference between cut and qcut in pandas?
Both turn numbers into categories, but they choose edges differently. cut uses explicit edges or a number of equal-width bins across this input's range. Explicit edges keep an age range fixed; asking for four equal-width bins does not fix their numerical boundaries across files. qcut computes sample quantiles, so groups have about equal counts when the values allow distinct edges, and their boundaries change with the input. Choose fixed cut edges for comparable age or price bands and qcut for relative quartiles or deciles. cut includes right edges by default; tied quantiles can make qcut refuse, while duplicates="drop" returns fewer groups.
-
Mid to senior
A dashboard counts demand for skills from the semicolon-separated skills column of job applications. The week the application form changed from a free-text box to checkboxes, demand for SQL rose 30 %. What do you check before believing it?
-
Mid to senior
Marketing labels customers top, high, mid and low every month with pd.qcut(spend, 4). After a slow month, the top group includes customers who spend half of what top customers spent a month earlier, and the campaign underperforms. Explain what happened, and design labels that stay comparable from month to month.
-
Mid to senior
You must turn a city column with 400 distinct values into inputs for a linear regression on 1 million rows. What does pd.get_dummies produce, what goes wrong, and what would you do instead?
Key takeaways
str.splitand explode give each list item its own row. Explode repeats the index labels, so useignore_index=Truebefore anything that aligns on the index.- get_dummies turns a label column into indicator columns and
from_dummiesreverses it;str.get_dummies(sep=…)handles several labels per cell. - cut bins numbers by edges you choose and includes each right edge; qcut puts the edges at quantiles, so its bins hold about equal numbers of rows.
retbins=Truereturns the edges, so that the same bins can be applied to other data.
Terms in this lesson
- bin edge
- A boundary used to place a numeric value in a range. The interval convention states which endpoint belongs to each adjacent bin and how out-of-range or missing values are handled.
- explode
- A reshape that gives each item of a list-like cell its own row and repeats the other fields. The declared input policy determines how empty and missing lists contribute rows.
- indicator column
- A column that marks whether each row has a specified label, often with Boolean values or zeros and ones. Several labels per observation can produce several marked columns.
Corrections
- Added figures and checks on the recorded applications; clarified missing-row controls, interval coverage and comparable quantile labels.
References
- pandas.DataFrame.explode (pandas API reference) (pandas development team (NumFOCUS))
- pandas.get_dummies (pandas API reference) (pandas development team (NumFOCUS))
- pandas.from_dummies (pandas API reference) (pandas development team (NumFOCUS))
- pandas.Series.str.get_dummies (pandas API reference) (pandas development team (NumFOCUS))
- pandas.cut (pandas API reference) (pandas development team (NumFOCUS))
- pandas.qcut (pandas API reference) (pandas development team (NumFOCUS))
- polars.DataFrame.explode (Polars API reference) (Polars)
- polars.Expr.cut (Polars API reference) (Polars)
- Polars 1.33.1 DataFrame API (official source) (Polars)
- Polars 1.33.1 Expr API (official source) (Polars)
- Upgrading to Polars 2 (explode, cut and qcut changes) (Polars)
- numpy.dtype.itemsize (NumPy API reference) (NumPy developers)
- OneHotEncoder (scikit-learn API reference) (scikit-learn developers)
- Ridge (scikit-learn API reference) (scikit-learn developers)
- TargetEncoder (scikit-learn API reference) (scikit-learn developers)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress