Pandas Intermediate¶
When you'd use this
DataFrames, Series, groupby, merge, pivot, window functions and data manipulation.
Load, clean, filter, group, and reshape tabular data — the everyday tool for data analysis, ETL, and preparing datasets for ML.
DataFrame basics¶
Create and load tabular data from dicts, CSV, JSON, or SQL — the DataFrame is the core object.
import pandas as pd
import numpy as np
# ─── Creating DataFrames ─────────────────────────
# From dict
df = pd.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"age": [30, 25, 35, 28, 32],
"city": ["NYC", "LA", "NYC", "Chicago", "LA"],
"salary": [85000, 72000, 95000, 68000, 91000],
})
print(df)
# name age city salary
# 0 Alice 30 NYC 85000
# 1 Bob 25 LA 72000
# 2 Charlie 35 NYC 95000
# 3 Diana 28 Chicago 68000
# 4 Eve 32 LA 91000
# From CSV
df = pd.read_csv("data.csv")
df = pd.read_csv("data.csv", parse_dates=["date_col"], index_col="id")
# From JSON
df = pd.read_json("data.json")
# From SQL
import sqlite3
conn = sqlite3.connect("app.db")
df = pd.read_sql("SELECT * FROM users", conn)
Inspecting data¶
Get a quick feel for a dataset — shape, types, summary stats, and null counts.
print(df.shape) # (5, 4) — rows, columns
print(df.dtypes) # data types per column
print(df.info()) # summary with null counts
print(df.describe()) # statistics for numeric columns
print(df.head(3)) # first 3 rows
print(df.tail(3)) # last 3 rows
print(df.columns) # Index(['name', 'age', 'city', 'salary'])
print(df.index) # RangeIndex(start=0, stop=5, step=1)
print(df.nunique()) # unique values per column
print(df.isnull().sum()) # null count per column
Selecting data¶
Pick columns and rows by label (loc) or position (iloc) — the foundation of analysis.
# ─── Column selection ─────────────────────────────
df["name"] # Series
df[["name", "age"]] # DataFrame (multiple columns)
# ─── Row selection ────────────────────────────────
df.iloc[0] # first row by position
df.iloc[1:4] # rows 1,2,3 by position
df.iloc[0, 2] # row 0, column 2
df.loc[0] # row with index label 0
df.loc[0, "name"] # specific cell
df.loc[0:2, "name":"city"] # slice by label (inclusive!)
# ─── Boolean filtering ────────────────────────────
df[df["age"] > 30] # age > 30
df[df["city"] == "NYC"] # city is NYC
df[(df["age"] > 25) & (df["salary"] > 80000)] # combined (use & not 'and')
df[df["city"].isin(["NYC", "LA"])] # city in list
df[df["name"].str.contains("li")] # string contains
# ─── query() — SQL-like filtering ─────────────────
df.query("age > 30 and city == 'NYC'")
df.query("salary > @threshold") # use variables with @
Modifying data¶
Add/rename/drop columns, sort, and set conditional values to shape a dataset.
# ─── Add/modify columns ──────────────────────────
df["bonus"] = df["salary"] * 0.1
df["full_name"] = df["name"] + " Smith"
df["age_group"] = pd.cut(df["age"], bins=[20, 30, 40], labels=["20s", "30s"])
# Conditional assignment
df["senior"] = np.where(df["age"] >= 30, True, False)
df.loc[df["city"] == "NYC", "region"] = "East"
# ─── Rename columns ──────────────────────────────
df = df.rename(columns={"name": "full_name", "city": "location"})
df.columns = df.columns.str.lower() # all lowercase
# ─── Drop columns/rows ───────────────────────────
df = df.drop(columns=["bonus"])
df = df.drop(index=[0, 1]) # drop rows by index
# ─── Sorting ─────────────────────────────────────
df = df.sort_values("salary", ascending=False)
df = df.sort_values(["city", "age"]) # multi-column sort
# ─── Reset index ─────────────────────────────────
df = df.reset_index(drop=True)
GroupBy — split-apply-combine¶
Aggregate by category — the single most useful analysis operation in pandas.
# ─── Basic groupby ────────────────────────────────
grouped = df.groupby("city")
# Aggregation
print(grouped["salary"].mean())
# city
# Chicago 68000.0
# LA 81500.0
# NYC 90000.0
# Multiple aggregations
stats = grouped["salary"].agg(["mean", "median", "std", "count"])
print(stats)
# Named aggregation (clearest syntax)
result = df.groupby("city").agg(
avg_salary=("salary", "mean"),
max_salary=("salary", "max"),
headcount=("name", "count"),
avg_age=("age", "mean"),
)
print(result)
# ─── Transform — return same-shape result ─────────
# Normalize salary within each city
df["salary_z"] = df.groupby("city")["salary"].transform(
lambda x: (x - x.mean()) / x.std()
)
# ─── Filter — keep/drop groups ────────────────────
# Keep only cities with more than 1 person
df_filtered = df.groupby("city").filter(lambda g: len(g) > 1)
# ─── Apply — arbitrary function per group ─────────
def top_earner(group):
return group.nlargest(1, "salary")
result = df.groupby("city").apply(top_earner).reset_index(drop=True)
Merge / Join¶
Combine DataFrames on keys like SQL joins — for linking related tables.
# ─── merge (SQL-style join) ───────────────────────
orders = pd.DataFrame({
"order_id": [1, 2, 3, 4],
"user_id": [101, 102, 101, 103],
"amount": [50, 30, 75, 20],
})
users = pd.DataFrame({
"user_id": [101, 102, 104],
"name": ["Alice", "Bob", "Diana"],
})
# Inner join (default) — only matching rows
inner = orders.merge(users, on="user_id")
# Left join — keep all left rows
left = orders.merge(users, on="user_id", how="left")
# Outer join — keep all rows from both
outer = orders.merge(users, on="user_id", how="outer")
# Different column names
orders.merge(users, left_on="user_id", right_on="user_id")
# ─── concat — stack DataFrames ────────────────────
df1 = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
df2 = pd.DataFrame({"a": [5, 6], "b": [7, 8]})
vertical = pd.concat([df1, df2], ignore_index=True) # stack vertically
horizontal = pd.concat([df1, df2], axis=1) # side by side
Pivot tables and reshaping¶
Reshape between wide and long form and summarize into matrices.
# ─── pivot_table — aggregate and reshape ──────────
sales = pd.DataFrame({
"date": ["Mon", "Mon", "Tue", "Tue", "Wed", "Wed"],
"product": ["A", "B", "A", "B", "A", "B"],
"revenue": [100, 150, 120, 130, 110, 140],
})
pivot = sales.pivot_table(
values="revenue",
index="date",
columns="product",
aggfunc="sum",
)
print(pivot)
# product A B
# date
# Mon 100 150
# Tue 120 130
# Wed 110 140
# ─── melt — wide to long ─────────────────────────
long = pivot.reset_index().melt(id_vars="date", var_name="product", value_name="revenue")
# ─── crosstab ────────────────────────────────────
pd.crosstab(df["city"], df["senior"])
Window functions (rolling, expanding)¶
Compute moving averages, running totals, and lags over ordered/time data.
# Time series data
dates = pd.date_range("2026-01-01", periods=30)
ts = pd.DataFrame({
"date": dates,
"value": np.random.randn(30).cumsum() + 100,
})
ts = ts.set_index("date")
# ─── Rolling window ──────────────────────────────
ts["ma_7"] = ts["value"].rolling(window=7).mean() # 7-day moving average
ts["ma_14"] = ts["value"].rolling(window=14).mean() # 14-day
ts["std_7"] = ts["value"].rolling(window=7).std() # rolling std
# ─── Expanding (cumulative) ───────────────────────
ts["cummax"] = ts["value"].expanding().max()
ts["cummin"] = ts["value"].expanding().min()
# ─── Shift (lag/lead) ────────────────────────────
ts["prev_day"] = ts["value"].shift(1) # yesterday's value
ts["next_day"] = ts["value"].shift(-1) # tomorrow's value
ts["daily_change"] = ts["value"].diff() # day-over-day change
ts["pct_change"] = ts["value"].pct_change() # percentage change
# ─── Rank ─────────────────────────────────────────
df["salary_rank"] = df["salary"].rank(ascending=False)
df["salary_rank_by_city"] = df.groupby("city")["salary"].rank(ascending=False)
Handling missing data¶
Detect, drop, fill, or interpolate NaNs before analysis or modeling.
# Check for nulls
print(df.isnull().sum())
print(df.isna().any())
# Drop rows with any null
df_clean = df.dropna()
df_clean = df.dropna(subset=["salary", "age"]) # only check these columns
# Fill missing values
df["salary"] = df["salary"].fillna(df["salary"].median())
df["city"] = df["city"].fillna("Unknown")
df["age"] = df["age"].ffill() # forward fill (fillna(method=...) was removed)
# Interpolation (for time series)
df["value"] = df["value"].interpolate(method="linear")
String operations¶
Vectorized text methods via .str for cleaning and extracting from text columns.
# .str accessor for vectorized string operations
df["name_lower"] = df["name"].str.lower()
df["name_len"] = df["name"].str.len()
df["first_char"] = df["name"].str[0]
df["has_a"] = df["name"].str.contains("a", case=False)
df["name_parts"] = df["name"].str.split(" ")
df["domain"] = df["email"].str.extract(r"@(.+)")
df["city_clean"] = df["city"].str.strip().str.title()
Performance optimization¶
Use categoricals, vectorization, and column selection to handle large datasets.
# 1. Use appropriate dtypes
df["category_col"] = df["category_col"].astype("category") # saves memory
df["id"] = df["id"].astype("int32") # use smaller int
# 2. Vectorized operations (avoid apply when possible)
# BAD (slow)
df["result"] = df["col"].apply(lambda x: x**2 + 1)
# GOOD (fast)
df["result"] = df["col"]**2 + 1
# 3. Use query() for complex filters (faster for large DataFrames)
df.query("age > 30 and salary > 80000")
# 4. Read only needed columns
df = pd.read_csv("huge.csv", usecols=["name", "age", "salary"])
# 5. Process in chunks
for chunk in pd.read_csv("huge.csv", chunksize=10000):
process(chunk)
Saving data¶
Write results to CSV, Parquet, Excel, JSON, or SQL for downstream use.
# CSV
df.to_csv("output.csv", index=False)
# Excel
df.to_excel("output.xlsx", sheet_name="Sheet1", index=False)
# JSON
df.to_json("output.json", orient="records", indent=2)
# Parquet (fast, compressed — best for large data)
df.to_parquet("output.parquet")
df = pd.read_parquet("output.parquet")
# SQL
df.to_sql("users", conn, if_exists="replace", index=False)
Practice Exercises¶
- Load a CSV with 100K+ rows, inspect it, handle missing values and duplicates.
- GroupBy + Agg: Find the top 5 categories by average revenue.
- Merge 3 tables (users, orders, products) and compute total spend per user.
- Time series: Compute 7-day and 30-day moving averages on stock prices.
- Pivot table: Transform transaction data into a monthly revenue by product matrix.
- Performance: Compare the speed of
.apply()vs vectorized operations on 1M rows. - Clean a messy dataset: Parse dates, fix typos in categorical columns, handle outliers.
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.