Notebook 2 — Pandas Basics for CA Professionals
Goal: Work with real Excel-style data in Python using Pandas.
You will learn:
- What Pandas is and how it relates to Excel
- Series and DataFrame — the two main objects
- Loading a CSV file and looking at it
- Selecting columns and filtering rows
- Adding new calculated columns (like a new Excel column)
- Sorting data
- GroupBy — like a Pivot Table
- Saving your results back to Excel/CSV
- A short mini-project at the end
Throughout the notebook we will use Nepali invoice, sales and payroll data placed in the
data/folder beside this notebook.
1. What is Pandas?
Pandas is a Python library that gives you a DataFrame — basically an Excel sheet inside Python.
| Excel | Pandas |
|---|---|
| Workbook | (your script) |
| Worksheet | DataFrame |
| Column | Series |
| Cell | one value |
| Pivot Table | groupby() |
| AutoFilter | boolean mask |
import pandas as pd
import numpy as np
print("Pandas version:", pd.__version__)
Pandas version: 3.0.3
2. A Series — one column of data
A Series is a single column with a label for each row (the index).
monthly = pd.Series(
data=[1250000, 1380000, 1120000, 1450000, 1620000],
index=["Shrawan", "Bhadra", "Ashwin", "Kartik", "Mangsir"],
name="Revenue_NPR",
)
print(monthly)
print()
print("Total :", monthly.sum())
print("Mean :", monthly.mean())
Shrawan 1250000
Bhadra 1380000
Ashwin 1120000
Kartik 1450000
Mangsir 1620000
Name: Revenue_NPR, dtype: int64
Total : 6820000
Mean : 1364000.0
Practice 1
Create a Series called expenses for these 5 months:
- Shrawan: 920000, Bhadra: 980000, Ashwin: 870000, Kartik: 1050000, Mangsir: 1180000
Then print the total and the highest expense.
# Your turn:
Show solution
expenses = pd.Series(
[920000, 980000, 870000, 1050000, 1180000],
index=["Shrawan","Bhadra","Ashwin","Kartik","Mangsir"],
name="Expense_NPR",
)
print(expenses)
print("Total :", expenses.sum())
print("Max :", expenses.max())
3. A DataFrame — the full table
A DataFrame is several Series joined together. Each column has a name; each row has an index.
df = pd.DataFrame({
"Month": ["Shrawan", "Bhadra", "Ashwin", "Kartik", "Mangsir"],
"Revenue": [1250000, 1380000, 1120000, 1450000, 1620000],
"Expense": [ 920000, 980000, 870000, 1050000, 1180000],
})
df
| Month | Revenue | Expense | |
|---|---|---|---|
| 0 | Shrawan | 1250000 | 920000 |
| 1 | Bhadra | 1380000 | 980000 |
| 2 | Ashwin | 1120000 | 870000 |
| 3 | Kartik | 1450000 | 1050000 |
| 4 | Mangsir | 1620000 | 1180000 |
Practice 2
Create a small DataFrame with 4 employees of your choice. Columns:
Name, Department, Salary. Print the DataFrame.
# Your turn:
Show solution
staff = pd.DataFrame({
"Name": ["Ramesh", "Sita", "Hari", "Anita"],
"Department": ["Audit", "Tax", "Audit", "Advisory"],
"Salary": [55000, 72000, 48000, 95000],
})
print(staff)
4. Loading data from a CSV file
In real life you don't type the data — you read it from a file. We have a folder called data/ with several Nepali financial datasets already prepared.
invoices = pd.read_csv("data/invoices.csv")
invoices.head() # show first 5 rows
| Invoice_No | Date | Customer | City | Product | Quantity | Unit_Price | Amount_NPR | VAT_NPR | Total_NPR | Payment_Status | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | INV-20810001 | 2024-07-16 | Janakpur Wholesale | Birgunj | Food | 39 | 1219.0 | 47541.0 | 6180.33 | 53721.33 | Paid |
| 1 | INV-20810002 | 2024-07-18 | Kathmandu Distributors | Kathmandu | Food | 27 | 1418.0 | 38286.0 | 4977.18 | 43263.18 | Overdue |
| 2 | INV-20810003 | 2024-07-20 | Biratnagar Enterprise | Butwal | Stationery | 10 | 1309.0 | 13090.0 | 1701.70 | 14791.70 | Paid |
| 3 | INV-20810004 | 2024-07-22 | Pokhara Mart | Birgunj | Food | 26 | 5501.0 | 143026.0 | 18593.38 | 161619.38 | Paid |
| 4 | INV-20810005 | 2024-07-24 | Janakpur Wholesale | Bhaktapur | Food | 19 | 1669.0 | 31711.0 | 4122.43 | 35833.43 | Paid |
Practice 3
Load the file data/monthly_sales.csv into a DataFrame called sales and show the first 5 rows.
# Your turn:
Show solution
sales = pd.read_csv("data/monthly_sales.csv")
sales.head()
| Month | Revenue_NPR | Expense_NPR | Units_Sold | |
|---|---|---|---|---|
| 0 | Shrawan | 1250000 | 920000 | 420 |
| 1 | Bhadra | 1380000 | 980000 | 465 |
| 2 | Ashwin | 1120000 | 870000 | 380 |
| 3 | Kartik | 1450000 | 1050000 | 498 |
| 4 | Mangsir | 1620000 | 1180000 | 540 |
5. Quick inspection — get to know your data
| Code | What it does |
|---|---|
df.head(n) |
first n rows |
df.tail(n) |
last n rows |
df.shape |
(rows, columns) |
df.columns |
column names |
df.dtypes |
data type of each column |
df.info() |
full overview |
df.describe() |
quick statistics for numeric columns |
print("Shape :", invoices.shape)
print("Columns :", list(invoices.columns))
print()
invoices.info()
Shape : (200, 11)
Columns : ['Invoice_No', 'Date', 'Customer', 'City', 'Product', 'Quantity', 'Unit_Price', 'Amount_NPR', 'VAT_NPR', 'Total_NPR', 'Payment_Status']
<class 'pandas.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 11 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Invoice_No 200 non-null str
1 Date 200 non-null str
2 Customer 200 non-null str
3 City 200 non-null str
4 Product 200 non-null str
5 Quantity 200 non-null int64
6 Unit_Price 200 non-null float64
7 Amount_NPR 200 non-null float64
8 VAT_NPR 200 non-null float64
9 Total_NPR 200 non-null float64
10 Payment_Status 200 non-null str
dtypes: float64(4), int64(1), str(6)
memory usage: 17.3 KB
invoices.describe() # statistics for all numeric columns
| Quantity | Unit_Price | Amount_NPR | VAT_NPR | Total_NPR | |
|---|---|---|---|---|---|
| count | 200.000000 | 200.000000 | 200.000000 | 200.000000 | 200.000000 |
| mean | 24.355000 | 4012.410000 | 93674.160000 | 12177.640800 | 105851.800800 |
| std | 14.109371 | 2314.909302 | 81983.202634 | 10657.816342 | 92641.018976 |
| min | 1.000000 | 318.000000 | 891.000000 | 115.830000 | 1006.830000 |
| 25% | 12.750000 | 1863.250000 | 33018.000000 | 4292.340000 | 37310.340000 |
| 50% | 25.000000 | 3785.500000 | 67947.500000 | 8833.175000 | 76780.675000 |
| 75% | 36.000000 | 5984.500000 | 135046.750000 | 17556.077500 | 152602.827500 |
| max | 49.000000 | 8416.000000 | 387590.000000 | 50386.700000 | 437976.700000 |
Practice 4
Load data/loans.csv into a DataFrame called loans. Then:
- Print its shape.
- Show its column names.
- Show
describe()for the numeric columns.
# Your turn:
Show solution
loans = pd.read_csv("data/loans.csv")
print("Shape :", loans.shape)
print("Columns :", list(loans.columns))
loans.describe()
| Loan_Amount | Tenure_Months | Interest_Rate | Annual_Income | Credit_Score | Defaulted | |
|---|---|---|---|---|---|---|
| count | 3.000000e+02 | 300.000000 | 300.000000 | 3.000000e+02 | 300.000000 | 300.000000 |
| mean | 9.315000e+05 | 49.720000 | 11.916633 | 6.734000e+05 | 581.816667 | 0.413333 |
| std | 1.111396e+06 | 37.531547 | 2.339863 | 6.099118e+05 | 158.994109 | 0.493254 |
| min | 1.000000e+05 | 12.000000 | 8.040000 | 1.800000e+05 | 300.000000 | 0.000000 |
| 25% | 2.500000e+05 | 24.000000 | 9.882500 | 3.600000e+05 | 443.500000 | 0.000000 |
| 50% | 5.000000e+05 | 36.000000 | 11.830000 | 3.600000e+05 | 602.500000 | 0.000000 |
| 75% | 1.000000e+06 | 60.000000 | 13.947500 | 6.000000e+05 | 718.500000 | 1.000000 |
| max | 5.000000e+06 | 120.000000 | 15.920000 | 2.400000e+06 | 849.000000 | 1.000000 |
6. Selecting columns
You can pick a single column (gives a Series), or several columns (gives a DataFrame).
# Single column → Series
invoices["Amount_NPR"].head()
0 47541.0
1 38286.0
2 13090.0
3 143026.0
4 31711.0
Name: Amount_NPR, dtype: float64
# Several columns → DataFrame
invoices[["Customer", "City", "Total_NPR"]].head()
| Customer | City | Total_NPR | |
|---|---|---|---|
| 0 | Janakpur Wholesale | Birgunj | 53721.33 |
| 1 | Kathmandu Distributors | Kathmandu | 43263.18 |
| 2 | Biratnagar Enterprise | Butwal | 14791.70 |
| 3 | Pokhara Mart | Birgunj | 161619.38 |
| 4 | Janakpur Wholesale | Bhaktapur | 35833.43 |
Practice 5
From invoices, show only the columns Invoice_No, Product, Quantity, Amount_NPR — first 10 rows.
# Your turn:
Show solution
invoices[["Invoice_No","Product","Quantity","Amount_NPR"]].head(10)
| Invoice_No | Product | Quantity | Amount_NPR | |
|---|---|---|---|---|
| 0 | INV-20810001 | Food | 39 | 47541.0 |
| 1 | INV-20810002 | Food | 27 | 38286.0 |
| 2 | INV-20810003 | Stationery | 10 | 13090.0 |
| 3 | INV-20810004 | Food | 26 | 143026.0 |
| 4 | INV-20810005 | Food | 19 | 31711.0 |
| 5 | INV-20810006 | Cosmetics | 39 | 118404.0 |
| 6 | INV-20810007 | Food | 3 | 22914.0 |
| 7 | INV-20810008 | Textile | 45 | 184860.0 |
| 8 | INV-20810009 | Hardware | 13 | 74412.0 |
| 9 | INV-20810010 | Electronics | 28 | 44492.0 |
7. Filtering rows — the AutoFilter of Pandas
Just like np.array[condition], but on a whole table.
# Invoices with amount over NPR 1 lakh
big = invoices[invoices["Amount_NPR"] > 100000]
print("Found", len(big), "high-value invoices")
big.head()
Found 75 high-value invoices
| Invoice_No | Date | Customer | City | Product | Quantity | Unit_Price | Amount_NPR | VAT_NPR | Total_NPR | Payment_Status | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 3 | INV-20810004 | 2024-07-22 | Pokhara Mart | Birgunj | Food | 26 | 5501.0 | 143026.0 | 18593.38 | 161619.38 | Paid |
| 5 | INV-20810006 | 2024-07-26 | Birgunj Cargo | Pokhara | Cosmetics | 39 | 3036.0 | 118404.0 | 15392.52 | 133796.52 | Pending |
| 7 | INV-20810008 | 2024-07-30 | Janakpur Wholesale | Bhaktapur | Textile | 45 | 4108.0 | 184860.0 | 24031.80 | 208891.80 | Overdue |
| 13 | INV-20810014 | 2024-08-11 | Annapurna Stores | Pokhara | Stationery | 41 | 2476.0 | 101516.0 | 13197.08 | 114713.08 | Paid |
| 19 | INV-20810020 | 2024-08-23 | Everest Suppliers | Nepalgunj | Cosmetics | 49 | 3192.0 | 156408.0 | 20333.04 | 176741.04 | Overdue |
# Combine conditions with & (and) and | (or) — wrap each in ()
overdue_big = invoices[(invoices["Payment_Status"] == "Overdue") &
(invoices["Amount_NPR"] > 50000)]
overdue_big.head()
| Invoice_No | Date | Customer | City | Product | Quantity | Unit_Price | Amount_NPR | VAT_NPR | Total_NPR | Payment_Status | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 7 | INV-20810008 | 2024-07-30 | Janakpur Wholesale | Bhaktapur | Textile | 45 | 4108.0 | 184860.0 | 24031.80 | 208891.80 | Overdue |
| 8 | INV-20810009 | 2024-08-01 | Biratnagar Enterprise | Hetauda | Hardware | 13 | 5724.0 | 74412.0 | 9673.56 | 84085.56 | Overdue |
| 18 | INV-20810019 | 2024-08-21 | Lumbini Imports | Lalitpur | Stationery | 28 | 1873.0 | 52444.0 | 6817.72 | 59261.72 | Overdue |
| 19 | INV-20810020 | 2024-08-23 | Everest Suppliers | Nepalgunj | Cosmetics | 49 | 3192.0 | 156408.0 | 20333.04 | 176741.04 | Overdue |
| 26 | INV-20810027 | 2024-09-06 | Birgunj Cargo | Bhaktapur | Electronics | 32 | 7444.0 | 238208.0 | 30967.04 | 269175.04 | Overdue |
Practice 6
From invoices:
- Find all invoices from the city "Kathmandu".
- Find all "Pending" invoices above NPR 30,000.
- Count how many invoices have
Quantity> 30.
# Your turn:
Show solution
ktm = invoices[invoices["City"] == "Kathmandu"]
pending = invoices[(invoices["Payment_Status"] == "Pending") &
(invoices["Amount_NPR"] > 30000)]
print("KTM invoices :", len(ktm))
print("Pending > 30K :", len(pending))
print("Qty > 30 count :", (invoices["Quantity"] > 30).sum())
8. Sorting
sort_values(by="ColumnName") is your friend — just like Data → Sort in Excel.
# Top 5 highest invoices
top5 = invoices.sort_values("Total_NPR", ascending=False).head(5)
top5[["Invoice_No","Customer","Total_NPR","Payment_Status"]]
| Invoice_No | Customer | Total_NPR | Payment_Status | |
|---|---|---|---|---|
| 96 | INV-20810097 | Nepalgunj Goods | 437976.70 | Pending |
| 132 | INV-20810133 | Annapurna Stores | 406037.25 | Overdue |
| 27 | INV-20810028 | Annapurna Stores | 398613.15 | Paid |
| 163 | INV-20810164 | Kathmandu Distributors | 352471.86 | Pending |
| 154 | INV-20810155 | Nepalgunj Goods | 346756.32 | Pending |
Practice 7
Sort invoices by Quantity (largest first) and show the top 10.
# Your turn:
Show solution
invoices.sort_values("Quantity", ascending=False).head(10)
| Invoice_No | Date | Customer | City | Product | Quantity | Unit_Price | Amount_NPR | VAT_NPR | Total_NPR | Payment_Status | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 136 | INV-20810137 | 2025-04-14 | Nepalgunj Goods | Lalitpur | Stationery | 49 | 4947.0 | 242403.0 | 31512.39 | 273915.39 | Paid |
| 41 | INV-20810042 | 2024-10-06 | Everest Suppliers | Bhaktapur | Electronics | 49 | 2258.0 | 110642.0 | 14383.46 | 125025.46 | Paid |
| 96 | INV-20810097 | 2025-01-24 | Nepalgunj Goods | Biratnagar | Hardware | 49 | 7910.0 | 387590.0 | 50386.70 | 437976.70 | Pending |
| 19 | INV-20810020 | 2024-08-23 | Everest Suppliers | Nepalgunj | Cosmetics | 49 | 3192.0 | 156408.0 | 20333.04 | 176741.04 | Overdue |
| 103 | INV-20810104 | 2025-02-07 | Janakpur Wholesale | Biratnagar | Food | 49 | 3702.0 | 181398.0 | 23581.74 | 204979.74 | Overdue |
| 196 | INV-20810197 | 2025-08-12 | Pokhara Mart | Lalitpur | Hardware | 48 | 1666.0 | 79968.0 | 10395.84 | 90363.84 | Pending |
| 76 | INV-20810077 | 2024-12-15 | Nepalgunj Goods | Kathmandu | Textile | 48 | 1000.0 | 48000.0 | 6240.00 | 54240.00 | Paid |
| 154 | INV-20810155 | 2025-05-20 | Nepalgunj Goods | Hetauda | Food | 48 | 6393.0 | 306864.0 | 39892.32 | 346756.32 | Pending |
| 111 | INV-20810112 | 2025-02-23 | Annapurna Stores | Lalitpur | Cosmetics | 48 | 4168.0 | 200064.0 | 26008.32 | 226072.32 | Paid |
| 28 | INV-20810029 | 2024-09-10 | Janakpur Wholesale | Hetauda | Hardware | 47 | 660.0 | 31020.0 | 4032.60 | 35052.60 | Paid |
9. Creating new columns
You can build new columns from existing ones — like adding a new column with an Excel formula.
sales = pd.read_csv("data/monthly_sales.csv")
# New calculated columns
sales["Profit"] = sales["Revenue_NPR"] - sales["Expense_NPR"]
sales["Margin_pct"] = (sales["Profit"] / sales["Revenue_NPR"] * 100).round(2)
sales["VAT_NPR"] = sales["Revenue_NPR"] * 0.13
sales
| Month | Revenue_NPR | Expense_NPR | Units_Sold | Profit | Margin_pct | VAT_NPR | |
|---|---|---|---|---|---|---|---|
| 0 | Shrawan | 1250000 | 920000 | 420 | 330000 | 26.40 | 162500.0 |
| 1 | Bhadra | 1380000 | 980000 | 465 | 400000 | 28.99 | 179400.0 |
| 2 | Ashwin | 1120000 | 870000 | 380 | 250000 | 22.32 | 145600.0 |
| 3 | Kartik | 1450000 | 1050000 | 498 | 400000 | 27.59 | 188500.0 |
| 4 | Mangsir | 1620000 | 1180000 | 540 | 440000 | 27.16 | 210600.0 |
| 5 | Poush | 1780000 | 1250000 | 605 | 530000 | 29.78 | 231400.0 |
| 6 | Magh | 1550000 | 1100000 | 515 | 450000 | 29.03 | 201500.0 |
| 7 | Falgun | 1420000 | 1020000 | 478 | 400000 | 28.17 | 184600.0 |
| 8 | Chaitra | 1680000 | 1190000 | 560 | 490000 | 29.17 | 218400.0 |
| 9 | Baishakh | 1910000 | 1320000 | 640 | 590000 | 30.89 | 248300.0 |
| 10 | Jestha | 2030000 | 1410000 | 690 | 620000 | 30.54 | 263900.0 |
| 11 | Ashadh | 2150000 | 1490000 | 720 | 660000 | 30.70 | 279500.0 |
Practice 8
Load data/payroll.csv into pay. Add a new column Annual_Net = Net_Salary × 12. Then sort by Annual_Net (descending) and show the top 5 employees.
# Your turn:
Show solution
pay = pd.read_csv("data/payroll.csv")
pay["Annual_Net"] = pay["Net_Salary"] * 12
pay.sort_values("Annual_Net", ascending=False).head(5)
| Emp_ID | Name | Department | Designation | Years_Exp | Basic_Salary | Allowance | Gross | EPF_10pct | SSF_1pct | Net_Salary | Annual_Net | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 24 | E1024 | Bishnu Paudel | Advisory | Manager | 22 | 250000 | 100000.0 | 350000.0 | 25000.0 | 3500.0 | 321500.0 | 3858000.0 |
| 7 | E1007 | Kabita Rai | Advisory | Senior | 8 | 250000 | 100000.0 | 350000.0 | 25000.0 | 3500.0 | 321500.0 | 3858000.0 |
| 14 | E1014 | Manoj Rana | Tax | Senior | 17 | 250000 | 100000.0 | 350000.0 | 25000.0 | 3500.0 | 321500.0 | 3858000.0 |
| 15 | E1015 | Kalpana Pokhrel | Audit | Senior | 2 | 180000 | 72000.0 | 252000.0 | 18000.0 | 2520.0 | 231480.0 | 2777760.0 |
| 4 | E1004 | Bikash Gurung | Tax | Junior | 4 | 180000 | 72000.0 | 252000.0 | 18000.0 | 2520.0 | 231480.0 | 2777760.0 |
10. GroupBy — the Pandas Pivot Table
groupby() is the most powerful feature you'll use. It splits data into groups, applies a function to each group, and combines the result. Exactly like a Pivot Table.
# Total invoice amount per city
invoices.groupby("City")["Total_NPR"].sum().sort_values(ascending=False)
City
Bhaktapur 2608959.82
Biratnagar 2432223.30
Kathmandu 2299763.57
Hetauda 2175177.68
Lalitpur 2024371.27
Butwal 1997602.70
Birgunj 1989168.38
Janakpur 1975963.20
Nepalgunj 1934153.20
Pokhara 1732977.04
Name: Total_NPR, dtype: float64
# Multiple metrics at once
invoices.groupby("Payment_Status").agg(
Count = ("Invoice_No", "count"),
Total_Amount = ("Total_NPR", "sum"),
Avg_Amount = ("Total_NPR", "mean"),
).round(0)
| Count | Total_Amount | Avg_Amount | |
|---|---|---|---|
| Payment_Status | |||
| Overdue | 28 | 3268298.0 | 116725.0 |
| Paid | 133 | 13633958.0 | 102511.0 |
| Pending | 39 | 4268104.0 | 109439.0 |
Practice 9
Using invoices:
- Find the total Amount_NPR per Product.
- Find the average Quantity per Customer.
- Count how many invoices each
Cityhas.
# Your turn:
Show solution
print(invoices.groupby("Product")["Amount_NPR"].sum())
print()
print(invoices.groupby("Customer")["Quantity"].mean().round(1))
print()
print(invoices.groupby("City").size())
11. Saving your work to a file
Once you have cleaned / processed data, you can save the result.
top5 = invoices.sort_values("Total_NPR", ascending=False).head(5)
top5.to_csv("top5_invoices.csv", index=False) # save as CSV
# top5.to_excel("top5_invoices.xlsx", index=False) # needs openpyxl
print("Saved top5_invoices.csv")
Saved top5_invoices.csv
12. Mini-Project — Sales Analysis
Using data/invoices.csv, answer the following:
- Which city has the highest total sales?
- Which product is sold the most (by quantity)?
- What % of invoices are still pending or overdue?
- List the top 3 customers by total revenue.
- Find the average invoice amount city-wise.
inv = pd.read_csv("data/invoices.csv")
# Your code here
Show solution
# 1
print("Top city by sales:")
print(inv.groupby("City")["Total_NPR"].sum().sort_values(ascending=False).head(1))
print()
# 2
print("Top product by qty:")
print(inv.groupby("Product")["Quantity"].sum().sort_values(ascending=False).head(1))
print()
# 3
pending_or_over = inv[inv["Payment_Status"] != "Paid"]
print(f"Pending+Overdue: {len(pending_or_over)/len(inv)*100:.1f}%")
print()
# 4
print("Top 3 customers:")
print(inv.groupby("Customer")["Total_NPR"].sum().sort_values(ascending=False).head(3))
print()
# 5
print("Average invoice per city:")
print(inv.groupby("City")["Total_NPR"].mean().round(0).sort_values(ascending=False))
What's next?
Pandas is now your everyday tool for working with tabular data.
In the next notebook (03_Matplotlib_Seaborn) we will turn this data into charts — line graphs, bar charts, heatmaps — so we can present it to clients and partners.