Importing, Exploring & Subsetting Data with Pandas
Today’s Mission:
Let’s take a few minutes to address any questions or concerns from last week before diving into new material.
Share with the Class
Open floor for questions about:
Don’t hesitate to ask - chances are others have the same questions!
Stuck on a homework problem?
Let me know which one — we can work through it together in Thursday’s lab.
Your Task: Find the average number of seats on aircraft manufactured by Embraer in 2004 or later
Reflect while you work:
Let’s see what people found…
Python stores data in memory for fast analysis, but first we need to get it there!
The Process:
Copied, not connected
Importing copies the data into your Python session — it does not open a live link to the file.
This matters most in Colab, where your session runs on a temporary cloud machine: when you close the notebook or the runtime restarts, that copy is gone. Same idea locally — close Python, and the data in memory disappears.
The takeaway: every session starts by re-importing your data. That’s not a chore, it’s what makes your analysis reproducible.
Meet Pandas: Python’s most powerful tool for working with spreadsheet-like data
Pandas enables:
Tip
🧠 Think of Pandas as:
Excel + Python power = Reproducible & scalable data analysis
Step 1: Import pandas and load the Ames housing data
Working from a local file instead? read_csv() takes a file path just as happily as a URL:
my_project/
├── notebooks/
│ └── analysis.ipynb ← You are here
└── data/
└── ames_raw.csv ← Your data
Tip
Pro tip: Prefer relative paths over absolute ones — they’re easier to share and maintain with co-workers.
The challenge: In Colab, your files aren’t on your local machine — they’re on a cloud VM.
Option 1: Load directly from a URL — what we just did
Because we read from a URL, that import ran the same way on your laptop and in Colab. This is the approach we’ll use most this term.
Option 2: Upload a local file — for data that isn’t online
Then read the uploaded file just like any local file:
Warning
Files uploaded this way only persist for the current Colab session. If you close the browser or restart your runtime, you’ll need to re-upload.
Here’s what we just imported:
| Order | PID | MS SubClass | MS Zoning | Lot Frontage | Lot Area | Street | Alley | Lot Shape | Land Contour | ... | Pool Area | Pool QC | Fence | Misc Feature | Misc Val | Mo Sold | Yr Sold | Sale Type | Sale Condition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 526301100 | 20 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2010 | WD | Normal | 215000 |
| 1 | 2 | 526350040 | 20 | RH | 80.0 | 11622 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 6 | 2010 | WD | Normal | 105000 |
| 2 | 3 | 526351010 | 20 | RL | 81.0 | 14267 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | Gar2 | 12500 | 6 | 2010 | WD | Normal | 172000 |
| 3 | 4 | 526353030 | 20 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 244000 |
| 4 | 5 | 527105010 | 60 | RL | 74.0 | 13830 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 3 | 2010 | WD | Normal | 189900 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2925 | 2926 | 923275080 | 80 | RL | 37.0 | 7937 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | GdPrv | NaN | 0 | 3 | 2006 | WD | Normal | 142500 |
| 2926 | 2927 | 923276100 | 20 | RL | NaN | 8885 | Pave | NaN | IR1 | Low | ... | 0 | NaN | MnPrv | NaN | 0 | 6 | 2006 | WD | Normal | 131000 |
| 2927 | 2928 | 923400125 | 85 | RL | 62.0 | 10441 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | MnPrv | Shed | 700 | 7 | 2006 | WD | Normal | 132000 |
| 2928 | 2929 | 924100070 | 20 | RL | 77.0 | 10010 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2006 | WD | Normal | 170000 |
| 2929 | 2930 | 924151050 | 60 | RL | 74.0 | 9627 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 11 | 2006 | WD | Normal | 188000 |
2930 rows × 82 columns
Pandas gives you a whole toolkit for getting to know a new dataset — run each of these in your notebook and read what comes back:
Tip
Don’t just run them — read the output. Anything surprise you?
Pop Quiz: Do you notice a difference between these commands?
Attributes = Looking at the “ID card”
Tip
Memory Trick: Methods = Actions = Parentheses!
Those six commands take about ten seconds. Skipping them costs hours.
What you’re really checking:
.shape — did every row load, or did the file get truncated?.dtypes — is SalePrice actually numeric, or did pandas read it as text because one cell had a stray character?.info() — which columns have missing values, and how many?.describe() — any impossible values? A minimum of 0, a maximum of 999999?The real point
Every dataset arrives with a story attached — “this is last year’s sales.”
Inspection is how you check whether the data matches the story before you build an analysis on top of it.
Tip
Golden Rule: .shape, .info(), .describe() — every time, before anything else.
A DataFrame is like an Excel spreadsheet in Python:
| Order | PID | MS SubClass | MS Zoning | Lot Frontage | Lot Area | Street | Alley | Lot Shape | Land Contour | ... | Pool Area | Pool QC | Fence | Misc Feature | Misc Val | Mo Sold | Yr Sold | Sale Type | Sale Condition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 526301100 | 20 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2010 | WD | Normal | 215000 |
| 1 | 2 | 526350040 | 20 | RH | 80.0 | 11622 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 6 | 2010 | WD | Normal | 105000 |
| 2 | 3 | 526351010 | 20 | RL | 81.0 | 14267 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | Gar2 | 12500 | 6 | 2010 | WD | Normal | 172000 |
| 3 | 4 | 526353030 | 20 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 244000 |
| 4 | 5 | 527105010 | 60 | RL | 74.0 | 13830 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | MnPrv | NaN | 0 | 3 | 2010 | WD | Normal | 189900 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2925 | 2926 | 923275080 | 80 | RL | 37.0 | 7937 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | GdPrv | NaN | 0 | 3 | 2006 | WD | Normal | 142500 |
| 2926 | 2927 | 923276100 | 20 | RL | NaN | 8885 | Pave | NaN | IR1 | Low | ... | 0 | NaN | MnPrv | NaN | 0 | 6 | 2006 | WD | Normal | 131000 |
| 2927 | 2928 | 923400125 | 85 | RL | 62.0 | 10441 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | MnPrv | Shed | 700 | 7 | 2006 | WD | Normal | 132000 |
| 2928 | 2929 | 924100070 | 20 | RL | 77.0 | 10010 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2006 | WD | Normal | 170000 |
| 2929 | 2930 | 924151050 | 60 | RL | 74.0 | 9627 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 11 | 2006 | WD | Normal | 188000 |
2930 rows × 82 columns
Prediction Game: What will each of these return?
Same column, three different requests — look at how the output shape changes:
Single brackets → Series
Note
What’s actually happening: the outer [ ] is pandas’ selection syntax — the inner [ ] is just a plain Python list.
So ames[['SalePrice']] is really ames[ ] handed the list ['SalePrice']. And because it’s an ordinary list, you can keep adding to it — ['SalePrice', 'Year Built', 'Neighborhood'] — which is exactly why double brackets scale to as many columns as you want, and single brackets don’t.
Rule #1 — always know which one you’re holding.
A Series — one column, with a dtype: footer underneath and no header row:
Three parts: values, index, and a single dtype.
Tip
Luckily, you can tell at a glance!
Rule #2 — they don’t share the same toolkit.
Sometimes the same attribute or method works on both, but hands back different output:
Series .shape: (2930,)
DataFrame .shape: (2930, 1)
SalePrice 180796.060068
dtype: float64
SalePrice 180796.060068
Year Built 1971.356314
dtype: float64
Other times an attribute or method exists for one and simply isn’t there on the other:
Important
Key Rule: Reach for [[ ]] when you want to keep working like a DataFrame — selecting several columns, chaining, joining, or writing back out to a file.
Using the ames DataFrame:
Neighborhood column as a Series. What neighborhood are the first 3 records in?Neighborhood column as a DataFrame.Neighborhood, Overall Qual, and SalePrice columns as a DataFrame.1 — One name in [ ] gives a Series. The first three records are all in NAmes (North Ames):
2 — Wrap that same name in a list to get a DataFrame back:
When analyzing data, you often want just a subset of your dataset:
Dimension 1: Select Columns
“I only care about year and engines”
Dimension 2: Filter Rows
“I only want aircraft built after 2000”
(Translates to Ames data: “I only care about price and year built”)
(Translates to Ames data: “I only want houses built after 2000”)
You already know how to do this!
One column → Series
Two columns → DataFrame
| SalePrice | Year Built | |
|---|---|---|
| 0 | 215000 | 1960 |
| 1 | 105000 | 1961 |
| 2 | 172000 | 1958 |
Three columns → DataFrame
| SalePrice | Year Built | Neighborhood | |
|---|---|---|---|
| 0 | 215000 | 1960 | NAmes |
| 1 | 105000 | 1961 | NAmes |
| 2 | 172000 | 1958 | NAmes |
Tip
Notice the pattern: the list just keeps growing. Want a fourth column? Add it to the list. The only real decision is one name in [ ] (a Series) versus a list in [[ ]] (a DataFrame, however many columns).
Challenge: Using our Ames dataset, can you find:
Detective Tools:
.shape.max().mean().nunique()Tip
Hint: Can’t remember what a column is called? ames.columns will list every one of them.
Let’s see what you discovered…
Mystery 1: How many houses?
Dataset shape: (2930, 82)
Number of houses: 2930
Mystery 2: Highest sale price?
Mystery 3: Average year built?
Average year built: 1971
Mystery 4: Unique neighborhoods?
Tip
Detective Skills Unlocked! 🔓
You now know how to:
.shape.max().mean().nunique()The Process: Ask a yes/no question about each row
Step 1: Create a condition (True/False for each row)
0 True
1 False
2 False
3 True
4 False
...
2925 False
2926 False
2927 False
2928 False
2929 False
Name: SalePrice, Length: 2930, dtype: bool
This creates a boolean Series - True/False for each row!
Step 2: Use the condition to filter
Original dataset: 2930 houses
Expensive houses: 857 houses
| Order | PID | MS SubClass | MS Zoning | Lot Frontage | Lot Area | Street | Alley | Lot Shape | Land Contour | ... | Pool Area | Pool QC | Fence | Misc Feature | Misc Val | Mo Sold | Yr Sold | Sale Type | Sale Condition | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 526301100 | 20 | RL | 141.0 | 31770 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 5 | 2010 | WD | Normal | 215000 |
| 3 | 4 | 526353030 | 20 | RL | 93.0 | 11160 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 244000 |
| 6 | 7 | 527127150 | 120 | RL | 41.0 | 4920 | Pave | NaN | Reg | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 4 | 2010 | WD | Normal | 213500 |
| 8 | 9 | 527146030 | 120 | RL | 39.0 | 5389 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 3 | 2010 | WD | Normal | 236500 |
| 14 | 15 | 527182190 | 120 | RL | NaN | 6820 | Pave | NaN | IR1 | Lvl | ... | 0 | NaN | NaN | NaN | 0 | 6 | 2010 | WD | Normal | 212000 |
5 rows × 82 columns
Now we have a filtered DataFrame with only expensive houses!
Real Estate Question: Find houses that are expensive AND recently built
Expensive AND recent houses: 478
Warning
Important: Always use & for AND and | for OR with pandas (not and/or)
Another Real Estate Question: Find houses at either end of the market — inexpensive (under $100,000) OR expensive (over $500,000)
# Step 1: Define our conditions
inexpensive = ames['SalePrice'] < 100000
expensive = ames['SalePrice'] > 500000
# Step 2: Combine with | (OR)
either_extreme = inexpensive | expensive
# Step 3: Filter the data
result = ames[either_extreme]
print(f"Inexpensive: {inexpensive.sum()} | Expensive: {expensive.sum()}")
print(f"Either extreme: {result.shape[0]}")Inexpensive: 237 | Expensive: 17
Either extreme: 254
Note
Notice the difference: & narrows your results — a row must satisfy both conditions. | widens them — a row only needs one. Here no house can be under $100k and over $500k at once, so the two groups don’t overlap and the counts simply add up.
So far we’ve treated our two dimensions separately — pick columns, or filter rows. In practice you almost always want them at the same time:
“Give me the neighborhood, price, and size — but only for the homes I actually care about.”
You could do it in two steps, filtering and then selecting. .loc[] does both in one:
Tip
Pattern: df.loc[rows, columns] — the row condition first, the column list second.
Why .loc? Cleaner, more explicit, avoids pandas warnings — and it’s what you’ll see in professional code.
Example 1 — Recent and expensive. Neighborhood, price, and living area for homes built after 2000 and sold above $200,000:
Matching homes: 478
| Neighborhood | SalePrice | Gr Liv Area | |
|---|---|---|---|
| 6 | StoneBr | 213500 | 1338 |
| 15 | StoneBr | 538000 | 3279 |
| 17 | StoneBr | 394432 | 1856 |
Example 2 — Either end of the market. Same three columns, but for homes under $100,000 or over $500,000:
Matching homes: 254
| Neighborhood | SalePrice | Gr Liv Area | |
|---|---|---|---|
| 15 | StoneBr | 538000 | 3279 |
| 29 | BrDale | 96000 | 987 |
| 31 | BrDale | 88000 | 1092 |
Note
Recognize that 254? It’s the same set of houses we found on the previous slide — except now we’re getting back just the three columns we asked for instead of all 82.
Your Challenge: A developer wants to know where the market for newer, larger homes actually is. Work these in order — each step builds on the one before it:
.loc[] to pull the Neighborhood, Year Built, Gr Liv Area, and SalePrice columns for homes built in 2000 or later and 3,000 sq ft or larger.Tip
Hint: Save your .loc[] result to a variable — then steps 2 and 3 are one short line each.
Detective Tools: .loc[] · .shape · .mean() · .nunique()
Step 1 — filter rows and select columns in one move:
| Neighborhood | Year Built | Gr Liv Area | SalePrice | |
|---|---|---|---|---|
| 15 | StoneBr | 2003 | 3279 | 538000 |
| 422 | NridgHt | 2008 | 3140 | 485000 |
| 565 | Somerst | 2004 | 3005 | 280750 |
Step 2 — how many homes?
Step 3 — now ask questions of that subset:
Average sale price: $339,653
Spanning 4 of the 28 neighborhoods in Ames
Important
That last number is the real finding. Homes this new and this large exist in just 4 of 28 neighborhoods — the top of this market is concentrated in a handful of places. Our developer now knows where this market is concentrated.
Notice the workflow: subset first, then analyze. Once new_large exists, every follow-up question is a one-liner.
Avoid these rookie errors:
❌ Forgetting parentheses:
❌ Case sensitivity:
❌ Wrong logical operators:
Back to our original challenge: Let’s solve it the Python way!
Your Task: Find the average number of seats on aircraft manufactured by Embraer in 2004 or later
| tailnum | year | type | manufacturer | model | engines | seats | speed | engine | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | N10156 | 2004.0 | Fixed wing multi engine | EMBRAER | EMB-145XR | 2 | 55 | NaN | Turbo-fan |
| 1 | N102UW | 1998.0 | Fixed wing multi engine | AIRBUS INDUSTRIE | A320-214 | 2 | 182 | NaN | Turbo-fan |
| 2 | N103US | 1999.0 | Fixed wing multi engine | AIRBUS INDUSTRIE | A320-214 | 2 | 182 | NaN | Turbo-fan |
| 3 | N104UW | 1999.0 | Fixed wing multi engine | AIRBUS INDUSTRIE | A320-214 | 2 | 182 | NaN | Turbo-fan |
| 4 | N10575 | 2002.0 | Fixed wing multi engine | EMBRAER | EMB-145LR | 2 | 55 | NaN | Turbo-fan |
Now it’s your turn! Write Python code to: 1. Filter for Embraer aircraft built in 2004 or later 2. Calculate the average number of seats
Hint: Remember to use .loc[] for filtering and .mean() for averages!
Compare this experience to your earlier spreadsheet work…
Complete Solution: Here’s how to solve it step by step
# Step 1: Filter for Embraer aircraft built in 2004 or later
embraer_recent = planes.loc[
(planes['manufacturer'] == 'EMBRAER') & (planes['year'] >= 2004)
]
print(f"Found {embraer_recent.shape[0]} matching aircraft")
# Step 2: Calculate the average number of seats
avg_seats = embraer_recent['seats'].mean()
print(f"Average seats on Embraer aircraft (2004+): {avg_seats:.1f}")
# Bonus: Let's see the range too
print(f"Seat range: {embraer_recent['seats'].min()} to {embraer_recent['seats'].max()}")Found 128 matching aircraft
Average seats on Embraer aircraft (2004+): 33.7
Seat range: 20 to 55
Tip
Python vs Spreadsheet:
| Code | What it does |
|---|---|
pd.read_csv(url) / pd.read_csv(path) |
Reads a CSV into a DataFrame — copies it into memory for this session only |
df.shape |
How big is it? Rows and columns, as a tuple |
df.head() |
Peek at the first 5 rows |
df.columns |
Every column name — your lookup when you forget one |
df.dtypes |
The data type of each column |
df.info() |
Types and missing-value counts, in one summary |
df.describe() |
Summary statistics for the numeric columns |
df['col'] |
One column, as a Series |
df[['col']] |
One column, as a DataFrame |
df[['a', 'b', 'c']] |
Several columns — the inner [ ] is just a Python list |
df[df['col'] > value] |
Keeps only the rows where the condition is True |
& and | |
Combine conditions — & narrows (AND), | widens (OR) |
df.loc[rows, cols] |
Filter rows and select columns in a single step |
s.mean() · s.max() · s.min() |
Collapse a column down to one number |
s.nunique() |
How many distinct values a column holds |
Important
The two ideas underneath all of it:
[ ] gives you a Series; a list in [[ ]] gives you a DataFrame — and they don’t share the same toolkit.This Week’s Lab: Data Detective Training
You’ll practice:
Dataset: COVID-19 college data - help universities understand their data!
Come prepared with questions about anything that felt confusing today!
About today’s concepts:
BANA 7025 | Week 3