Module 01 Beginner 22 min read

Pandas for Data Science

DataFrames, Series, GroupBy, and Pivot Tables — the complete toolkit for tabular data wrangling.

Updated 2025 · Edit on GitHub

Why Pandas?

Real-world data is messy, heterogeneous, and labelled. NumPy works on uniform arrays; Pandas adds column names, row labels, mixed types, and a rich API for data wrangling — the day-to-day reality of ML engineering.

Core Data Structures

Series1-D labelled array. Like a NumPy array with an index. Think: one column of a spreadsheet.
DataFrame2-D labelled table of Series. Think: the spreadsheet itself. Columns can have different dtypes.
Python
import pandas as pd
import numpy as np

# ── Series
s = pd.Series([10, 20, 30, 40], index=["a","b","c","d"])
print(s["b"])          # 20  (label-based)
print(s.iloc[1])       # 20  (position-based)
print(s[s > 15])       # b:20  c:30  d:40

# ── DataFrame — from dict
df = pd.DataFrame({
    "name":    ["Alice", "Bob",  "Carol", "Dan"],
    "age":     [28,       35,    22,       41  ],
    "salary":  [75000,    92000, 58000,    110000],
    "dept":    ["Eng",    "Mkt", "Eng",    "Eng" ],
})

# Inspect
print(df.shape)         # (4, 4)
print(df.dtypes)        # object, int64, int64, object
print(df.head(2))       # first 2 rows
print(df.describe())    # count, mean, std, min, quartiles, max
print(df.info())        # dtypes + non-null counts

Selection & Filtering

Python
# Column selection
df["age"]                    # Series
df[["name","salary"]]        # DataFrame

# Row selection
df.loc[0]                    # row with label 0
df.loc[1:2, "name":"salary"] # rows 1-2, cols name to salary (inclusive)
df.iloc[0:2, 0:3]            # positional: rows 0-1, cols 0-2

# Boolean filtering
senior    = df[df["age"] > 30]
eng_high  = df[(df["dept"] == "Eng") & (df["salary"] > 70000)]
top3      = df.nlargest(3, "salary")

# Assign a new column
df["seniority"] = df["age"].apply(lambda x: "senior" if x >= 35 else "junior")
df["bonus"]     = df["salary"] * 0.1          # vectorised

GroupBy: Split-Apply-Combine

The most powerful Pandas pattern. Split the data into groups, apply an aggregation function, combine the results.

Python
import pandas as pd

# Avg salary & headcount per department
dept_stats = df.groupby("dept")["salary"].agg(["mean","count","std"])
print(dept_stats)

# Multiple columns
summary = df.groupby("dept").agg(
    avg_salary  = ("salary", "mean"),
    max_salary  = ("salary", "max"),
    avg_age     = ("age",    "mean"),
    headcount   = ("name",   "count"),
)

# Custom aggregation
dept_salary_range = df.groupby("dept")["salary"].agg(
    lambda x: x.max() - x.min()
)

# Transform: broadcast group-level stats back to original rows
df["dept_avg_salary"] = df.groupby("dept")["salary"].transform("mean")

Pivot Tables

Python
import pandas as pd, numpy as np

sales = pd.DataFrame({
    "month":    ["Jan","Jan","Feb","Feb","Mar","Mar"],
    "product":  ["A",  "B",  "A",  "B",  "A",  "B"],
    "revenue":  [1200, 800, 1500, 950, 1100, 1300],
})

# Pivot: rows=month, cols=product, values=revenue
pivot = sales.pivot_table(
    values  = "revenue",
    index   = "month",
    columns = "product",
    aggfunc = "sum",        # default is mean
    fill_value = 0          # fill NaN with 0
)
print(pivot)

# Reverse: melt wide → long format (common for plotting)
melted = pivot.reset_index().melt(id_vars="month", var_name="product", value_name="revenue")

Reading & Writing Data

Python
import pandas as pd

# CSV
df = pd.read_csv("data.csv", parse_dates=["date"], index_col="id")
df.to_csv("output.csv", index=False)

# Excel
df = pd.read_excel("data.xlsx", sheet_name="Sheet1")
df.to_excel("output.xlsx", index=False)

# Parquet (fast, compressed — preferred for large datasets)
df = pd.read_parquet("data.parquet")
df.to_parquet("output.parquet", index=False)

# From a URL
df = pd.read_csv("https://raw.githubusercontent.com/mwaskom/seaborn-data/master/titanic.csv")

Performance Tips

Use vectorised opsPrefer df["col"] * 2 over df["col"].apply(lambda x: x*2). The former is C-level; the latter is Python-level.
Use category dtypeConvert low-cardinality string columns: df["dept"] = df["dept"].astype("category"). Cuts memory 5–10× for large DataFrames.
Avoid iterrows()iterrows() is 100× slower than vectorised operations. Almost always replaceable with .apply(), .transform(), or direct array ops.
Read with dtypesPass dtype= to read_csv() to avoid loading everything as float64, saving memory.

Summary

  • DataFrame: 2-D labelled table. Series: 1-D labelled array.
  • Use .loc[] for label-based, .iloc[] for positional selection.
  • GroupBy: split → apply aggregation → combine. The workhorse of data analysis.
  • Pivot tables: reshape data from long to wide format (and back with melt()).
  • Use category dtype, vectorised ops, and Parquet for performance.