Welcome Data Detectives! 🔍

Today’s Mission:

  • Learn to import real-world datasets
  • Master DataFrame investigation techniques
  • Discover the power of data subsetting

Discussion: Last Week’s Content

Questions from Week 2?

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:

  • Variables, data types, and operations
  • Jupyter notebooks and Python basics
  • Homework assignments or exercises
  • Any concepts that felt confusing
  • How to approach problem-solving in Python

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.

Experience First: The Spreadsheet Challenge

Try This!

Your Task: Find the average number of seats on aircraft manufactured by Embraer in 2004 or later

  1. Download and open: https://tinyurl.com/42nartw7
  2. Use Excel, Google Sheets, or any spreadsheet tool
  3. Try to answer the question using filters, sorting, or manual search

Reflect while you work:

  • How long is this taking?
  • What if this had 1 million rows?
  • Could you easily repeat this process?

Let’s see what people found…

📓 Follow Along in the Notebook

Week 3 Lecture Notebook

Open the companion notebook in Colab now and follow along as we work through today’s examples on importing data, exploring DataFrames, and subsetting rows and columns.

Open In Colab

Getting Data Into Python

The Data Journey: From Disk to Detective Work

Python stores data in memory for fast analysis, but first we need to get it there!

The Process:

  1. Data sits on your computer’s “disk” (hard drive)
  2. Python copies the file into memory (RAM)
  3. You can now investigate and analyze!

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.

Your Detective Toolkit: Pandas 🐼

Meet Pandas: Python’s most powerful tool for working with spreadsheet-like data

Pandas enables:

  • Importing data from files like CSV and Excel
  • Exploring and understanding datasets
  • Cleaning and transforming messy data
  • Filtering and subsetting rows/columns
  • Analyzing and summarizing information

Tip

🧠 Think of Pandas as:

Excel + Python power = Reproducible & scalable data analysis

Pandas Documentation

Let’s Import Our First Dataset!

Step 1: Import pandas and load the Ames housing data

import pandas as pd

# Load the real estate data straight from the course repo
url = "https://raw.githubusercontent.com/bradleyboehmke/uc-bana-7025/main/data/ames_raw.csv"
ames = pd.read_csv(url)


Working from a local file instead? read_csv() takes a file path just as happily as a URL:

# Absolute path — full address from the root
pd.read_csv("/Users/jane/project/data/ames_raw.csv")

# Relative path — directions from where you are
pd.read_csv("../data/ames_raw.csv")
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.

Importing Data in Google Colab ☁️

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

from google.colab import files

# Opens a file picker — select a file from your computer
uploaded = files.upload()

Then read the uploaded file just like any local file:

ames = pd.read_csv('ames_raw.csv')

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.

Getting to Know Your Data 🧐

Meet Your Data

Here’s what we just imported:

ames
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

Size It Up Yourself

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:

ames.shape       # How big is it?
ames.columns     # What are the columns called?
ames.dtypes      # What type is each column?
ames.head()      # What do the first rows look like?
ames.info()      # Types and missing values, all at once
ames.describe()  # Summary statistics for numeric columns

Tip

Don’t just run them — read the output. Anything surprise you?

Attributes vs Methods: Your Detective Tools

Pop Quiz: Do you notice a difference between these commands?

ames.shape      # No parentheses
ames.columns    # No parentheses  
ames.dtypes     # No parentheses

Attributes = Looking at the “ID card”

  • Basic properties of your data
  • No parentheses needed
ames.head()     # With parentheses
ames.info()     # With parentheses
ames.describe() # With parentheses

Methods = Asking questions

  • Functions that DO something
  • Always need parentheses

Tip

Memory Trick: Methods = Actions = Parentheses!

Why Inspect Before You Analyze

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.

Understanding DataFrames & Series

DataFrames: Your Digital Spreadsheet

A DataFrame is like an Excel spreadsheet in Python:

  • 2D structure (rows × columns)
  • Labeled rows (index)
  • Named columns
  • Each column is a Series
  • Built for data analysis

# Let's look at our Ames data again
ames
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

Quick Challenge! 🎯

Prediction Game: What will each of these return?

Single brackets:

ames['SalePrice']

Double brackets:

ames[['SalePrice']]

Multiple columns:

ames[['SalePrice', 'Year Built']]

Think about:

  • What do [] do?
  • What do you think the output values will be?
  • What do you think the output types will be?

The Answer Revealed!

Same column, three different requests — look at how the output shape changes:

Single brackets → Series


ames['SalePrice'].head(3)
0    215000
1    105000
2    172000
Name: SalePrice, dtype: int64

Double brackets → DataFrame

ames[['SalePrice']].head(3)
SalePrice
0 215000
1 105000
2 172000

Two columns → DataFrame


ames[['SalePrice', 'Year Built']].head(3)
SalePrice Year Built
0 215000 1960
1 105000 1961
2 172000 1958

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.

Why Should You Care?

Rule #1 — always know which one you’re holding.

A Series — one column, with a dtype: footer underneath and no header row:

ames['SalePrice'].head(3)
0    215000
1    105000
2    172000
Name: SalePrice, dtype: int64

Three parts: values, index, and a single dtype.

A DataFrame — a table with a header row, even holding just one column:

ames[['SalePrice']].head(3)
SalePrice
0 215000
1 105000
2 172000

Tip

Luckily, you can tell at a glance!

Why Should You Care?

Rule #2 — they don’t share the same toolkit.

Sometimes the same attribute or method works on both, but hands back different output:

print("Series .shape:   ", ames['SalePrice'].shape)
print("DataFrame .shape:", ames[['SalePrice']].shape)
Series .shape:    (2930,)
DataFrame .shape: (2930, 1)
ames['SalePrice'].mean()      # one number
np.float64(180796.0600682594)
ames[['SalePrice']].mean()    # a Series — note the dtype: footer!
SalePrice    180796.060068
dtype: float64
ames[['SalePrice', 'Year Built']].mean()   
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:

ames['SalePrice'].columns    # ❌ AttributeError — a Series has no columns
ames[['SalePrice']].columns  # ✅ Index(['SalePrice'], dtype='object')

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.

Your Turn! 🎯

Using the ames DataFrame:

  1. Extract the Neighborhood column as a Series. What neighborhood are the first 3 records in?
  2. Extract the Neighborhood column as a DataFrame.
  3. Extract the 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):

ames['Neighborhood'].head(3)
0    NAmes
1    NAmes
2    NAmes
Name: Neighborhood, dtype: str

2 — Wrap that same name in a list to get a DataFrame back:

ames[['Neighborhood']].head(3)
Neighborhood
0 NAmes
1 NAmes
2 NAmes

3 — Keep adding to the list:

ames[['Neighborhood', 'Overall Qual', 'SalePrice']].head(3)
Neighborhood Overall Qual SalePrice
0 NAmes 6 215000
1 NAmes 5 105000
2 NAmes 6 172000

Data Subsetting: Finding What Matters

The Two Dimensions of Subsetting

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”)

Selecting Columns: Pick Your Variables

You already know how to do this!

One column → Series

ames['SalePrice'].head(3)
0    215000
1    105000
2    172000
Name: SalePrice, dtype: int64

Two columns → DataFrame

ames[['SalePrice', 'Year Built']].head(3)
SalePrice Year Built
0 215000 1960
1 105000 1961
2 172000 1958

Three columns → DataFrame

ames[['SalePrice', 'Year Built', 'Neighborhood']].head(3)
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).

Data Mystery #1 🕵️

Challenge: Using our Ames dataset, can you find:

  1. How many houses are in our dataset?
  2. What’s the highest sale price?
  3. What’s the average year built?
  4. How many unique neighborhoods are represented?

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…

Data Mystery #1 Solutions 🎯

Mystery 1: How many houses?

# Check dataset dimensions
print(f"Dataset shape: {ames.shape}")
print(f"Number of houses: {ames.shape[0]}")
Dataset shape: (2930, 82)
Number of houses: 2930

Mystery 2: Highest sale price?

# Find maximum sale price
highest_price = ames['SalePrice'].max()
print(f"Highest sale price: ${highest_price:,}")
Highest sale price: $755,000

Mystery 3: Average year built?

# Calculate average year built
avg_year = ames['Year Built'].mean()
print(f"Average year built: {avg_year:.0f}")
Average year built: 1971

Mystery 4: Unique neighborhoods?

# Count the distinct values in the column
n_hoods = ames['Neighborhood'].nunique()
print(f"Unique neighborhoods: {n_hoods}")
Unique neighborhoods: 28

Tip

Detective Skills Unlocked! 🔓

You now know how to:

  • Check dataset size with .shape
  • Find maximum values with .max()
  • Calculate averages with .mean()
  • Count distinct values with .nunique()

Filtering Rows: The Magic of Conditions

The Process: Ask a yes/no question about each row

Step 1: Create a condition (True/False for each row)

# Which houses sold for more than $200,000?
expensive_houses = ames['SalePrice'] > 200000
expensive_houses
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

# Keep only the True rows
filtered_ames = ames[expensive_houses]
print(f"Original dataset: {ames.shape[0]} houses")
print(f"Expensive houses: {filtered_ames.shape[0]} houses")
filtered_ames.head()
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!

Building More Complex Filters

Real Estate Question: Find houses that are expensive AND recently built

# Step 1: Define our conditions
expensive = ames['SalePrice'] > 200000
recent = ames['Year Built'] > 2000

# Step 2: Combine with & (AND)
expensive_and_recent = expensive & recent

# Step 3: Filter the data
result = ames[expensive_and_recent]
print(f"Expensive AND recent houses: {result.shape[0]}")
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.

The Powerful .loc Accessor

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:

rows = (ames['Year Built'] > 2000) & (ames['SalePrice'] > 200000)
cols = ['Neighborhood', 'SalePrice', 'Gr Liv Area']

recent_expensive = ames.loc[rows, cols]
print(f"Matching homes: {recent_expensive.shape[0]}")
recent_expensive.head(3)
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:

rows = (ames['SalePrice'] < 100000) | (ames['SalePrice'] > 500000)
cols = ['Neighborhood', 'SalePrice', 'Gr Liv Area']

price_extremes = ames.loc[rows, cols]
print(f"Matching homes: {price_extremes.shape[0]}")
price_extremes.head(3)
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.

Data Mystery #2 🕵️‍♀️

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:

  1. Use .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.
  2. How many homes meet that condition?
  3. For those homes: what’s the average sale price, and how many different neighborhoods do they span?

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

Data Mystery #2 Solutions 🎯

Step 1 — filter rows and select columns in one move:

rows = (ames['Year Built'] >= 2000) & (ames['Gr Liv Area'] >= 3000)
cols = ['Neighborhood', 'Year Built', 'Gr Liv Area', 'SalePrice']

new_large = ames.loc[rows, cols]
new_large.head(3)
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?

print(f"Homes built 2000+ at 3,000+ sq ft: {new_large.shape[0]}")
Homes built 2000+ at 3,000+ sq ft: 7

Step 3 — now ask questions of that subset:

avg_price = new_large['SalePrice'].mean()
n_hoods = new_large['Neighborhood'].nunique()

print(f"Average sale price: ${avg_price:,.0f}")
print(f"Spanning {n_hoods} of the {ames['Neighborhood'].nunique()} neighborhoods in Ames")
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.

Common Detective Mistakes 🚨

Avoid these rookie errors:

❌ Forgetting parentheses:

ames.head     # Returns function, not data
ames.head()   # ✅ Returns first 5 rows

❌ Case sensitivity:

ames['saleprice']  # ❌ KeyError!
ames['SalePrice']  # ✅ Correct spelling

❌ Wrong logical operators:

ames[(SalePrice > 200000) and (ames['Year Built'] > 2000)]  # ❌
ames[(ames['SalePrice'] > 200000) & (ames['Year Built'] > 2000)]    # ✅

❌ Confusing brackets:

ames['SalePrice']    # Series
ames[['SalePrice']]  # DataFrame

❌ Forgetting column references:

ames[Year Built > 2000]        # ❌ NameError
ames[ames['Year Built'] > 2000] # ✅ Correct

Tip

Pro Tip: Use .columns to check exact column names!

Putting It All Together

Real-World Detective Work Challenge!

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

# Load the planes dataset
planes_url = "https://raw.githubusercontent.com/bradleyboehmke/uc-bana-7025/main/data/planes.csv"
planes = pd.read_csv(planes_url)

# Take a quick look at the data structure
planes.head()
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…

The Python Solution 🎯

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:

  • Spreadsheet: Manual filtering + manual calculation = prone to errors
  • Python: Automated, reproducible, scalable to millions of rows!

🧾 What You Learned

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:

  1. Know what you’re holding. One name in [ ] gives you a Series; a list in [[ ]] gives you a DataFrame — and they don’t share the same toolkit.
  2. Subset first, then analyze. Narrow to the rows and columns that matter, then ask your questions of that subset.

Thursday’s Lab Preview

Get Ready for Hands-On Detective Work!

This Week’s Lab: Data Detective Training

You’ll practice:

  • Importing multiple datasets
  • Exploring real-world messy data
  • Solving data mysteries with filtering
  • Building detective workflows

Dataset: COVID-19 college data - help universities understand their data!

Come prepared with questions about anything that felt confusing today!

Any Final Questions? 🙋‍♀️

About today’s concepts:

  • Data importing or file paths?
  • DataFrame vs Series confusion?
  • Filtering or selection techniques?
  • About Thursday’s lab?
  • Questions from last week’s homework assignment?
  • Upcoming assignments?