Exercises — Pandas DataFrames and Series basics
This page is a self-test. Read each problem, try it yourself, THEN open the collapsible solution. The problems climb a ladder: from just recognising what a thing is, up to building whole pipelines. Every symbol used here was built in Pandas DataFrames and Series basics. If a term feels new, that note is your anchor.
Prerequisites you may want open: NumPy arrays (a Series wraps one), Python lists & dicts (the raw ingredients).
Level 1 — Recognition
Here we only ask: can you read the object and name its parts? No computation, just vocabulary.
Exercise 1.1 — Name the three parts of a Series
Given:
s = pd.Series([10, 20, 30], index=['a', 'b', 'c'], name='scores')State (a) the values, (b) the index, (c) the name, and (d) the dtype.
Recall Solution
A Series is three arrays glued together plus a label. The figure below stacks them so you can point at each part.

Reading them straight off:
- values — the actual data as a NumPy array:
[10, 20, 30]. - index — the labels that name each value:
['a', 'b', 'c']. - name — the optional identifier for the whole column:
'scores'. - dtype — the shared type of every value. All three are whole numbers, so
int64.
WHY dtype is single: a Series is homogeneous — one type for the whole column. That is what lets NumPy store it as one contiguous block and do fast vectorized maths.
Exercise 1.2 — Read a DataFrame's shape
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'score': [85, 92, 78, 95]
})What is df.shape? What does each number mean?
Recall Solution
df.shape is (4, 2).
- The first number, , is the number of rows (one per person) — the vertical
axis=0direction from the intro figure. - The second number, , is the number of columns (
nameandscore) — the horizontalaxis=1direction.
The rule from the parent note: with index length and columns, the shape is . Picture a table — height first, width second, exactly like reading a NumPy 2-D array.
Level 2 — Application
Now you build and access. Straightforward constructor and indexing use.
Exercise 2.1 — Build a Series and access by label
Create a Series of monthly sales [15000, 18000, 22000] labelled Jan, Feb, Mar, named
sales. Then retrieve February's value.
Recall Solution
sales = pd.Series([15000, 18000, 22000],
index=['Jan', 'Feb', 'Mar'],
name='sales')
sales['Feb'] # 18000WHAT we did: attached human-readable labels to raw numbers. WHY: sales['Feb'] says exactly
what we want, whereas sales[1] forces the reader to remember that position 1 is February.
Under the hood Pandas hashes the label 'Feb' → position 1 → array value in .
Exercise 2.2 — DataFrame from a dict, extract a column
data = {'name': ['Alice', 'Bob', 'Charlie'],
'score': [85, 92, 78]}
df = pd.DataFrame(data)(a) What is df['score'], and what type is it?
(b) What is df['score'].sum()?
Recall Solution
(a) df['score'] extracts one column. Since a DataFrame is a dictionary of Series sharing
one index, a single column is a Series:
0 85
1 92
2 78
Name: score, dtype: int64
So the type is Series.
(b) Summing the values: .
Level 3 — Analysis
Now you must reason about .loc vs .iloc, alignment, and missing data.
Exercise 3.1 — .loc vs .iloc with a custom index
df = pd.DataFrame(
{'name': ['Alice', 'Bob', 'Charlie'],
'score': [85, 92, 78]},
index=['s1', 's2', 's3'])Give the result of each, and explain the difference:
(a) df.loc['s2', 'score']
(b) df.iloc[1, 1]
(c) Why does df.loc[1, 'score'] raise an error?
Recall Solution
(a) .loc is label-based. Row label 's2', column label 'score' → intersection → .
(b) .iloc is position-based. Row position (the 2nd row), column position (the 2nd
column score) → same cell → .
(c) .loc wants labels. The row labels here are the strings 's1','s2','s3' — there is no
label 1 (an integer). So df.loc[1, 'score'] raises KeyError: 1. WHY it feels tempting:
without a custom index the labels would be integers , so .loc[1,...] would work. Once
you overwrite the index with strings, integers are no longer valid labels — you must switch to
.iloc.
Bonus edge case — the scalar accessors .at and .iat. When you want one single cell
(not a slice), Pandas gives faster twins:
df.at['s2', 'score']is the label version — same answer as.loc, but optimised for a single value.df.iat[1, 1]is the position version — same answer as.iloc.
Rule of thumb: .loc/.iloc for ranges and slices; .at/.iat for one scalar. Same
label-vs-position split you already learned, just the "single cell" express lane.
Exercise 3.2 — Missing data from mismatched dict keys
records = [
{'name': 'Alice', 'score': 85},
{'name': 'Bob', 'grade': 'A'} # Bob has 'name' and 'grade' but NO 'score'
]
df = pd.DataFrame(records)(a) What are the column names? (b) What is df.loc[1, 'score']? (c) What dtype is the score
column, and why is it NOT int64?
Recall Solution
(a) Pandas takes the union of all keys across records: name, score, grade.
(b) Row 1 (Bob) had no 'score' key, so Pandas fills the gap with NaN (Not-a-Number, the
"missing" marker). So df.loc[1, 'score'] is NaN.
(c) NaN is a floating-point value. To hold both and NaN in one homogeneous column,
Pandas must promote the whole column to float64 — so scores read as 85.0 and NaN. WHY: a
Series is one dtype; the moment a NaN appears, integers can't represent it, so the column
upgrades to float.
Level 4 — Synthesis
Now you combine filtering, column creation, and aggregation into one flow.
Exercise 4.1 — Filter then aggregate
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'score': [85, 92, 78, 95]
})(a) Build the boolean Series df['score'] > 80.
(b) Use it to keep only high scorers.
(c) What is the mean score of the kept rows?
Recall Solution
(a) Comparing a Series to a number is vectorized — it tests every element and returns a boolean Series with the SAME index. The figure below shades which rows survive:

0 True (85 > 80)
1 True (92 > 80)
2 False (78 > 80)
3 True (95 > 80)
(b) df[df['score'] > 80] keeps rows where the boolean is True — Alice, Bob, Diana. WHY this
works: the boolean Series and df share the same index, so Pandas aligns them row-by-row and
drops the False rows (the shaded-out row 2 in the figure).
(c) Mean of the kept scores: the sum divided by items gives
.
Exercise 4.2 — Add a derived column
Using the same df, add a column passed that is True when score >= 90, then count how many
passed.
Recall Solution
df['passed'] = df['score'] >= 90
df['passed'].sum()WHAT: we assigned a new column built by a vectorized comparison. WHY assigning to df['passed']
creates it: bracket-assignment on a missing label adds that column (bracket-read on a missing
label errors — assignment and reading differ!).
The boolean column: [False, True, False, True] (only Bob=92 and Diana=95 clear ).
.sum() on booleans counts True as : . 2 students passed.
This is exactly the spirit of feature engineering: turning
raw numbers into a new signal your model can use.
Level 5 — Mastery
Now you must reason about alignment — the deepest Pandas idea — and predict outputs exactly.
Exercise 5.1 — Automatic index alignment in Series arithmetic
a = pd.Series([1, 2, 3], index=['x', 'y', 'z'])
b = pd.Series([10, 20, 30], index=['y', 'z', 'w'])
result = a + bWrite out result fully, including any NaN, and explain the rule.
Recall Solution
Pandas adds by matching labels, not positions. It takes the union of both indices
{w, x, y, z} and lines up values by label. The figure shows this as overlapping label sets
(Venn style): only the shared labels y and z get a real sum; the lonely labels become
NaN.

- Label
w: only inb→ no partner →NaN. - Label
x: only ina→ no partner →NaN. - Label
y: in both → . - Label
z: in both → .
So the result reads:
w NaN
x NaN
y 12.0
z 23.0
dtype: float64
WHY the NaNs: w exists only in b, x only in a; with no partner, the sum is undefined →
NaN. WHY the dtype is float64: presence of NaN forces the float promotion (same reason as
3.2). This label-first arithmetic is the superpower over plain
NumPy arrays, which would align by raw position and
silently give wrong sums.
Exercise 5.2 — DataFrame + DataFrame block alignment
p = pd.DataFrame({'x': [1, 2], 'y': [3, 4]}, index=['r0', 'r1'])
q = pd.DataFrame({'y': [10, 20], 'z': [30, 40]}, index=['r1', 'r2'])
result = p + qPredict the full result grid, including NaN, and state the rule.
Recall Solution
The Series rule from 5.1 now runs in both directions at once: Pandas takes the union of the row labels and the union of the column labels, then adds cell-by-cell only where BOTH a row label and a column label match in the two frames.
- Row union:
{r0, r1, r2}. Column union:{x, y, z}. - A cell gets a real number only if that (row, column) pair exists in both
pandq. The only overlap is rowr1, columny:phas there,qhas → . - Every other cell is missing in at least one frame →
NaN.
Result:
x y z
r0 NaN NaN NaN
r1 NaN 14.0 NaN
r2 NaN NaN NaN
WHY only one number survives: alignment is label intersection per axis. This is the block-wise generalisation of 5.1 — same "shared label → combine, lonely label → NaN" law, applied to a 2-D grid instead of a 1-D line.
Exercise 5.3 — Predict .iloc slice vs .loc slice endpoints
df = pd.DataFrame({'v': [10, 20, 30, 40]}, index=['a', 'b', 'c', 'd'])(a) What does df.iloc[0:2] return? (b) What does df.loc['a':'c'] return? (c) Explain the
endpoint difference.
Recall Solution
The figure contrasts the two slice brackets over the same four rows.

(a) .iloc[0:2] is positional and follows Python's rule: the stop index is excluded. So rows
at positions and → labels a, b → values .
(b) .loc['a':'c'] is label-based and here the stop label is included. So a, b, c →
values .
(c) WHY they differ: positional slicing inherits Python/NumPy's half-open convention (stop
excluded, so 0:2 = 2 items). Label slicing has no notion of "one past the end", so Pandas
chose the intuitive inclusive endpoint — if you name 'c', you expect 'c' in the result.
This asymmetry catches everyone once; memorise: iloc excludes the stop, loc includes it.
Exercise 5.4 — Reconstruct a DataFrame's .values sum
df = pd.DataFrame({'q1': [1, 2], 'q2': [3, 4], 'q3': [5, 6]})
total = df.values.sum()What is total, and what is df.values conceptually?
Recall Solution
df.values strips away labels and returns the raw 2-D NumPy array underneath: two rows
[1, 3, 5] and [2, 4, 6]. .sum() over the whole array adds every entry:
. So total = 21.
WHY this matters: it shows a DataFrame is labels on top of a NumPy block — pull the block out
whenever you need pure array speed and no longer care about labels.
See also: Data cleaning (what to do about those
NaNs next) and Matplotlib basics (plotting the
filtered results).