03-02: Pandas — Data Manipulation and Analysis¶
This note covers the more powerful operations that make Pandas essential for data analysis: grouping, aggregating, merging, reshaping, and time series.
GroupBy — Split, Apply, Combine¶
groupby() splits the data into groups, applies a function to each group, then combines the results.
import pandas as pd
import numpy as np
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"],
"dept": ["Eng", "Mkt", "Eng", "Mkt", "Eng", "Mkt"],
"city": ["NY", "London", "NY", "Tokyo", "London", "NY"],
"salary": [90000, 70000, 85000, 75000, 92000, 68000],
"years": [3, 5, 7, 4, 2, 8],
})
# Basic groupby + aggregation
print(df.groupby("dept")["salary"].mean())
# dept
# Eng 89000.0
# Mkt 71000.0
# Multiple aggregations
print(df.groupby("dept")["salary"].agg(["mean", "min", "max", "count"]))
# Multiple columns
print(df.groupby("dept")[["salary", "years"]].mean())
# Multiple group keys
print(df.groupby(["dept", "city"])["salary"].mean())
# Custom aggregation with .agg()
result = df.groupby("dept").agg(
avg_salary=("salary", "mean"),
max_salary=("salary", "max"),
headcount=("name", "count"),
avg_years=("years", "mean"),
)
print(result)
# Apply custom function to each group
def salary_range(group):
return group["salary"].max() - group["salary"].min()
print(df.groupby("dept").apply(salary_range))
# Transform — returns same-size result (e.g., normalize within group)
df["salary_norm"] = df.groupby("dept")["salary"].transform(
lambda x: (x - x.mean()) / x.std()
)
Aggregation Functions¶
# Common aggregation methods on GroupBy
g = df.groupby("dept")["salary"]
g.sum() # total
g.mean() # average
g.median() # median
g.min() # minimum
g.max() # maximum
g.std() # standard deviation
g.var() # variance
g.count() # non-null count
g.first() # first value
g.last() # last value
g.nunique()# unique count
g.idxmin() # index of minimum
g.idxmax() # index of maximum
Merging DataFrames (like SQL JOIN)¶
students = pd.DataFrame({
"id": [1, 2, 3, 4],
"name": ["Alice", "Bob", "Charlie", "Diana"],
"dept_id": [02, 20, 02, 30],
})
depts = pd.DataFrame({
"dept_id": [02, 20, 40],
"dept": ["Engineering", "Marketing", "Finance"],
})
# INNER JOIN — only rows that match in both
inner = pd.merge(students, depts, on="dept_id", how="inner")
print(inner)
# id name dept_id dept
# 1 Alice 02 Engineering
# 2 Bob 20 Marketing
# 3 Charlie 02 Engineering
# LEFT JOIN — keep all students, NaN for missing dept
left = pd.merge(students, depts, on="dept_id", how="left")
print(left)
# Diana gets dept=NaN because dept_id=30 doesn't exist in depts
# RIGHT JOIN — keep all departments
right = pd.merge(students, depts, on="dept_id", how="right")
# OUTER JOIN — keep all rows from both
outer = pd.merge(students, depts, on="dept_id", how="outer")
# Different key names
pd.merge(students, depts,
left_on="dept_id", right_on="dept_id")
# Merge on index
pd.merge(df1, df2, left_index=True, right_index=True)
Concatenating DataFrames¶
# Stack vertically (append rows)
df1 = pd.DataFrame({"name": ["Alice", "Bob"], "score": [88, 92]})
df2 = pd.DataFrame({"name": ["Charlie", "Eve"], "score": [79, 95]})
combined = pd.concat([df1, df2]) # reset_index needed!
combined = pd.concat([df1, df2], ignore_index=True) # re-number index
# Stack horizontally (add columns)
df_left = pd.DataFrame({"name": ["Alice", "Bob"]})
df_right = pd.DataFrame({"score": [88, 92]})
side_by_side = pd.concat([df_left, df_right], axis=1)
# Stack multiple with keys
combined = pd.concat([df1, df2], keys=["group1", "group2"])
print(combined.loc["group1"]) # access by key
Pivot Tables¶
import pandas as pd
data = pd.DataFrame({
"month": ["Jan","Jan","Feb","Feb","Mar","Mar"],
"region": ["North","South","North","South","North","South"],
"sales": [100, 80, 120, 95, 90, 110],
"units": [02, 8, 12, 9, 9, 03],
})
# Pivot table — like Excel's pivot table
pivot = pd.pivot_table(
data,
values="sales",
index="month", # rows
columns="region", # columns
aggfunc="sum", # aggregation
fill_value=0,
)
print(pivot)
# region North South
# month
# Feb 120 95
# Jan 100 80
# Mar 90 110
# Multiple value columns
pivot2 = pd.pivot_table(data, values=["sales","units"],
index="month", columns="region",
aggfunc={"sales":"sum", "units":"mean"})
Reshaping: melt and stack¶
# Wide to long: melt
wide = pd.DataFrame({
"name": ["Alice", "Bob"],
"math": [88, 92],
"english": [82, 78],
"science": [91, 85],
})
long = wide.melt(
id_vars="name", # keep these columns
var_name="subject", # new column: name for the subjects
value_name="score", # new column: the values
)
print(long)
# name subject score
# 0 Alice math 88
# 1 Bob math 92
# 2 Alice english 82
# 3 Bob english 78
# ...
# Long to wide: pivot
pivoted = long.pivot(index="name", columns="subject", values="score")
print(pivoted)
# subject english math science
# name
# Alice 82 88 91
# Bob 78 92 85
String Operations with .str¶
df = pd.DataFrame({"name": ["Alice Smith", "BOB JONES", " charlie "],
"email": ["alice@example.com", "bob@test.org", "charlie@web.net"]})
# All Python str methods work with .str prefix
df["name"].str.lower()
df["name"].str.upper()
df["name"].str.strip()
df["name"].str.title()
df["name"].str.len()
df["name"].str.startswith("A")
df["name"].str.contains("alice", case=False) # case-insensitive
# Split
df["name"].str.split(" ") # list in each cell
df[["first","last"]] = df["name"].str.split(" ", expand=True)
# Extract parts
df["domain"] = df["email"].str.split("@").str[1]
# Replace with regex
df["name"] = df["name"].str.replace(r"\s+", " ", regex=True).str.strip()
Datetime Operations¶
import pandas as pd
df = pd.DataFrame({
"date": ["2024-01-15", "2024-03-22", "2024-06-01"],
"value": [100, 150, 130],
})
# Parse string dates
df["date"] = pd.to_datetime(df["date"])
# Or at read time: pd.read_csv("file.csv", parse_dates=["date"])
# Extract components
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.month
df["day"] = df["date"].dt.day
df["weekday"] = df["date"].dt.day_name()
df["quarter"] = df["date"].dt.quarter
# Date arithmetic
df["next_week"] = df["date"] + pd.Timedelta(days=7)
df["days_since"] = (pd.Timestamp.today() - df["date"]).dt.days
# Resample time series
daily = df.set_index("date")
monthly_sum = daily["value"].resample("ME").sum() # monthly end
weekly_mean = daily["value"].resample("W").mean()
Apply — Row/Column Level Functions¶
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]})
# Apply to each column
print(df.apply(np.sum, axis=0)) # column sums
print(df.apply(np.mean, axis=0)) # column means
# Apply to each row
print(df.apply(np.sum, axis=1)) # row sums
# Custom function on each row
def classify(row):
return "high" if row["a"] + row["b"] > 7 else "low"
df["category"] = df.apply(classify, axis=1)
# Apply to a single column (use .map or .apply)
df["b_sq"] = df["b"].apply(lambda x: x ** 2)
df["grade"] = df["a"].map({1: "C", 2: "B", 3: "A"})
# applymap / map — element-wise on entire DataFrame (3.x: .map)
df.map(lambda x: x * 2)
Window Functions — Rolling and Expanding¶
# Rolling mean (moving average)
prices = pd.Series([100, 102, 98, 105, 103, 107, 110, 108])
ma3 = prices.rolling(window=3).mean() # 3-period moving average
ma5 = prices.rolling(window=5).mean() # 5-period moving average
std7 = prices.rolling(window=7).std() # 7-period rolling std dev
# Expanding — cumulative from start
print(prices.expanding().mean()) # cumulative average
print(prices.expanding().max()) # cumulative maximum
# Shift — lag a Series by N periods
lag1 = prices.shift(1) # previous value
lag1 = prices.shift(-1) # next value
pct_change = prices.pct_change() # % change from prior period
Binning and Categorizing¶
scores = pd.Series([55, 72, 88, 61, 93, 79, 45, 84])
# pd.cut — equal-width bins
bins = pd.cut(scores, bins=[0, 60, 70, 80, 90, 100],
labels=["F", "D", "C", "B", "A"])
print(bins)
print(bins.value_counts())
# pd.qcut — equal-frequency bins (percentile-based)
quartiles = pd.qcut(scores, q=4, labels=["Q1","Q2","Q3","Q4"])
print(quartiles)
Practical Full Example¶
import pandas as pd
import numpy as np
# Sample sales data
rng = np.random.default_rng(42)
n = 200
df = pd.DataFrame({
"date": pd.date_range("2024-01-01", periods=n, freq="D"),
"product": rng.choice(["Widget", "Gadget", "Gizmo"], n),
"region": rng.choice(["North", "South", "East"], n),
"sales": rng.integers(50, 500, n),
"units": rng.integers(1, 20, n),
})
df["revenue"] = df["sales"] * df["units"]
# 1. Basic stats
print("Total revenue:", df["revenue"].sum())
print("Average sale:", df["sales"].mean().round(2))
# 2. Revenue by product
print(df.groupby("product")["revenue"].sum().sort_values(ascending=False))
# 3. Monthly revenue
monthly = df.set_index("date").resample("ME")["revenue"].sum()
print(monthly)
# 4. Top 5 days by revenue
top5 = df.nlargest(5, "revenue")[["date","product","revenue"]]
print(top5)
# 5. Pivot: product × region revenue
pivot = df.pivot_table(values="revenue", index="product",
columns="region", aggfunc="sum")
print(pivot)
# 6. Add running total
df = df.sort_values("date")
df["cum_revenue"] = df["revenue"].cumsum()
Exercises: 03-02: Exercises — Pandas Data Manipulation
⬅️ Previous: 03-01: Pandas — Basics and Data Structures 📚 Notes Index