Pandas Fundamentals — Series & DataFrame

Data Science Fundamentals

Chapter 3 · Pandas Fundamentals — Series & DataFrame

ds1-2 built the ndarray. This chapter delivers the second row of ds1-1's own stack roadmap: pandas, the library this entire course's own idea of "a dataset" is actually built around. Every remaining chapter through ds1-9 works with the two structures introduced here.

Series — A Labeled ndarray

A pandas Series is, structurally, exactly what ds1-2's own ndarray already was, plus one addition: a labeled index alongside the values.

import pandas as pd

s = pd.Series([120, 340, 95, 410], index=["Mon", "Tue", "Wed", "Thu"])
s["Tue"]      # 340 — access by label
s.values      # array([120, 340, 95, 410]) — the underlying ndarray, unchanged from ds1-2

That last line is worth pausing on: .values hands back a genuine NumPy ndarray. A Series isn't a new kind of data underneath — it's an ndarray with labels attached on top.

DataFrame — A Table of Aligned Series

A DataFrame extends the same idea to two dimensions: a table where each column is its own Series, and every column shares the same row index. ds1-1's own throughline promised this explicitly — a DataFrame is a labeled wrapper around an ndarray, not a separate structure invented from scratch.

df = pd.DataFrame({
    "product": ["Latte", "Espresso", "Latte", "Mocha"],
    "quantity": [12, 8, 15, 6],
    "revenue": [54.00, 24.00, 67.50, 30.00],
})
df.values     # a 2D ndarray underneath — same relationship as Series.values above

Reading Real Data — CSV and JSON

py2-7 covered Python's own json module — json.load() parsing a file into nested dicts and lists by hand, followed by manually walking that structure to pull out the fields you actually want. At dataset scale, pandas replaces that manual walk with a single call:

df = pd.read_csv("sales.csv")
df = pd.read_json("sales.json")   # the py2-7 json.load() step, plus the table-construction step, in one call
Not a replacement for py2-7 — a higher layer built on top of it
pd.read_json() is doing the exact same underlying parsing py2-7's own json.load() does — it's just also handling the "now build a table from this nested structure" step that would otherwise be written by hand. Knowing what json.load() actually does underneath makes it much easier to understand why read_json() sometimes needs extra arguments (orient=, for deeply nested JSON) to know how to flatten a structure that isn't already table-shaped.

.loc vs. .iloc — A Genuinely Common Beginner Confusion

Both select rows and columns. The difference is what they select by:

Selects byExample
.locLabel (the index value itself)df.loc[2] — the row labeled 2, even after sorting or filtering has reordered rows
.ilocInteger position (0-based, like a list)df.iloc[2] — the third row physically, regardless of what its label happens to be
Why this distinction actually bites people
For a fresh DataFrame with a default 0, 1, 2, ... index, .loc[2] and .iloc[2] happen to return the same row — which hides the difference until, later, some rows get filtered or sorted and the index labels no longer line up with position. At that point .loc[2] and .iloc[2] can silently return two completely different rows. Preferring .loc for label-based work and .iloc only when position genuinely is what's meant avoids this trap entirely.

Boolean Filtering — ds1-2's Own Mechanism, One Layer Up

df[df["quantity"] > 10]   # only rows where quantity exceeds 10

This is ds1-2's own boolean masking, applied to whole rows instead of individual array elements — df["quantity"] > 10 produces a Series of True/False values (one per row), and indexing the DataFrame with it keeps only the matching rows. Exactly the pattern ds1-2's own warn-box flagged as worth remembering.

A First Look, Column Selection & Derived Columns

df.head()      # first 5 rows — a quick look
df.info()      # column types, non-null counts
df.describe()  # summary statistics — previews ds1-6

df["revenue"]                 # select one column (a Series)
df["avg_price"] = df["revenue"] / df["quantity"]   # add a computed column

This Course's Own Running Dataset

Picking up ds1-1's own retail example directly: a small daily sales log for a chain of coffee shops. This exact table — messy details included — is what ds1-4 spends its own chapter cleaning up, so nothing here needs fixing yet.

order_idstore_iddateproductquantityrevenue
1001S12026-07-10Latte1254.00
1002S22026-07-10Espresso824.00
1003NaN2026-07-10Latte1567.50
1004S107/11/2026Mocha630.00
1005S22026-07-11latte1045.00
1005S22026-07-11latte1045.00

Four real, distinct problems, deliberately left visible here rather than fixed on sight: a missing store_id, an inconsistent date format on row 1004, an inconsistent product-name capitalization on row 1005, and a fully duplicated row at the end. ds1-4 addresses each one by name.

Hands-On Exercises

Exercise 1

Explain, using this chapter's own .values examples, why a DataFrame is described as "a labeled wrapper around an ndarray" rather than a genuinely new kind of data structure.

📄 View solution
Exercise 2

Using this chapter's own warn-box, explain why .loc[2] and .iloc[2] can return the same row on a fresh DataFrame but different rows later, and explain the general rule for when to prefer one over the other.

📄 View solution
Exercise 3

List the four distinct problems this chapter's own sample dataset deliberately leaves unfixed, and explain why leaving them visible here (rather than fixing them immediately) serves this course's own stated teaching approach.

📄 View solution

Chapter 3 Quick Reference

  • Series — a labeled ndarray · DataFrame — a table of aligned Series, both backed by ds1-2's own ndarray underneath (.values)
  • pd.read_csv() / pd.read_json() — the py2-7 json.load() step plus table construction, in one call
  • .loc selects by label, .iloc by integer position — they only look interchangeable on a fresh, unsorted index
  • Boolean filtering (df[df["col"] > x]) — ds1-2's own masking mechanism, applied to whole rows
  • This chapter's own sample dataset carries forward, messy details intact, into ds1-4
  • Next chapter: Data Cleaning