Wide vs Long Format
Wide vs Long Format
Data comes in two fundamental shapes, and knowing which one you need is half the battle. Wide format stores each variable in its own column. Long format (also called tidy or stacked format) stores variable names in one column and values in another.
Consider monthly sales data. In wide format, each month is a column:
import pandas as pd
wide = pd.DataFrame({
'product': ['A', 'B', 'C'],
'jan': [100, 150, 200],
'feb': [120, 160, 210],
'mar': [110, 170, 220]
})
print(wide)
# product jan feb mar
# 0 A 100 120 110
# 1 B 150 160 170
# 2 C 200 210 220
In long format, month names and values are stacked into two columns:
long = pd.DataFrame({
'product': ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C'],
'month': ['jan', 'feb', 'mar', 'jan', 'feb', 'mar', 'jan', 'feb', 'mar'],
'sales': [100, 120, 110, 150, 160, 170, 200, 210, 220]
})
print(long)
# product month sales
# 0 A jan 100
# 1 A feb 120
# 2 A mar 110
# 3 B jan 150
# 4 B feb 160
# 5 B mar 170
# 6 C jan 200
# 7 C feb 210
# 8 C mar 220
Neither format is universally better. Wide format is compact and human-readable — it is what you would put in a report or a slide. Long format is structured for computation — it is what groupby, merge, and most plotting libraries expect.
The principle that guides the choice: long format is the default for analysis. When each row is an observation and each column is a variable, operations like filtering, grouping, and joining become straightforward. Wide format is a presentation concern — convert to it only when you need to display results.
This distinction matters because most real-world data cleaning is a series of conversions between these two shapes. Data arrives in wide format from Excel exports, database dumps, and API responses. You melt it to long format for analysis. You pivot it back to wide format for reporting. Fluency in both directions is a core data manipulation skill.
Melting: Wide to Long
Melting: Wide to Long
pd.melt() converts wide-format data to long-format by unpivoting columns into rows. The function takes identifier columns (which stay as columns) and value columns (which get stacked into a variable-value pair).
import pandas as pd
wide = pd.DataFrame({
'product': ['A', 'B', 'C'],
'jan': [100, 150, 200],
'feb': [120, 160, 210],
'mar': [110, 170, 220]
})
long = pd.melt(
wide,
id_vars=['product'],
value_vars=['jan', 'feb', 'mar'],
var_name='month',
value_name='sales'
)
print(long)
# product month sales
# 0 A jan 100
# 1 B jan 150
# 2 C jan 200
# 3 A feb 120
# 4 B feb 160
# 5 C feb 210
# 6 A mar 110
# 7 B mar 170
# 8 C mar 220
The id_vars parameter lists columns that identify each observation and should remain as columns. The value_vars parameter lists columns to unpivot. If you omit value_vars, all columns not in id_vars are melted. The var_name and value_name parameters let you name the resulting variable and value columns instead of using the defaults variable and value.
A practical example: survey data where each respondent's answers to multiple questions are stored in separate columns.
survey = pd.DataFrame({
'respondent_id': [1, 2, 3],
'age': [25, 34, 45],
'q1_satisfaction': [4, 5, 3],
'q2_recommend': [3, 4, 2],
'q3_return': [5, 4, 3]
})
long_survey = pd.melt(
survey,
id_vars=['respondent_id', 'age'],
value_vars=['q1_satisfaction', 'q2_recommend', 'q3_return'],
var_name='question',
value_name='score'
)
print(long_survey)
After melting, you can group by question to compute average scores per question, or group by age to see how responses vary by demographic. These operations are awkward or impossible in the wide format.
One thing to watch: melting increases row count. A DataFrame with 1000 rows and 12 month columns becomes 12,000 rows after melting. This is expected, but be aware of it when working with large datasets. The long format trades row count for structural clarity.
Pivoting: Long to Wide
Pivoting: Long to Wide
pivot() is the inverse of melt(). It converts long-format data to wide-format by spreading the values of one column into multiple columns. The key difference from melt() is that pivot() requires a unique combination of index and column values — it cannot handle duplicates.
import pandas as pd
long = pd.DataFrame({
'product': ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C'],
'month': ['jan', 'feb', 'mar', 'jan', 'feb', 'mar', 'jan', 'feb', 'mar'],
'sales': [100, 120, 110, 150, 160, 170, 200, 210, 220]
})
wide = long.pivot(index='product', columns='month', values='sales')
print(wide)
# month jan feb mar
# product
# A 100 120 110
# B 150 160 170
# C 200 210 220
The index parameter specifies which column becomes the row index. The columns parameter specifies whose values become column headers. The values parameter specifies which column provides the cell values.
The result has a MultiIndex on the columns if you pass a list to columns. Call .reset_index() to flatten the index back into a column.
wide = long.pivot(index='product', columns='month', values='sales').reset_index()
print(wide)
# month product jan feb mar
# 0 A 100 120 110
# 1 B 150 160 170
# 2 C 200 210 220
pivot() raises a ValueError if there are duplicate entries for the same (index, columns) combination. This is a feature, not a bug — it prevents silent data loss. If you have duplicates, you need pivot_table() instead.
# This raises ValueError if (product, month) is not unique
# ValueError: Index contains duplicate entries, cannot reshape
When to use pivot(): when you have a clean long-format dataset with unique keys and you want to reshape it for display or for feeding into an algorithm that expects wide input. When to use pivot_table(): when you need to aggregate duplicates during the reshape.
Pivot Table: Aggregation During Reshape
Pivot Table: Aggregation During Reshape
pivot_table() is pivot() with a built-in aggregation step. It handles duplicate (index, column) combinations by applying an aggregation function, defaulting to the mean.
import pandas as pd
# Long-format sales data with duplicate (product, month) entries
long = pd.DataFrame({
'product': ['A', 'A', 'A', 'A', 'B', 'B', 'B', 'B'],
'month': ['jan', 'jan', 'feb', 'feb', 'jan', 'jan', 'feb', 'feb'],
'store': ['X', 'Y', 'X', 'Y', 'X', 'Y', 'X', 'Y'],
'sales': [100, 120, 110, 130, 150, 160, 170, 180]
})
# pivot_table aggregates duplicates (mean by default)
wide = long.pivot_table(
index='product',
columns='month',
values='sales',
aggfunc='mean'
)
print(wide)
# month jan feb
# product
# A 110 120
# B 155 175
The aggfunc parameter accepts any aggregation function: 'sum', 'mean', 'count', 'min', 'max', or a custom function. You can also pass a list of functions to compute multiple aggregations at once.
wide = long.pivot_table(
index='product',
columns='month',
values='sales',
aggfunc=['mean', 'sum', 'count']
)
print(wide)
pivot_table() also supports multiple index and column levels, creating a MultiIndex on both axes. This is useful for cross-tabulations.
# Cross-tabulation: product vs month, broken down by store
wide = long.pivot_table(
index=['product', 'store'],
columns='month',
values='sales',
aggfunc='sum'
)
print(wide)
The margins parameter adds row and column totals, which is useful for summary reports.
wide = long.pivot_table(
index='product',
columns='month',
values='sales',
aggfunc='sum',
margins=True,
margins_name='Total'
)
print(wide)
# month jan feb Total
# product
# A 220 240 460
# B 310 350 660
# Total 530 590 1120
Choosing between pivot() and pivot_table() is simple: if your data has unique keys and you want a pure reshape, use pivot(). If you need to aggregate during the reshape, use pivot_table(). Using pivot_table() when you expect unique keys is also fine — it will just apply the aggregation function to single values, which is a no-op.
Practice Problems
You have a wide-format survey DataFrame with columns [respondent_id, age, q1_satisfaction, q2_recommend, q3_return]. Melt it to long format with columns [respondent_id, age, question, score].
Solution
long_survey = pd.melt(
survey,
id_vars=['respondent_id', 'age'],
value_vars=['q1_satisfaction', 'q2_recommend', 'q3_return'],
var_name='question',
value_name='score'
)
print(long_survey.head()) Given a long-format DataFrame with columns [product, month, sales], pivot it to wide format where products are rows, months are columns, and sales are values. Then reset the index so product becomes a column.
Solution
wide = long.pivot(index='product', columns='month', values='sales').reset_index()
print(wide) You have a long-format DataFrame with columns [product, month, store, sales] where the same (product, month) combination appears multiple times (once per store). Use pivot_table to compute the mean sales per product per month.
Solution
wide = long.pivot_table(
index='product',
columns='month',
values='sales',
aggfunc='mean',
margins=True,
margins_name='Total'
)
print(wide) Quiz
1. What is the main difference between wide and long data formats?
2. When should you use pivot_table() instead of pivot()?
3. What does the id_vars parameter specify in pd.melt()?
Flashcards
Question
What is the difference between wide and long format?
Click to reveal answer
Answer
Wide format stores each variable in its own column (good for display). Long format stacks variable names and values into separate columns (good for computation). Long format is the default for analysis.
Question
When should you use pivot_table() instead of pivot()?
Click to reveal answer
Answer
Use pivot_table() when your data has duplicate (index, column) combinations that need aggregation. pivot() raises an error on duplicates. pivot_table() applies an aggregation function (mean by default) to handle them.
Question
What does pd.melt() do?
Click to reveal answer
Answer
It converts wide-format data to long-format by unpivoting specified columns into rows. id_vars stay as columns; value_vars get stacked into a variable-value pair.
Question
What does the margins parameter do in pivot_table()?
Click to reveal answer
Answer
It adds row and column totals to the pivot table output. The margins_name parameter controls the label for the totals row/column (default is 'All').
Revision Notes
Key Takeaways
- 1. Long format (one row per observation) is the default for analysis; wide format is for display
- 2. pd.melt() converts wide to long by unpivoting columns into variable-value pairs
- 3. pivot() converts long to wide but requires unique (index, column) combinations
- 4. pivot_table() handles duplicates by aggregating them during the reshape
- 5. Choose pivot() for pure reshapes and pivot_table() when aggregation is needed
Interview Tips
- • Explain why long format is preferred for analysis — it makes groupby, merge, and plotting straightforward
- • Know when to use pivot_table() over pivot() — the duplicate key handling is a common interview follow-up
- • Be able to describe a real scenario where you converted between formats and why
Cheat Sheet
Cheat Sheet: Melt and Pivot
Format Decision
- Long format: one row per observation, one column per variable → use for analysis
- Wide format: one column per variable value → use for display/reporting
Melt (Wide → Long)
- pd.melt(df, id_vars=['id'], value_vars=['c1', 'c2'],
var_name='variable', value_name='value') - id_vars: columns that stay as columns
- value_vars: columns to unpivot (default: all non-id columns)
Pivot (Long → Wide)
- df.pivot(index='row', columns='col', values='val')
- Requires unique (index, column) combinations
- Raises ValueError on duplicates
Pivot Table (Long → Wide + Aggregation)
- df.pivot_table(index='row', columns='col', values='val',
aggfunc='mean', margins=True) - Handles duplicates via aggregation
- margins=True adds totals
Key Distinction
- pivot(): pure reshape, no aggregation, fails on duplicates
- pivot_table(): reshape + aggregation, handles duplicates