Lesson 14 of 16 · Level 4: Expert

Python for Data Analysis with pandas

  • ⏱ 11 min read
  • ❓ 4 review questions
  • ✍️ Exercise with solution
  • 📥 Downloadable template
In this lesson
  1. Set up in two minutes
  2. Step 1: Load and inspect
  3. Step 2: Clean in five lines
  4. Step 3: Describe and group
  5. Step 4: Filter rows
  6. Step 5: Pivot tables and dates
  7. Step 6: Merge tables (the JOIN)
  8. Step 7: Chart and export
  9. Excel, SQL and pandas side by side
  10. Common pandas mistakes
  11. Summary
What you'll learn
  • Why analysts add Python and pandas to their toolkit, and how to start for free in the browser.
  • How to load and inspect a CSV: shape, first rows, missing values and duplicates.
  • How to clean the raw practice data in a few reproducible lines of code.
  • How to filter, group, pivot and merge, the pandas versions of WHERE, GROUP BY, pivot tables and JOIN.
  • How to chart and export your results, and avoid the most common pandas errors.
📥 Practice this lesson

Upload these two files to Google Colab or your Jupyter folder. Every code block below was run on them, and the outputs shown are the real outputs.

pandas is the most widely used Python library for working with tables of data. Think of it as a spreadsheet you control with code: every step is written down, so you can rerun the whole analysis on next month's file in seconds, share it, and handle millions of rows that would freeze Excel. You don't need to be a programmer to use it. In this lesson you'll repeat what you did in Lessons 5 to 9 (cleaning, statistics, pivots and joins) on the practice data, with code you can copy and run.

  1. Load
  2. Inspect
  3. Clean
  4. Analyze
  5. Chart and export

Set up in two minutes

The easiest start is Google Colab: a free notebook that runs Python in your browser with pandas already installed. Sign in with a Google account, create a new notebook, click the folder icon on the left and upload the two CSV files. If you prefer working offline, install the free Anaconda distribution and use Jupyter Notebook, which works the same way.

A notebook is made of cells. Type code in a cell and press Shift+Enter to run it; the output appears right below. The code in this lesson was tested with pandas 3.0 and also works with pandas 2.x.

Step 1: Load and inspect

import pandas as pd

df = pd.read_csv("sales-practice-data.csv")
print(df.shape)
df.head(3)
(203, 10)
order_idorder_dateregionstateproductcategorychannelunitsunit_pricerevenue
0SO-100012025-01-01NortheastPennsylvaniaOffice ChairOfficeRetail2249498.0
1SO-100022025-01-07NortheastNew YorkMonitor StandOfficeWholesale24451080.0
2SO-100032025-01-09MidwestMichiganWireless EarbudsElectronicsOnline279158.0

df is a DataFrame, pandas' name for a table. shape gives (rows, columns): 203 rows, 10 columns. The numbers in the first column (shown in bold in a notebook) are the index, pandas' row labels. Notice that revenue shows decimals (498.0): pandas stores a numeric column with missing values as decimals, a first hint that something is blank.

Now profile it, exactly like Lesson 5:

print(df.isna().sum()["revenue"])   # blank revenues
print(df.duplicated().sum())        # fully duplicated rows
print(df["state"].nunique())        # distinct state spellings
4
3
24

Four missing revenues, three duplicate rows and 24 state spellings for 17 real states. df.info() and df.describe() are two more inspection commands to run on every new dataset: the first lists column types and non-missing counts, the second gives summary statistics for numeric columns.

1. df.duplicated().sum() returns 3. What does that mean?

  • Three columns have duplicate names
  • Three rows are exact copies of an earlier row
  • Three order IDs appear more than three times

duplicated() marks each row that's identical (in every column) to a row above it; summing the True values counts them. The first occurrence isn't counted, so 3 means three extra copies to remove.

Step 2: Clean in five lines

df = df.drop_duplicates().reset_index(drop=True)
df["order_date"] = pd.to_datetime(df["order_date"], format="mixed")
state_fix = {"CA": "California", "TX": "Texas", "NY": "New York",
             "N. Carolina": "North Carolina"}
df["state"] = df["state"].str.strip().replace(state_fix).str.title()
df["revenue"] = df["revenue"].fillna(df["units"] * df["unit_price"]).astype(int)

print(df.shape)
print(df["state"].nunique())
print(df["revenue"].sum())
(200, 10)
17
89735

Line by line: drop exact duplicates and renumber the index; convert dates, allowing mixed formats (the one "10/15/2025"); trim spaces, map abbreviations and capitalize state names; fill missing revenue with units × price and store it as whole numbers. The result matches the clean answer key exactly: 200 orders, 17 states, $89,735. Next month, the same five lines clean the new file instantly. That's the real advantage over doing it by hand.

Without format="mixed", the date line fails

pd.to_datetime(df["order_date"]) guesses the format from the first value (YYYY-MM-DD) and stops with an error at the first value that doesn't match: time data "10/15/2025" doesn't match format "%Y-%m-%d". That error is useful: it tells you exactly which value is inconsistent. format="mixed" requires pandas 2.0 or later.

Step 3: Describe and group

print(df["revenue"].describe().round(2))
count     200.00
mean      448.68
std       794.93
min        35.00
25%        79.00
50%       138.00
75%       398.00
max      5229.00
Name: revenue, dtype: float64

One line gives the statistics from Lesson 6: mean $448.68, median $138, standard deviation $794.93 (pandas uses the sample version, like STDEV.S). Now group by region, the pandas version of a pivot table or GROUP BY:

by_region = (df.groupby("region")
               .agg(orders=("order_id", "count"),
                    revenue=("revenue", "sum"),
                    avg_order=("revenue", "mean"))
               .sort_values("revenue", ascending=False)
               .round(2))
print(by_region)
           orders  revenue  avg_order
region
West           64    36626     572.28
Northeast      45    20786     461.91
South          59    18684     316.68
Midwest        32    13639     426.22

Each item in agg reads as new_column=("source column", "function"). The same numbers you got with SUMIFS and SQL, which is exactly the point: three tools, one answer.

Step 4: Filter rows

west_wh = df[(df["region"] == "West") & (df["channel"] == "Wholesale")]
print(len(west_wh))
print(west_wh.nlargest(3, "revenue")[["order_id", "state", "product", "revenue"]])
14
     order_id       state       product  revenue
185  SO-10186     Arizona  Office Chair     5229
45   SO-10046  Washington   Smart Watch     4776
47   SO-10048  California   Smart Watch     4378

Inside the brackets goes a True/False condition for each row. Combine conditions with & (and) or | (or), and wrap each condition in parentheses. The numbers on the left are the rows' index labels, kept from the full table.

2. Which line correctly selects wholesale orders from the West?

  • df[df["region"] == "West" and df["channel"] == "Wholesale"]
  • df[df["region"] == "West" & df["channel"] == "Wholesale"]
  • df[(df["region"] == "West") & (df["channel"] == "Wholesale")]

pandas needs & (not Python's and) to combine conditions row by row, and the parentheses are required because & is evaluated before ==. Without them you get a TypeError.

Step 5: Pivot tables and dates

pivot = pd.pivot_table(df, index="region", columns="channel",
                       values="revenue", aggfunc="sum",
                       margins=True, margins_name="Total")
print(pivot)
channel    Online  Retail  Wholesale  Total
region
Midwest      2429    1743       9467  13639
Northeast    3505    3253      14028  20786
South        4334    4497       9853  18684
West         5593    2902      28131  36626
Total       15861   12395      61479  89735

The same cross-tab you built by dragging fields in Lesson 7. Because order_date is now a real date, the .dt accessor extracts parts of it:

df["month"] = df["order_date"].dt.month
monthly = df.groupby("month")["revenue"].sum()
print(monthly.idxmax(), monthly.max())
12 18319

December (month 12) was the best month, with $18,319.

Step 6: Merge tables (the JOIN)

products = pd.read_csv("sales-practice-products.csv")
df = df.merge(products[["product", "unit_cost"]], on="product", how="left")
df["gross_profit"] = df["units"] * (df["unit_price"] - df["unit_cost"])
print(df["gross_profit"].sum())
print(round(df["gross_profit"].sum() / df["revenue"].sum() * 100, 1))
42514
47.4

merge is pandas' JOIN: on is the key and how is the join type ("inner", "left", "right" or "outer"). Gross profit is $42,514, a 47.4% margin, matching the SQL and XLOOKUP results. After a merge, always check that the row count didn't change unexpectedly: len(df) should still be 200.

3. You need every product from the catalog, including ones with no sales. Which merge do you use?

  • products.merge(df, on="product", how="left")
  • df.merge(products, on="product", how="inner")
  • products.merge(df, on="order_id", how="left")

Putting products on the left with how="left" keeps every product, including the unsold Desk Lamp (with missing sales values). An inner merge drops it, and order_id isn't a column in the products table.

Step 7: Chart and export

ax = monthly.plot(kind="line", marker="o", title="Revenue by month, 2025")
ax.set_xlabel("Month")
ax.set_ylabel("Revenue ($)")
ax.figure.savefig("monthly_revenue.png", dpi=150, bbox_inches="tight")

df.to_csv("sales-clean-from-python.csv", index=False)

pandas draws charts with matplotlib, which Colab and Anaconda include. savefig saves the chart as an image for slides; to_csv writes the cleaned, enriched table for Power BI, Looker Studio or a colleague. For a quick look at another breakdown, a one-liner is enough:

print(df.groupby("channel")["revenue"].sum().sort_values(ascending=False))

Output of the line above, as a chart: revenue by channel

Wholesale$61,479
Online$15,861
Retail$12,395

Excel, SQL and pandas side by side

TaskExcel / SheetsSQLpandas
Filter rowsFilter, FILTER()WHEREdf[df["region"] == "West"]
SortSortORDER BYdf.sort_values("revenue")
Sum by groupSUMIFS, pivot tableGROUP BY + SUMdf.groupby("region")["revenue"].sum()
Cross-tabPivot tableGROUP BY two columnspd.pivot_table(...)
Lookup / joinXLOOKUPJOINdf.merge(...)
Remove duplicatesRemove DuplicatesSELECT DISTINCTdf.drop_duplicates()
Count blanksCOUNTBLANKWHERE … IS NULLdf.isna().sum()
SpreadsheetsPoint and click
Learning curve
●●●●
Repeatability
●●●●
Best for
Small data, quick checks, sharing with non-technical colleagues.
Fast start
pandasCode
Learning curve
●●●●
Repeatability
●●●●
Best for
Large or messy files, monthly repeats, automation and complex cleaning.
Scales up

Common pandas mistakes

df.drop_duplicates() on its own line, then wondering why nothing changed.

Most methods return a new table: assign it, df = df.drop_duplicates().

Chained assignment: df[df["region"] == "West"]["revenue"] = 0.

Use .loc: df.loc[df["region"] == "West", "revenue"] = 0. Chained assignment doesn't change the original table.

Title-casing before mapping abbreviations ("CA" becomes "Ca").

Strip, map, then title-case, in that order.

Merging without checking row counts.

Compare len(df) before and after; duplicate keys multiply rows.

4. In df.groupby("region").agg(avg_order=("revenue", "mean")), what does avg_order contain for the West?

  • The average revenue per West order, $572.28
  • The West's total revenue, $36,626
  • The number of West orders, 64

The tuple says "take the revenue column and apply mean" within each region group, and name the result avg_order. For the West: $36,626 ÷ 64 = $572.28.

Practice exercise

Using the cleaned df, write code that shows revenue by category, sorted from largest to smallest, with each category's share of total revenue as a percentage rounded to one decimal.

See solution
by_cat = df.groupby("category")["revenue"].sum().sort_values(ascending=False)
share = (by_cat / by_cat.sum() * 100).round(1)
print(pd.DataFrame({"revenue": by_cat, "share_pct": share}))
                revenue  share_pct
category
Electronics       44068       49.1
Home & Kitchen    17875       19.9
Office            15507       17.3
Fitness           12285       13.7

The shares add up to 100% and match the donut chart from Lesson 7. Dividing a grouped Series by its total works because pandas divides each value by the same number. Bonus: by_cat.plot(kind="barh") turns it into a horizontal bar chart.

Summary

  • pandas is a code-based spreadsheet: reproducible, scalable and free in Google Colab or Jupyter.
  • Inspect every dataset with shape, head, info, isna().sum() and duplicated().sum().
  • Five lines clean the raw practice file into 200 orders and $89,735, matching the answer key.
  • groupby/agg, pivot_table and merge are pandas' GROUP BY, pivot table and JOIN.
  • Filter with &/| and parentheses; modify with .loc; assign results back.
  • Chart with .plot() and export with to_csv for dashboards and reports.

Next lesson: Portfolio Project: Analyze a Business End to End. You'll put every skill together in a complete project you can show employers.

🎓 Save your progress and get a free certificate

Create your account in 30 seconds, mark your lessons and download your certificate at the end.

Enroll for free