In the lab we found that numpy is much faster than a list and a for loop. Numpy is powerful and useful to analyse large numerical series.
The array numpy introduced became the shape of scientific computing — pandas, scipy and scikit-learn all sit on top of it. PyTorch took the same idea and put it on GPUs, and that is the machinery modern AI is built from, ChatGPT and Claude included.
An array holds one kind of thing. Today a row reads August, Hoggs, Rbean, 140, 0, 1 — words and numbers together, every column meaning something different.
numpy has no answer to that. pandas does.
Two summers, 1,620 surveys, 18 crops, six Bombus species — counted flower by flower. One row = one survey.
| Month | Allotment | Crop | Flowers | Honeybees | Bumblebees |
|---|---|---|---|---|---|
| August | Hoggs | Bbean | 350 | 0 | 1 |
| June | Hoggs | Bbean | 100 | 0 | 1 |
| August | Hoggs | Squash | 6 | 5 | 0 |
| August | Hoggs | Squash | 8 | 5 | 1 |
| August | Hoggs | Raspberry | 60 | 1 | 2 |
Words on the left, numbers on the right — the shape numpy could not hold. (Bbean = broad bean, Rbean = runner bean.)

If the bumblebees vanished, what would you lose?
Nicholls, Griffiths-Lee, Basu, Chatterjee & Goulson (2022), Plants People Planet — collected about two miles from this room.
Each survey counts who visited a patch of flowers. Two columns matter today: Honeybees and Bumblebees. Both are bees, both carry pollen — but they are very different animals.
One species, mostly kept by beekeepers: livestock, in hives of tens of thousands. If a colony dies, the beekeeper replaces it.
About two dozen wild species in the UK, six in our data. Small nests in the ground and hedges. Big and hairy: they fly in cold, wet weather, and their tongues come in different lengths — so they suit different flowers.
Wild bumblebees are declining: lost meadows and hedges, pesticides, disease, a warming climate. The short-haired bumblebee was last seen in Britain in 1988. No beekeeper can restock them.
bee visits = honeybee visits + bumblebee visits
Take the bumblebees away and the honeybees are still there. A crop both of them visit would cope. A crop that mostly relies on bumblebees would be left with almost no bees at all.
The bumblebees vanish; the honeybees stay. Which crop would be left with almost no bee visits? Go with your gut — by the end of the hour, pandas will tell us who was right.
RaspberrySquashBroad beanAppleBlackberryRemember what you picked. Watch for clues on the way — the data settles it at the end.
One question, answered with pandas from start to finish. The rest of pandas is for the lab.
Open the file and meet two receptions — the table and its columns — then go looking for trouble in the data.
Five steps, one pandas method each, from the raw table to the crop that would lose its bees. Run each one, then fix a broken copy.
Your votes from the start, now with the answer.
Everything else pandas can do, tried on the same survey at your own pace.
You won't remember every method — nobody does. The cheat sheet at the end is for practising later.
Working with messy tabular data of varied types, the reality of biology data...

Real data comes as CSV or Excel files. pandas reads the header row, works out a type for every column, and hands you a DataFrame — a table with its own analysis methods.
np.loadtxt(path, delimiter=",", skiprows=1, usecols=1)
Say the separator, skip the header, and give the column number. Mixed text and numbers? An error.
pd.read_csv(path)
Reads the header itself, names every column, and picks a type per column. Then .head() shows the first five rows.
Last week: an object is a hotel, and you only ever talk to its reception. The DataFrame is the main reception you will work with today.
df.shape: 1,620 rows, 13 columns. df.dtypes: the type of every column.
df.head(), df.describe(), df.sort_values() — jobs the table does for you.
An array has one type for everything. A DataFrame has one type per column: text where there is text, integers where there are counts.
A taste of the menu at the table's reception. You don't learn it; you look it up.
…and about 200 more. The full menu: pandas.pydata.org → DataFrame
Ask the table for one column and you get the second type of the day — a numpy array that remembers its own name.
.name and .dtype — a column knows what it is called and what it holds. One type, like an array.
.sum(), .mean(), .max(), and a mask counted by summing its Trues — all last week's numpy.
548 bumblebees in 1,620 surveys, and only 311 surveys saw any. Most saw none at all — remember that when you start averaging.
Everything numpy did, plus jobs for columns of words: counting categories, finding the distinct values, working on text.
…and about 200 more. The full menu: pandas.pydata.org → Series
Now use the Series menu on the real data. Before any analysis, go looking for trouble: run the first line, then uncomment the second. Spot something odd? Say so — or at least think it loudly.
Flowers = open flowers on the watched patch. Typical patch: 25 (the 50% row). Max: 2,000 — a fruit tree in blossom. And the big values are all round: 500, 800, 1,000. Nobody counted those; they are guesses.
Weald was surveyed 495 times, Camp_Site twice. Any total for Camp_Site means nothing. Remember this one — it matters at the end.
Same table as on the slides. Pick the line that does the job.
import pandas as pd
df = pd.read_csv("pollinators_lecture.csv")
# 1,620 surveys (rows), 13 columnsWhat does df.shape give you?
df = pd.read_csv("pollinators_lecture.csv") # 1,620 surveys, 13 columns1620(13, 1620)(1620, 13)[1620, 13]Which line gives you the Bumblebees column as a Series?
df = pd.read_csv("pollinators_lecture.csv")df[["Bumblebees"]]df["Bumblebees"]df("Bumblebees")df.Bumblebees()How many surveys saw at least one bumblebee? Which line tells you?
df = pd.read_csv("pollinators_lecture.csv")df["Bumblebees"].sum()df["Bumblebees"].count()len(df["Bumblebees"])(df["Bumblebees"] > 0).sum()Try this after the lecture. One column at a time — every method is one you used on arrays last week.
Five steps, one method each — and at the end, your bet.
"If the bumblebees vanished, which crop would lose its bees?" is too big for one line. Break it into questions pandas can answer one at a time.
1Who visits one crop?
rasp = df[df["Crop"] == "Raspberry"]2…and every crop at once?
visits = df.groupby("Crop")[["Honeybees", "Bumblebees"]].sum()3What share is bumblebee?
visits["share"] = visits["Bumblebees"] / (visits["Honeybees"] + visits["Bumblebees"])4Which crop is top?
visits.sort_values("share", ascending=False)5Can we trust it?
enough = visits[visits["Bumblebees"] >= 20]→ the crop that would lose its bees
Each step: what we want → the method → run it → read the result → fix a broken line.
df["Crop"] == "Raspberry" is a mask: 1,620 True/False. Inside df[ ] it keeps the rows where it is True. The result is a smaller DataFrame, so we can ask its columns again.
Raspberry has the most bumblebee visits — but 245 honeybee visits too. Lose the bumblebees and the honeybees are still there. And there are 17 more crops to check.
Should give the raspberry rows: KeyError. Run it, vote, fix it.
rasp = df[df["Crop"] == "Raspberry"]rasp = df["Crop"] = "Raspberry"rasp = df["Raspberry"]rasp = df[df["Crop"]] == "Raspberry"Three moves: split the rows into piles, apply a sum to each pile, combine the answers. Six real rows from the survey — click through.
df.groupby("Crop")[["Honeybees", "Bumblebees"]].sum()| Crop | Honey | Bumble |
|---|---|---|
| Bbean | 0 | 1 |
| Squash | 5 | 0 |
| Raspberry | 1 | 2 |
| Bbean | 0 | 1 |
| Squash | 5 | 1 |
| Raspberry | 2 | 2 |
| Bbean | 0 | 1 |
| Bbean | 0 | 1 |
| Raspberry | 1 | 2 |
| Raspberry | 2 | 2 |
| Squash | 5 | 0 |
| Squash | 5 | 1 |
| Bbean | 0 | 2 |
sum of 2 rows
| Raspberry | 3 | 4 |
sum of 2 rows
| Squash | 10 | 1 |
sum of 2 rows
visits
| Honey | Bumble | |
|---|---|---|
| Bbean | 0 | 2 |
| Raspberry | 3 | 4 |
| Squash | 10 | 1 |
the table
split
apply
combine
Six rows, crops mixed up.
Step 2 does exactly this, with 1,620 rows and 18 piles.
.groupby("Crop") sorts the rows into one pile per crop. Pick the two columns, .sum() each pile: one row per crop.
Eighteen crops, each with its two totals. The crop names are now the row labels. Look down the list — does any crop look different?
Two columns are a list: [["Honeybees", "Bumblebees"]]. Outer brackets pick, inner ones make the list.
Should give one row per crop: ValueError. Run it, vote, fix it.
visits = df.groupby["Crop"][["Honeybees", "Bumblebees"]].sum()visits = df.groupby(Crop)[["Honeybees", "Bumblebees"]].sum()visits = df.groupby("Crop")[["Honeybees", "Bumblebees"]].sum()visits = df["Crop"].groupby()[["Honeybees", "Bumblebees"]].sum()Assign to a column name that doesn't exist yet and pandas makes it. The sum runs down every row at once — last week's numpy, with names.
The share is the question that matters: 0.92 means 92 of every 100 bee visits are bumblebees. Take them away and only 8 are left. A big share is a big loss.
No error, but a share can’t be above 1. Run it, vote, fix it.
visits.share = visits["Bumblebees"] / (visits["Honeybees"] + visits["Bumblebees"])visits["share"] = visits["Bumblebees"] / (visits["Honeybees"] + visits["Bumblebees"])visits["share"] == visits["Bumblebees"] / (visits["Honeybees"] + visits["Bumblebees"])share = visits["Bumblebees"] / visits["Honeybees"] + visits["Bumblebees"].sort_values("share") orders the rows by one column, smallest first. ascending=False turns it round.
Strawberry and blueberry: 100% bumblebees! From one bee each. Odd thing 2 from Part 1 is back — a number built on almost no data means nothing.
No error, but the list is upside down. Run it, vote, fix it.
print(visits.sort_values().head())print(visits["share"].sort().head())print(visits.sort_values("share").tail(1))print(visits.sort_values("share", ascending=False).head())Step 1's mask again, this time on the crop table: keep only crops with at least 20 bumblebee visits, then sort.
23 bumblebee visits, 2 from honeybees. Take the bumblebees away and almost nothing is left. Raspberry has the most bumblebees, but its 245 honeybees would carry on.
No error, but where is the table? Run it, vote, fix it.
enough = visits[visits["Bumblebees"] => 20]enough = visits["Bumblebees" >= 20]enough = visits[visits["Bumblebees"] >= 20]enough = visits.loc["Bumblebees" >= 20]The same poll as at the start of the lecture — your votes, now with the answer.
RaspberrySquashBroad beanAppleBlackberryMost money usually goes on raspberry: the biggest number in the table. The answer was the biggest share. Picking the right question is half of data analysis.
Today gave you a first answer. The lab tests whether it holds up — same file, the same tools, put together.
Bumblebee share of each crop’s visits — done together today
groupby, a new column, filter, sort
Make it fair: visits per survey, per flower
new columns, .mean()
Is broad bean’s 92% solid? It rests on 25 visits
groupby(...).size(), filter
Which bumblebee species carry the broad beans?
the six species columns
Does the answer change from month to month?
groupby(["Month", "Crop"])
The catch to watch for: a crop surveyed 269 times will always have the biggest totals. Compare like with like — per survey, or per flower — before you crown a winner.
Everything from today, and a few relatives. Practise them on df in the revision slides — this is the list for later.
s is any one column, e.g. s = df["Bumblebees"]. Full docs: pandas.pydata.org → User Guide.
Two ideas and five moves — enough to take a raw survey to an answer.
The rest of pandas — .loc, .iloc, value_counts, & and | — and real files that fight back: missing values, and a column named "Hortorum " with a trailing space, so asking for it raises a KeyError.
Next lecture: making these tables into figures people can read.
You learn pandas by using it. Pick one thing from the left column and do a little each week.
Reuven Lerner (Manning). 200 exercises on real data sets — the lab, times ten.
DataCamp. Filtering, groupby and more, in short browser exercises — our parallel course.
Free, short, in the browser. A good evening’s revision.
Wes McKinney, who wrote pandas. Free to read online.
The official docs, one question per page: “How do I select a subset of a table?”
The whole library on one page, with examples.
Two pages, worth printing.