1.4.6 · D4Python & Scientific Computing

Exercises — Pandas DataFrames and Series basics

2,989 words14 min readBack to topic

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.

Figure — Pandas DataFrames and Series basics

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=0 direction from the intro figure.
  • The second number, , is the number of columns (name and score) — the horizontal axis=1 direction.

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']   # 18000

WHAT 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:

Figure — Pandas DataFrames and Series basics
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 + b

Write 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.

Figure — Pandas DataFrames and Series basics
  • Label w: only in b → no partner → NaN.
  • Label x: only in a → 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 + q

Predict 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 p and q. The only overlap is row r1, column y: p has there, q has .
  • 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.

Figure — Pandas DataFrames and Series basics

(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).