20 Pandas: The Absolute Minimum You Must Know
Pandas looks like a thousand methods, but daily use rests on one data structure, one selection idiom, and one habit — all on this page. Master .loc with a boolean mask and the rest of the library becomes variations on a theme.
20.1 The Mental Model: A DataFrame Is a Dict of Columns
A DataFrame is a table: a dictionary whose keys are column names and whose values are Series — one-dimensional arrays that all share the same row labels (the index, the bold numbers down the left). That’s why you build one from a dict literal, and why df['mark'] gets you a column, not a row:
import pandas as pd
df = pd.DataFrame({
'name': ['Ava', 'Ben', 'Chloe', 'Dan'],
'unit': ['ISYS2001', 'ISYS2001', 'COMP1005', 'COMP1005'],
'mark': [72, 58, 91, 64],
})…and it displays as the table it is, index down the left:
>>> df
name unit mark
0 Ava ISYS2001 72
1 Ben ISYS2001 58
2 Chloe COMP1005 91
3 Dan COMP1005 64In real work the dict literal is replaced by pd.read_csv('marks.csv') — everything after that line is identical. Operations are vectorised: df['mark'] + 5 adds 5 to every row at once. If you’re writing a for loop over rows, you’re almost always fighting the library instead of using it.
20.2 The Habit: Look at Your Data First
Every analysis starts the same way, before any cleverness:
>>> df.head(2) # the first rows — is this the shape you expected?
name unit mark
0 Ava ISYS2001 72
1 Ben ISYS2001 58
>>> df.shape # (rows, columns)
(4, 3)Then df.info() (column types and missing-value counts — the number-that-loaded-as-text bug lives here) and df.describe() (summary statistics — impossible values like a mark of 910 jump out). Thirty seconds of looking saves hours of debugging a “clean” analysis built on dirty data.
20.3 Selecting: loc, iloc, and Boolean Masks
This is the core idea. Two indexers, one rule: .loc selects by label, .iloc selects by position, both as [row, column]:
>>> df.loc[2, 'name'] # row labelled 2, column 'name'
'Chloe'
>>> df.iloc[0, 0] # first row, first column, by position
'Ava'
>>> df['mark'].tolist() # a bare [] with a name is column shorthand
[72, 58, 91, 64]The power move is putting a boolean mask — a Series of True/False built from a comparison — in the row slot. Read it as “the rows where”:
>>> df['mark'] >= 65 # a mask: True/False per row
0 True
1 False
2 True
3 False
Name: mark, dtype: bool
>>> df.loc[df['mark'] >= 65, 'name'].tolist()
['Ava', 'Chloe']Combine conditions with & (and), | (or), ~ (not) — not Python’s and/or, which raise a ValueError — and parenthesise each comparison, because & binds tighter than >=:
>>> passed = (df['unit'] == 'ISYS2001') & (df['mark'] >= 50)
>>> df.loc[passed, 'name'].tolist()
['Ava', 'Ben']20.4 Groupby in One Breath
groupby is “split the rows into groups, apply something to each group, glue the results back together” — the pandas spelling of a pivot table:
>>> df.groupby('unit')['mark'].mean()
unit
COMP1005 77.5
ISYS2001 65.0
Name: mark, dtype: float64Read it right to left: the mean of the mark column, per unit. Swap .mean() for .count(), .max(), or .agg(['mean', 'count']) and you’ve covered most reporting.
20.5 The SettingWithCopy Trap
To change values in a filtered selection, you must do the filter and the assignment in one .loc:
>>> df.loc[df['mark'] < 60, 'mark'] = 60 # one loc: select AND assign
>>> df['mark'].tolist()
[72, 60, 91, 64]The tempting two-step version — df[df['mark'] < 60]['mark'] = 60 — is chained indexing: the first [] returns a copy, the second [] writes to that copy, and the copy is thrown away. Since pandas 3.0 (Copy-on-Write) this is guaranteed to change nothing, and pandas emits a ChainedAssignmentError warning to tell you so. One .loc, always.
20.6 Directing the Machine
An AI writes pandas fluently, but only an informed prompt — one that names columns, masks, .loc, and groupby — gets you code you can verify instead of code that merely runs. Vague prompts invite row loops and chained indexing.
Vague:
"clean up the marks in my dataframe and summarise it"
Informed:
"In df (columns: name, unit, mark), set marks below 50 to 50 using a single .loc
assignment — no chained indexing, no row loop. Then give me the mean and count of
mark per unit with one groupby. I'll check the result with df.describe()."
20.7 Spot the Confabulation
An AI assistant explains how to cap the outliers:
Filter down to the affected rows, then assign the new value:
high = df[df['mark'] > 100]
high['mark'] = 100
Because `high` is a view of `df`, the original DataFrame now has the capped marks.
What’s wrong?
high is not a view — filtering with [] returns a copy, so high['mark'] = 100 edits a throwaway object and df is untouched. The code runs without an exception, which is what makes it dangerous: the analysis continues on uncapped data. Under pandas 3.0’s Copy-on-Write this no-op is guaranteed (you’ll see a ChainedAssignmentError warning). The fix is one line: df.loc[df['mark'] > 100, 'mark'] = 100.
20.8 Where to Practice
- 100 pandas puzzles — a free notebook of short exercises graded from easy to hard; the first thirty drill exactly the selection and groupby idioms on this page. No signup.
- 10 minutes to pandas — the official guided tour; keep it open as the next step whenever this page runs out.
20.9 Quick Reference
| Idiom | What it does |
|---|---|
pd.DataFrame({'col': [...]}) |
build a table from a dict of columns |
pd.read_csv('file.csv') |
build the same table from a file |
df.head() / df.shape |
first rows / (rows, columns) |
df.info() / df.describe() |
types & missing values / summary stats |
df['col'] |
one column, as a Series |
df.loc[row, 'col'] |
select by label |
df.iloc[i, j] |
select by position |
df.loc[mask, 'col'] |
the rows where the mask is True |
(m1) & (m2), \|, ~ |
combine masks — never and/or, always parenthesise |
df.loc[mask, 'col'] = x |
filtered assignment — one .loc, never chained [] |
df.groupby('key')['col'].mean() |
split-apply-combine summary |
series.tolist() |
back to a plain Python list |
That covers the absolute minimum! You can now load a table, look before you leap, select and change exactly the rows you mean, and summarise per group — and to plot what you found, see TAMYMN-Matplotlib.md; everything else in pandas is a variation on these idioms.