πΌ Pandas for CA Professionals
From Raw Data to Financial Insight
Pre-requisite: Module 2 (NumPy)
Estimated time: 5β6 hours
Session structure: Why Pandas β What is Pandas β Core Concepts β Hands-on Practice β Applications
π Table of Contents
| Part | Section | Topic |
|---|---|---|
| Part 1: Why Pandas? | 1 | The Limits of NumPy β What It Cannot Handle |
| 2 | What is Pandas? β History & Design Philosophy | |
| Part 2: Fundamentals | 3 | The Index β Pandas' Core Innovation |
| 4 | Pandas Data Types (dtype) | |
| 5 | Series & DataFrame Architecture | |
| Part 3: Hands-on | 6 | Installing & Importing Pandas |
| 7 | Series β A Single Column | |
| 8 | DataFrame β The Full Spreadsheet | |
| 9 | Exploring Your Data | |
| 10 | Selecting Columns & Rows | |
| 11 | Filtering Data | |
| 12 | Sorting Data | |
| 13 | Calculated Columns | |
| 14 | GroupBy β Pivot Tables | |
| 15 | Handling Missing Values | |
| 16 | Merging DataFrames (VLOOKUP) | |
| 17 | Reading & Writing Excel / CSV | |
| 18 | String Operations | |
| Part 4: Practice | 19 | Practice Exercises |
Part 1: Why Pandas?
Section 1: The Limits of NumPy β What It Cannot Handle
You have already learned NumPy β Python's fast numerical engine. So why do we need another library?
NumPy is outstanding for pure mathematical computation, but real-world financial data is messy:
| What real data looks like | NumPy's limitation |
|---|---|
| A ledger has dates, names, amounts, GST codes | NumPy arrays are homogeneous β one data type per array |
You want to say ledger['Apr'] |
NumPy uses only positional indexing β array[3] |
| Bank statements have blank cells | NumPy has no native missing value handling |
| VAT-401 has column headers (GSTIN, HSN, Rateβ¦) | 2-D NumPy arrays have no column names |
| You need to join two tables (like VLOOKUP) | NumPy has no built-in merge / join |
The NumPy wall β a concrete example
Imagine you receive a trial balance as a CSV with 500 rows and 8 columns:
Account | Type | Opening Dr | Opening Cr | Transactions Dr | Transactions Cr | Closing Dr | Closing Cr
In NumPy you can only store this if you: - Convert all text to numbers (losing meaning), OR - Use a structured array (complex, rarely used in practice)
In Pandas, you simply call pd.read_csv('trial_balance.csv') β and you're done.
The rule of thumb: Use NumPy when you have pure numbers and need maximum speed.
Use Pandas when you have real-world tabular data with mixed types, labels, and missing values.
import numpy as np
import pandas as pd
# --- NumPy approach: storing a simple transaction table ---
# NumPy can only hold ONE data type. Mixing strings + numbers forces 'object' dtype
# and you lose ALL numeric speed advantages.
np_attempt = np.array([
['INV-001', 'Alpha Ltd', '85000', '18', 'Paid'],
['INV-002', 'Beta Corp', '42000', '12', 'Pending'],
['INV-003', 'Everest Imports Pvt. Ltd.', '120000', '5', 'Paid'],
])
print("NumPy mixed array dtype:", np_attempt.dtype) # object β all converted to string!
print("Can we sum the Amount column?")
try:
print(np_attempt[:, 2].sum()) # string concat, not addition!
except Exception as e:
print("Error:", e)
# --- Pandas approach: same data, zero effort ---
df = pd.DataFrame({
'Invoice': ['INV-001', 'INV-002', 'INV-003'],
'Party': ['Alpha Ltd', 'Beta Corp', 'Everest Imports Pvt. Ltd.'],
'Amount': [85000, 42000, 120000], # stored as int64
'Rate': [18, 12, 5], # stored as int64
'Status': ['Paid', 'Pending', 'Paid'], # stored as object (string)
})
print("\nPandas dtypes:")
print(df.dtypes)
print("\nTotal amount:", df['Amount'].sum()) # Works perfectly!
print("Pending invoices:")
print(df[df['Status'] == 'Pending'][['Invoice', 'Party', 'Amount']])
NumPy mixed array dtype: <U25
Can we sum the Amount column?
Error: the resolved dtypes are not compatible with add.reduce. Resolved (dtype('<U25'), dtype('<U25'), dtype('<U50'))
Pandas dtypes:
Invoice object
Party object
Amount int64
Rate int64
Status object
dtype: object
Total amount: 247000
Pending invoices:
Invoice Party Amount
1 INV-002 Beta Corp 42000
Section 2: What is Pandas? β History & Design Philosophy
The Origin Story
In 2008, Wes McKinney was working at AQR Capital Management β a quantitative hedge fund in Greenwich, Connecticut managing billions of dollars. His job was to analyse financial time-series data: stock prices, returns, correlations.
Excel was too slow for millions of rows. R's data frames were close, but Python had nothing.
So he built one.
"I started pandas because I was frustrated" β Wes McKinney, 2012
He open-sourced Pandas in 2009. Today it has over 200 million downloads per month and is the backbone of financial data analysis worldwide.
What does the name mean?
Pandas = Pan-el Data + Python
"Panel data" is an economics term for data that tracks multiple subjects over time β like a company's quarterly financials across multiple years. That is exactly the kind of data Pandas was designed to handle.
Where Pandas fits in the Python ecosystem
βββββββββββββββββββββββββββββββββββββββββββ
Your Python β Pandas Matplotlib Seaborn Plotly β β Analysis & Viz
Analysis Stack β (all built on top of NumPy) β
βββββββββββββββββββββββββββββββββββββββββββ€
β NumPy β β Numerical Engine
βββββββββββββββββββββββββββββββββββββββββββ€
β CPython / C β β Runtime
βββββββββββββββββββββββββββββββββββββββββββ
Who uses Pandas today?
| Sector | Use case |
|---|---|
| Accounting & Audit | Trial balance analysis, VAT Return reconciliation, variance reports |
| Investment Banking | Financial modelling, M&A data rooms |
| Central Banks | Monetary data aggregation, policy analysis |
| Big Four firms | Automated data extraction from ERP systems |
| Regulators (SEBON, NRB) | Market surveillance, filing analysis |
For a CA professional, Pandas is arguably the single most valuable Python library to learn.
Part 2: Pandas Fundamentals
Section 3: The Index β Pandas' Core Innovation
The single most important concept in Pandas β and the feature that makes it superior to plain NumPy for financial data β is the Index.
What is an Index?
Every Pandas object (Series or DataFrame) has an Index: a set of labels attached to its rows.
| In Excel | In NumPy | In Pandas |
|---|---|---|
| Row 3 (position) | array[2] (position) |
df.loc['Apr'] (label) |
Think of the Index as a register β like a ledger folio number that lets you find any entry by name, not just by page count.
Why does this matter for finance?
- Access monthly P&L data by month name (
'Apr','Mar') rather than position (0, 11) - Work with date-indexed time series (stock prices, daily cash flow)
- Align two datasets automatically on their index β Pandas lines them up even if the rows are in different orders
- Perform time-based resampling (daily β monthly β quarterly) using the DatetimeIndex
Types of Index
| Index type | Example | Finance use case |
|---|---|---|
| RangeIndex (default) | 0, 1, 2, 3β¦ | General tabular data |
| StringIndex | 'Apr', 'May'β¦ | Monthly reports |
| DatetimeIndex | 2025-04-01, 2025-04-02β¦ | Daily price/transaction data |
| IntegerIndex | 1001, 1002, 1003β¦ | Voucher numbers, GL codes |
| MultiIndex | (Q1, Apr), (Q1, May)β¦ | Hierarchical financial statements |
import pandas as pd
# --- Default RangeIndex (0, 1, 2...) ---
revenue_default = pd.Series([1250000, 1380000, 1120000])
print("Default index:")
print(revenue_default)
# --- Custom StringIndex: access by month name ---
revenue = pd.Series(
data = [1250000, 1380000, 1120000, 1450000, 1620000, 1780000,
1550000, 1420000, 1680000, 1910000, 2030000, 2150000],
index = ['Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','Jan','Feb','Mar'],
name = 'Revenue (NPR )'
)
print("\nNamed index β access by month:")
print("April:", revenue['Apr']) # label-based
print("Last month:", revenue.iloc[-1]) # still works positionally too
# --- Automatic alignment by index (NumPy cannot do this!) ---
revenue_fy25 = pd.Series([1250000, 1380000, 1120000], index=['Apr', 'May', 'Jun'])
revenue_fy26 = pd.Series([1450000, 1650000, 1210000], index=['May', 'Jun', 'Jul'])
# Pandas aligns on the shared index automatically β NaN for unmatched months
growth = revenue_fy26 - revenue_fy25
print("\nGrowth (auto-aligned by month name):")
print(growth) # Apr=NaN (no FY26 Apr), May/Jun=difference, Jul=NaN (no FY 2082 BS Jul)
Default index:
0 1250000
1 1380000
2 1120000
dtype: int64
Named index β access by month:
April: 1250000
Last month: 2150000
Growth (auto-aligned by month name):
Apr NaN
Jul NaN
Jun 530000.0
May 70000.0
dtype: float64
Section 4: Pandas Data Types (dtype)
Every column in a Pandas DataFrame has a dtype β the type of data it stores. Understanding dtypes is essential because:
- Wrong dtype β wrong results (e.g., amounts stored as text can't be summed)
- Right dtype β better memory efficiency and faster operations
- Pandas operations behave differently depending on dtype
The Pandas dtype table
| dtype | What it stores | Memory | Finance example |
|---|---|---|---|
int64 |
Whole numbers | 8 bytes | Voucher numbers, quantities |
float64 |
Decimal numbers | 8 bytes | Amounts, GST rates, percentages |
object |
Strings / mixed | Variable | Party names, account codes, GSTIN |
bool |
True / False | 1 byte | Paid? Audited? Flagged? |
datetime64[ns] |
Dates and times | 8 bytes | Invoice dates, due dates, FY periods |
category |
Fixed set of values | Low | GST slabs, account types, quarters |
The most common mistake in financial data
When you import a CSV from Tally or SAP, amounts are often read as object (string) because of formatting:
"NPR 1,25,000" β Pandas cannot sum this until you clean and cast it to float64.
The category dtype β a hidden gem
If a column has only a few repeating values ('Q1','Q2','Q3','Q4' or 'Asset','Liability','Income','Expense'), store it as category:
- Uses 5β10Γ less memory than object
- Sorting respects the logical order you define (Apr before May, not alphabetical)
import pandas as pd
import numpy as np
# --- Checking dtypes ---
trial_balance = pd.DataFrame({
'Account': ['Cash','Trade Debtors','Fixed Assets','Creditors','Sales'],
'Type': ['Asset','Asset','Asset','Liability','Income'],
'Quarter': ['Q4','Q4','Q4','Q4','Q4'],
'Debit': [485000, 1250000, 8500000, 0, 0],
'Credit': [0, 0, 0, 980000, 5200000],
'Date': ['2025-03-31']*5,
})
print("Default dtypes after DataFrame creation:")
print(trial_balance.dtypes)
# --- Type conversion ---
# Convert Date string β datetime64
trial_balance['Date'] = pd.to_datetime(trial_balance['Date'])
# Convert Type and Quarter β category (saves memory, enables proper sorting)
trial_balance['Type'] = trial_balance['Type'].astype('category')
trial_balance['Quarter'] = trial_balance['Quarter'].astype('category')
print("\nAfter conversion:")
print(trial_balance.dtypes)
# --- Memory comparison ---
n = 50000 # simulate a large ledger
large_df = pd.DataFrame({'Type': np.random.choice(['Asset','Liability','Income','Expense'], n)})
mem_object = large_df['Type'].astype('object').memory_usage(deep=True)
mem_category = large_df['Type'].astype('category').memory_usage(deep=True)
print(f"\nMemory for 50,000 account-type rows:")
print(f" object dtype: {mem_object:,} bytes")
print(f" category dtype: {mem_category:,} bytes ({mem_object/mem_category:.0f}x smaller!)")
# --- The common import problem: amount as string ---
messy = pd.Series(['1,25,000', 'NPR 82,000', '45000', '1,20,000'])
print("\nAmount column as imported (object):", messy.dtype)
cleaned = messy.str.replace('[NPR ,]', '', regex=True).astype(float)
print("After cleaning (float64):", cleaned.dtype)
print("Sum:", cleaned.sum())
Default dtypes after DataFrame creation:
Account object
Type object
Quarter object
Debit int64
Credit int64
Date object
dtype: object
After conversion:
Account object
Type category
Quarter category
Debit int64
Credit int64
Date datetime64[ns]
dtype: object
Memory for 50,000 account-type rows:
object dtype: 3,187,540 bytes
category dtype: 50,555 bytes (63x smaller!)
Amount column as imported (object): object
After cleaning (float64): float64
Sum: 372000.0
Section 5: Series & DataFrame Architecture
Before we dive into hands-on work, let's understand exactly what a Series and a DataFrame are β structurally.
Series: A Labeled Column
A Series is a one-dimensional labeled array. Think of it as a single column from a ledger β with row labels (the Index) and values.
Index β Values
ββββββββββΌββββββββββ
Apr β 1,250,000 βββ one row
May β 1,380,000
Jun β 1,120,000
β
name: Revenue (NPR )
dtype: int64
A Series has exactly two attributes that define it: index + values (+ a name).
DataFrame: A Labeled Table
A DataFrame is a collection of Series that share the same Index. Think of it as a complete spreadsheet β rows identified by the Index, columns identified by column names.
Index β Month β Revenue β COGS β Net_Profit
ββββββββββΌβββββββββΌββββββββββββΌββββββββββΌβββββββββββ
0 β Apr β 1,250,000 β 750,000 β 197,000
1 β May β 1,380,000 β 828,000 β 229,000
2 β Jun β 1,120,000 β 672,000 β 185,000
β β β β
β object β int64 β int64 β int64 βββ each column has its own dtype
Key relationships:
- df['Revenue'] β returns a Series (one column)
- df[['Revenue', 'COGS']] β returns a DataFrame (subset of columns)
- df.loc[0] β returns a Series (one row)
The CA Analogy
| Pandas object | CA equivalent |
|---|---|
| Index | Folio / Row reference number |
| Series | A single column in a ledger (e.g., "Credit" column) |
| DataFrame | The complete ledger / trial balance / VAT-401 table |
| Column dtype | The data validation rule for that column |
With this foundation, every operation you do in Pandas β filtering, grouping, merging β makes intuitive sense. You are always working with labeled rows and columns.
Part 3: Hands-on with Pandas
Section 6: Installing & Importing Pandas
# Install if needed (run once)
# !pip install pandas openpyxl
import pandas as pd # pd is the universal shorthand
import numpy as np # Often used alongside pandas
print('Pandas version:', pd.__version__)
print('Ready!')
Pandas version: 2.3.2
Ready!
Section 7: Series β A Single Column
A Series is the simplest Pandas object β it's like one column of a spreadsheet.
It has an index (row label) and values (the data).
# Creating a Series β monthly revenue
monthly_revenue = pd.Series(
data = [1250000, 1380000, 1120000, 1450000, 1620000, 1780000,
1550000, 1420000, 1680000, 1910000, 2030000, 2150000],
index = ['Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar'],
name = 'Revenue (NPR )'
)
print(monthly_revenue)
print('\nType:', type(monthly_revenue))
Apr 1250000
May 1380000
Jun 1120000
Jul 1450000
Aug 1620000
Sep 1780000
Oct 1550000
Nov 1420000
Dec 1680000
Jan 1910000
Feb 2030000
Mar 2150000
Name: Revenue (NPR ), dtype: int64
Type: <class 'pandas.core.series.Series'>
# Series operations β all vectorized like NumPy
print('Total: NPR ', monthly_revenue.sum())
print('Mean: NPR ', round(monthly_revenue.mean(), 2))
print('Max: NPR ', monthly_revenue.max(), '(', monthly_revenue.idxmax(), ')')
print('Min: NPR ', monthly_revenue.min(), '(', monthly_revenue.idxmin(), ')')
print()
# Access by label (like looking up a row in Excel)
print('April Revenue:', monthly_revenue['Apr'])
print('Q4 Revenue:')
print(monthly_revenue[['Jan', 'Feb', 'Mar']])
# Filtering β months where revenue exceeded NPR 17 lakhs
print('\nHigh revenue months (>NPR 17L):')
print(monthly_revenue[monthly_revenue > 1700000])
Total: NPR 19340000
Mean: NPR 1611666.67
Max: NPR 2150000 ( Mar )
Min: NPR 1120000 ( Jun )
April Revenue: 1250000
Q4 Revenue:
Jan 1910000
Feb 2030000
Mar 2150000
Name: Revenue (NPR ), dtype: int64
High revenue months (>NPR 17L):
Sep 1780000
Jan 1910000
Feb 2030000
Mar 2150000
Name: Revenue (NPR ), dtype: int64
Section 3 & 4: DataFrame β The Full Spreadsheet
A DataFrame is Pandas' main object β think of it as a complete Excel worksheet.
It has rows (indexed), columns (named), and can hold different data types in different columns.
Creating a DataFrame
# Method 1: From a dictionary (most common)
# Each key = column name, each value = list of column data
ledger_data = {
'Date': ['01-Apr-2025', '03-Apr-2025', '05-Apr-2025', '08-Apr-2025',
'10-Apr-2025', '12-Apr-2025', '15-Apr-2025', '18-Apr-2025'],
'Voucher_No': ['SV-001', 'PV-101', 'SV-002', 'PV-102', 'SV-003', 'PV-103', 'PV-104', 'SV-004'],
'Description': ['Sales - Client A', 'Office Rent', 'Sales - Client B', 'Electricity Bill',
'Sales - Client C', 'Salaries Paid', 'Vendor Payment', 'Sales - Client D'],
'Category': ['Revenue', 'Expense', 'Revenue', 'Expense',
'Revenue', 'Expense', 'Expense', 'Revenue'],
'Debit': [0, 45000, 0, 12000, 0, 180000, 95000, 0],
'Credit': [320000, 0, 185000, 0, 420000, 0, 0, 275000],
}
df = pd.DataFrame(ledger_data)
print(df)
Date Voucher_No Description Category Debit Credit
0 01-Apr-2025 SV-001 Sales - Client A Revenue 0 320000
1 03-Apr-2025 PV-101 Office Rent Expense 45000 0
2 05-Apr-2025 SV-002 Sales - Client B Revenue 0 185000
3 08-Apr-2025 PV-102 Electricity Bill Expense 12000 0
4 10-Apr-2025 SV-003 Sales - Client C Revenue 0 420000
5 12-Apr-2025 PV-103 Salaries Paid Expense 180000 0
6 15-Apr-2025 PV-104 Vendor Payment Expense 95000 0
7 18-Apr-2025 SV-004 Sales - Client D Revenue 0 275000
Section 10: Exploring Your Data
When you receive a financial data file, the first step is always to understand what's in it.
Pandas has several built-in functions for this.
# Create a larger sample dataset β Monthly P&L (FY 2081-82 BS)
import numpy as np
np.random.seed(42)
months = ['Shrawan', 'Bhadra', 'Ashwin', 'Kartik', 'Mangsir', 'Poush',
'Magh', 'Falgun', 'Chaitra', 'Baishakh', 'Jestha', 'Ashadh']
# Nepal FY quarters: Q1=Shrawan-Ashwin, Q2=Kartik-Poush, Q3=Magh-Chaitra, Q4=Baishakh-Ashadh
quarters = ['Q1','Q1','Q1', 'Q2','Q2','Q2', 'Q3','Q3','Q3', 'Q4','Q4','Q4']
revenue = np.random.randint(800000, 2000000, 12)
cogs = revenue * np.random.uniform(0.35, 0.55, 12)
gross_profit = revenue - cogs
opex = gross_profit * np.random.uniform(0.20, 0.40, 12)
net_profit = gross_profit - opex
pl_df = pd.DataFrame({
'Month': months,
'Quarter': quarters,
'Revenue': revenue.round(-3).astype(int),
'COGS': cogs.round(-3).astype(int),
'Gross_Profit': gross_profit.round(-3).astype(int),
'Opex': opex.round(-3).astype(int),
'Net_Profit': net_profit.round(-3).astype(int),
})
pl_df['Margin_Pct'] = (pl_df['Net_Profit'] / pl_df['Revenue'] * 100).round(2)
print(f'Monthly P&L Dataset β FY 2081-82 BS ({pl_df.shape[0]} months)')
print(pl_df.head())
Monthly P&L Dataset β FY 2081-82 BS (12 months)
Month Quarter Revenue COGS Gross_Profit Opex Net_Profit \
0 Shrawan Q1 922000 349000 573000 160000 413000
1 Bhadra Q1 1471000 706000 765000 160000 605000
2 Ashwin Q1 932000 337000 595000 235000 360000
3 Kartik Q2 1059000 524000 536000 132000 403000
4 Mangsir Q2 910000 489000 421000 92000 329000
Margin_Pct
0 44.79
1 41.13
2 38.63
3 38.05
4 36.15
print('--- Last 3 rows (tail) ---')
print(pl_df.tail(3))
print('\n--- Dataset Dimensions ---')
print('Rows x Columns:', pl_df.shape)
print('\n--- Column Names ---')
print(pl_df.columns.tolist())
print('\n--- Data Types of Each Column ---')
print(pl_df.dtypes)
--- Last 3 rows (tail) ---
Month Quarter Revenue COGS Gross_Profit Opex Net_Profit \
9 Baishakh Q4 1936000 680000 1256000 467000 789000
10 Jestha Q4 1713000 607000 1105000 371000 734000
11 Ashadh Q4 975000 444000 532000 154000 377000
Margin_Pct
9 40.75
10 42.85
11 38.67
--- Dataset Dimensions ---
Rows x Columns: (12, 8)
--- Column Names ---
['Month', 'Quarter', 'Revenue', 'COGS', 'Gross_Profit', 'Opex', 'Net_Profit', 'Margin_Pct']
--- Data Types of Each Column ---
Month object
Quarter object
Revenue int64
COGS int64
Gross_Profit int64
Opex int64
Net_Profit int64
Margin_Pct float64
dtype: object
# describe() β the most useful single command for financial data
# Gives count, mean, std, min, max, quartiles for all numeric columns
print('--- Statistical Summary ---')
print(pl_df[['Revenue', 'COGS', 'Gross_Profit', 'Net_Profit', 'Margin_Pct']].describe().round(2))
--- Statistical Summary ---
Revenue COGS Gross_Profit Net_Profit Margin_Pct
count 12.00 12.00 12.00 12.00 12.00
mean 1340833.33 584250.00 756916.67 522833.33 38.89
std 424008.33 208294.43 272493.36 175512.67 4.09
min 910000.00 337000.00 421000.00 298000.00 31.80
25% 935750.00 444000.00 535000.00 372750.00 37.00
50% 1265000.00 530500.00 680000.00 509000.00 38.65
75% 1734750.00 686500.00 961500.00 671500.00 41.56
max 1936000.00 1044000.00 1256000.00 789000.00 44.79
Section 11: Selecting Columns & Rows
Selecting columns
# Select a single column β returns a Series
print('Revenue column:')
print(pl_df['Revenue'])
print('\n--- Select multiple columns β returns a DataFrame ---')
print(pl_df[['Month', 'Revenue', 'Net_Profit', 'Margin_Pct']])
Revenue column:
0 922000
1 1471000
2 932000
3 1059000
4 910000
5 1532000
6 1903000
7 937000
8 1800000
9 1936000
10 1713000
11 975000
Name: Revenue, dtype: int64
--- Select multiple columns β returns a DataFrame ---
Month Revenue Net_Profit Margin_Pct
0 Shrawan 922000 413000 44.79
1 Bhadra 1471000 605000 41.13
2 Ashwin 932000 360000 38.63
3 Kartik 1059000 403000 38.05
4 Mangsir 910000 329000 36.15
5 Poush 1532000 673000 43.93
6 Magh 1903000 622000 32.69
7 Falgun 937000 298000 31.80
8 Chaitra 1800000 671000 37.28
9 Baishakh 1936000 789000 40.75
10 Jestha 1713000 734000 42.85
11 Ashadh 975000 377000 38.67
# Selecting rows using .loc[] (label-based) and .iloc[] (position-based)
# .iloc[] β by row NUMBER (like Excel row number, starts from 0)
print('First row (iloc[0]):')
print(pl_df.iloc[0])
print('\nRows 0 to 2 (iloc[0:3]):')
print(pl_df.iloc[0:3])
print('\nLast row:')
print(pl_df.iloc[-1])
First row (iloc[0]):
Month Shrawan
Quarter Q1
Revenue 922000
COGS 349000
Gross_Profit 573000
Opex 160000
Net_Profit 413000
Margin_Pct 44.79
Name: 0, dtype: object
Rows 0 to 2 (iloc[0:3]):
Month Quarter Revenue COGS Gross_Profit Opex Net_Profit \
0 Shrawan Q1 922000 349000 573000 160000 413000
1 Bhadra Q1 1471000 706000 765000 160000 605000
2 Ashwin Q1 932000 337000 595000 235000 360000
Margin_Pct
0 44.79
1 41.13
2 38.63
Last row:
Month Ashadh
Quarter Q4
Revenue 975000
COGS 444000
Gross_Profit 532000
Opex 154000
Net_Profit 377000
Margin_Pct 38.67
Name: 11, dtype: object
Section 12: Filtering Data β Like Excel AutoFilter
# Filter rows where Revenue > NPR 15 lakhs
high_revenue = pl_df[pl_df['Revenue'] > 1500000]
print('Months with revenue > NPR 15L:')
print(high_revenue[['Month', 'Revenue', 'Net_Profit']].to_string(index=False))
Months with revenue > NPR 15L:
Month Revenue Net_Profit
Poush 1532000 673000
Magh 1903000 622000
Chaitra 1800000 671000
Baishakh 1936000 789000
Jestha 1713000 734000
# Multiple conditions β using & (and) and | (or)
# Months where revenue > NPR 15L AND profit margin > 13%
strong_months = pl_df[(pl_df['Revenue'] > 1500000) & (pl_df['Margin_Pct'] > 13)]
print('Strong months (Revenue >NPR 15L AND Margin >13%):')
print(strong_months[['Month', 'Revenue', 'Margin_Pct']].to_string(index=False))
# Filter by text β Q1 months only
q1_data = pl_df[pl_df['Quarter'] == 'Q1']
print('\nQ1 Data:')
print(q1_data[['Month', 'Quarter', 'Revenue', 'Net_Profit']].to_string(index=False))
# isin() β filter where value is in a list (like Excel's multiple AutoFilter)
selected_quarters = pl_df[pl_df['Quarter'].isin(['Q1', 'Q4'])]
print('\nQ1 and Q4 data:')
print(selected_quarters[['Month', 'Quarter', 'Revenue']].to_string(index=False))
Strong months (Revenue >NPR 15L AND Margin >13%):
Month Revenue Margin_Pct
Poush 1532000 43.93
Magh 1903000 32.69
Chaitra 1800000 37.28
Baishakh 1936000 40.75
Jestha 1713000 42.85
Q1 Data:
Month Quarter Revenue Net_Profit
Shrawan Q1 922000 413000
Bhadra Q1 1471000 605000
Ashwin Q1 932000 360000
Q1 and Q4 data:
Month Quarter Revenue
Shrawan Q1 922000
Bhadra Q1 1471000
Ashwin Q1 932000
Baishakh Q4 1936000
Jestha Q4 1713000
Ashadh Q4 975000
Section 13: Sorting Data
# Sort by a column
sorted_by_revenue = pl_df.sort_values('Revenue', ascending=False)
print('Months ranked by Revenue (highest first):')
print(sorted_by_revenue[['Month', 'Revenue', 'Net_Profit', 'Margin_Pct']].to_string(index=False))
Months ranked by Revenue (highest first):
Month Revenue Net_Profit Margin_Pct
Baishakh 1936000 789000 40.75
Magh 1903000 622000 32.69
Chaitra 1800000 671000 37.28
Jestha 1713000 734000 42.85
Poush 1532000 673000 43.93
Bhadra 1471000 605000 41.13
Kartik 1059000 403000 38.05
Ashadh 975000 377000 38.67
Falgun 937000 298000 31.80
Ashwin 932000 360000 38.63
Shrawan 922000 413000 44.79
Mangsir 910000 329000 36.15
# Create a transaction dataset for more realistic examples
transactions = pd.DataFrame({
'Txn_ID': ['T001','T002','T003','T004','T005','T006','T007','T008','T009','T010'],
'Date': ['05-Shrawan','12-Shrawan','18-Shrawan','25-Shrawan','02-Bhadra','08-Bhadra','15-Bhadra','22-Bhadra','28-Bhadra','03-Ashwin'],
'Party': ['Himalayan Traders Pvt. Ltd.','Annapurna Enterprises Pvt. Ltd.','Sagarmatha Trading Pvt. Ltd.','Himalayan Traders Pvt. Ltd.','Everest Imports Pvt. Ltd.','Annapurna Enterprises Pvt. Ltd.','Barahi Industries Ltd.','Sagarmatha Trading Pvt. Ltd.','Himalayan Traders Pvt. Ltd.','Everest Imports Pvt. Ltd.'],
'Type': ['Sale','Sale','Purchase','Sale','Sale','Sale','Purchase','Purchase','Sale','Sale'],
'Amount': [85000, 42000, 67000, 115000, 78000, 93000, 54000, 38000, 127000, 65000],
'VAT_Rate': [13, 0, 13, 13, 0, 13, 13, 0, 13, 13],
'Status': ['Paid','Pending','Paid','Paid','Pending','Paid','Paid','Pending','Paid','Pending'],
})
transactions['VAT_Amount'] = (transactions['Amount'] * transactions['VAT_Rate'] / 100).round(2)
transactions['Total'] = transactions['Amount'] + transactions['VAT_Amount']
print(transactions.to_string(index=False))
Txn_ID Date Party Type Amount VAT_Rate Status VAT_Amount Total
T001 05-Shrawan Himalayan Traders Pvt. Ltd. Sale 85000 13 Paid 11050.0 96050.0
T002 12-Shrawan Annapurna Enterprises Pvt. Ltd. Sale 42000 0 Pending 0.0 42000.0
T003 18-Shrawan Sagarmatha Trading Pvt. Ltd. Purchase 67000 13 Paid 8710.0 75710.0
T004 25-Shrawan Himalayan Traders Pvt. Ltd. Sale 115000 13 Paid 14950.0 129950.0
T005 02-Bhadra Everest Imports Pvt. Ltd. Sale 78000 0 Pending 0.0 78000.0
T006 08-Bhadra Annapurna Enterprises Pvt. Ltd. Sale 93000 13 Paid 12090.0 105090.0
T007 15-Bhadra Barahi Industries Ltd. Purchase 54000 13 Paid 7020.0 61020.0
T008 22-Bhadra Sagarmatha Trading Pvt. Ltd. Purchase 38000 0 Pending 0.0 38000.0
T009 28-Bhadra Himalayan Traders Pvt. Ltd. Sale 127000 13 Paid 16510.0 143510.0
T010 03-Ashwin Everest Imports Pvt. Ltd. Sale 65000 13 Pending 8450.0 73450.0
Section 14: Adding Calculated Columns
Just like adding a formula column in Excel β but applied to the entire column instantly.
# Add new computed columns
transactions['Effective_Rate'] = (transactions['VAT_Amount'] / transactions['Amount'] * 100).round(1)
# Conditional column using np.where() β like Excel IF()
transactions['Risk_Flag'] = np.where(transactions['Amount'] > 100000, 'High Value', 'Normal')
# Using apply() for complex logic β applies a function to each row/column
def classify_status(row):
if row['Status'] == 'Pending' and row['Amount'] > 80000:
return 'URGENT FOLLOW-UP'
elif row['Status'] == 'Pending':
return 'Follow-up'
else:
return 'Cleared'
transactions['Action'] = transactions.apply(classify_status, axis=1)
print(transactions[['Txn_ID', 'Party', 'Amount', 'Status', 'Risk_Flag', 'Action']].to_string(index=False))
Txn_ID Party Amount Status Risk_Flag Action
T001 Himalayan Traders Pvt. Ltd. 85000 Paid Normal Cleared
T002 Annapurna Enterprises Pvt. Ltd. 42000 Pending Normal Follow-up
T003 Sagarmatha Trading Pvt. Ltd. 67000 Paid Normal Cleared
T004 Himalayan Traders Pvt. Ltd. 115000 Paid High Value Cleared
T005 Everest Imports Pvt. Ltd. 78000 Pending Normal Follow-up
T006 Annapurna Enterprises Pvt. Ltd. 93000 Paid Normal Cleared
T007 Barahi Industries Ltd. 54000 Paid Normal Cleared
T008 Sagarmatha Trading Pvt. Ltd. 38000 Pending Normal Follow-up
T009 Himalayan Traders Pvt. Ltd. 127000 Paid High Value Cleared
T010 Everest Imports Pvt. Ltd. 65000 Pending Normal Follow-up
Section 15: GroupBy β Like a Pivot Table
groupby() is one of Pandas' most powerful features. It is the equivalent of Excel's Pivot Table.
Concept: Split the data into groups β Apply a function to each group β Combine results
Think: "Total sales by party", "Average invoice by GST rate", "Count of pending vs paid"
# Group by Party β total business with each party
party_summary = transactions.groupby('Party')['Amount'].sum().sort_values(ascending=False)
print('Total Business by Party:')
print(party_summary)
print()
# Group by Type (Sale/Purchase) β net position
type_summary = transactions.groupby('Type')[['Amount', 'VAT_Amount', 'Total']].sum()
print('Summary by Transaction Type:')
print(type_summary)
Total Business by Party:
Party
Himalayan Traders Pvt. Ltd. 327000
Everest Imports Pvt. Ltd. 143000
Annapurna Enterprises Pvt. Ltd. 135000
Sagarmatha Trading Pvt. Ltd. 105000
Barahi Industries Ltd. 54000
Name: Amount, dtype: int64
Summary by Transaction Type:
Amount VAT_Amount Total
Type
Purchase 159000 15730.0 174730.0
Sale 605000 63050.0 668050.0
# Multiple aggregations at once β like Excel Pivot Table with multiple Value fields
party_detail = transactions.groupby('Party').agg(
Transaction_Count = ('Txn_ID', 'count'),
Total_Amount = ('Amount', 'sum'),
Average_Amount = ('Amount', 'mean'),
Max_Transaction = ('Amount', 'max'),
Total_GST = ('VAT_Amount', 'sum')
).round(2)
print('Detailed Party-wise Analysis:')
print(party_detail.to_string())
Detailed Party-wise Analysis:
Transaction_Count Total_Amount Average_Amount Max_Transaction Total_GST
Party
Annapurna Enterprises Pvt. Ltd. 2 135000 67500.0 93000 12090.0
Barahi Industries Ltd. 1 54000 54000.0 54000 7020.0
Everest Imports Pvt. Ltd. 2 143000 71500.0 78000 8450.0
Himalayan Traders Pvt. Ltd. 3 327000 109000.0 127000 42510.0
Sagarmatha Trading Pvt. Ltd. 2 105000 52500.0 67000 8710.0
# GroupBy with pivot table β GST liability by rate and type
gst_pivot = transactions.pivot_table(
values = 'VAT_Amount',
index = 'VAT_Rate',
columns = 'Type',
aggfunc = 'sum',
fill_value = 0
)
print('VAT Summary by Rate and Type:')
print(gst_pivot)
print()
# GST payable = Output GST (Sales) - Input GST (Purchases)
gst_pivot['Net_GST_Payable'] = gst_pivot.get('Sale', 0) - gst_pivot.get('Purchase', 0)
print('Net GST Payable:')
print(gst_pivot)
VAT Summary by Rate and Type:
Type Purchase Sale
VAT_Rate
0 0.0 0.0
13 15730.0 63050.0
Net GST Payable:
Type Purchase Sale Net_GST_Payable
VAT_Rate
0 0.0 0.0 0.0
13 15730.0 63050.0 47320.0
# Monthly P&L grouped by Quarter β like a quarterly summary pivot table
quarterly = pl_df.groupby('Quarter').agg(
Total_Revenue = ('Revenue', 'sum'),
Total_Expenses = ('Opex', 'sum'),
Total_Net_Profit = ('Net_Profit', 'sum'),
Avg_Margin = ('Margin_Pct', 'mean')
).round(2)
quarterly['Revenue_Share_%'] = (quarterly['Total_Revenue'] / quarterly['Total_Revenue'].sum() * 100).round(1)
print('Quarterly P&L Summary:')
print(quarterly.to_string())
print(f"\nFull Year Net Profit: NPR {quarterly['Total_Net_Profit'].sum():,.0f}")
Quarterly P&L Summary:
Total_Revenue Total_Expenses Total_Net_Profit Avg_Margin Revenue_Share_%
Quarter
Q1 3325000 555000 1378000 41.52 20.7
Q2 3501000 546000 1405000 39.38 21.8
Q3 4640000 713000 1591000 33.92 28.8
Q4 4624000 992000 1900000 40.76 28.7
Full Year Net Profit: NPR 6,274,000
Section 16: Handling Missing Values
Real-world financial data is almost always incomplete β missing values are a fact of life.
Pandas uses NaN (Not a Number) to represent missing data.
# Create a DataFrame with missing values (typical in imported data)
incomplete_data = pd.DataFrame({
'Invoice_No': ['INV-001', 'INV-002', 'INV-003', 'INV-004', 'INV-005', 'INV-006'],
'Party': ['Alpha Ltd', None, 'Everest Imports Pvt. Ltd.', 'Delta Corp', None, 'Zeta Co'],
'Amount': [45000, 82000, None, 67000, 92000, None],
'VAT_Rate': [18, 12, 18, None, 5, 18],
'Due_Date': ['30-Apr', '15-May', None, '01-Jun', '10-May', '25-Jun'],
})
print('Raw data with missing values:')
print(incomplete_data)
print('\nMissing value count per column:')
print(incomplete_data.isnull().sum())
print('\nAny missing values?', incomplete_data.isnull().any().any())
Raw data with missing values:
Invoice_No Party Amount VAT_Rate Due_Date
0 INV-001 Alpha Ltd 45000.0 18.0 30-Apr
1 INV-002 None 82000.0 12.0 15-May
2 INV-003 Everest Imports Pvt. Ltd. NaN 18.0 None
3 INV-004 Delta Corp 67000.0 NaN 01-Jun
4 INV-005 None 92000.0 5.0 10-May
5 INV-006 Zeta Co NaN 18.0 25-Jun
Missing value count per column:
Invoice_No 0
Party 2
Amount 2
VAT_Rate 1
Due_Date 1
dtype: int64
Any missing values? True
# Strategies to handle missing values
df_clean = incomplete_data.copy()
# Strategy 1: Fill with a specific value
df_clean['Party'] = df_clean['Party'].fillna('UNKNOWN')
df_clean['VAT_Rate'] = df_clean['VAT_Rate'].fillna(13) # Default to 13% (Nepal standard VAT)
df_clean['Due_Date'] = df_clean['Due_Date'].fillna('TBD')
# Strategy 2: Fill with the column average
df_clean['Amount'] = df_clean['Amount'].fillna(df_clean['Amount'].mean())
# Strategy 3: Drop rows with any remaining missing values
# df_clean = df_clean.dropna() # Use this if you want to remove incomplete rows
print('After cleaning:')
print(df_clean)
print('\nMissing values remaining:', df_clean.isnull().sum().sum())
After cleaning:
Invoice_No Party Amount VAT_Rate Due_Date
0 INV-001 Alpha Ltd 45000.0 18.0 30-Apr
1 INV-002 UNKNOWN 82000.0 12.0 15-May
2 INV-003 Everest Imports Pvt. Ltd. 71500.0 18.0 TBD
3 INV-004 Delta Corp 67000.0 13.0 01-Jun
4 INV-005 UNKNOWN 92000.0 5.0 10-May
5 INV-006 Zeta Co 71500.0 18.0 25-Jun
Missing values remaining: 0
Section 17: Merging DataFrames β Like VLOOKUP
VLOOKUP in Excel pulls data from one table into another using a matching key.
In Pandas, merge() does the same β and handles millions of rows instantly.
# Table 1: Invoice Register
invoices = pd.DataFrame({
'Invoice_No': ['INV-001', 'INV-002', 'INV-003', 'INV-004', 'INV-005'],
'Client_ID': ['C001', 'C003', 'C002', 'C001', 'C004'],
'Amount': [85000, 42000, 120000, 67000, 95000],
'Date': ['05-Shrawan', '08-Shrawan', '12-Shrawan', '18-Shrawan', '22-Shrawan'],
})
# Table 2: Client Master (like a lookup table)
clients = pd.DataFrame({
'Client_ID': ['C001', 'C002', 'C003', 'C004'],
'Client_Name': ['Sunrise Manufacturing', 'Blue Ocean Pvt Ltd', 'Alpha Traders', 'Tech Solutions'],
'City': ['Kathmandu', 'Lalitpur', 'Pokhara', 'Kathmandu'],
'VAT_Rate': [13, 0, 13, 13], # 0% for exempt, 13% standard Nepal VAT
})
print('Invoice Table:')
print(invoices)
print('\nClient Master:')
print(clients)
Invoice Table:
Invoice_No Client_ID Amount Date
0 INV-001 C001 85000 05-Shrawan
1 INV-002 C003 42000 08-Shrawan
2 INV-003 C002 120000 12-Shrawan
3 INV-004 C001 67000 18-Shrawan
4 INV-005 C004 95000 22-Shrawan
Client Master:
Client_ID Client_Name City VAT_Rate
0 C001 Sunrise Manufacturing Kathmandu 13
1 C002 Blue Ocean Pvt Ltd Lalitpur 0
2 C003 Alpha Traders Pokhara 13
3 C004 Tech Solutions Kathmandu 13
# MERGE β joins the two tables on Client_ID (like VLOOKUP using Client_ID)
# how='left' means: keep ALL rows from invoices, add matching data from clients
merged = invoices.merge(clients, on='Client_ID', how='left')
# Calculate GST using the rate from client master
merged['VAT_Amount'] = (merged['Amount'] * merged['VAT_Rate'] / 100).round(2)
merged['Total'] = merged['Amount'] + merged['VAT_Amount']
print('Enriched Invoice Register (after merge):')
print(merged[['Invoice_No', 'Client_Name', 'City', 'Amount', 'VAT_Rate', 'VAT_Amount', 'Total']].to_string(index=False))
Enriched Invoice Register (after merge):
Invoice_No Client_Name City Amount VAT_Rate VAT_Amount Total
INV-001 Sunrise Manufacturing Kathmandu 85000 13 11050.0 96050.0
INV-002 Alpha Traders Pokhara 42000 13 5460.0 47460.0
INV-003 Blue Ocean Pvt Ltd Lalitpur 120000 0 0.0 120000.0
INV-004 Sunrise Manufacturing Kathmandu 67000 13 8710.0 75710.0
INV-005 Tech Solutions Kathmandu 95000 13 12350.0 107350.0
Section 18: Reading & Writing Excel / CSV Files
In practice, you will almost always import data from Excel or CSV rather than typing it manually.
Pandas makes this effortless.
# First, let's save our DataFrame to files
# Save as CSV
pl_df.to_csv('pl_data.csv', index=False) # index=False prevents writing row numbers
print('Saved pl_data.csv')
# Save as Excel (requires openpyxl: pip install openpyxl)
pl_df.to_excel('pl_data.xlsx', sheet_name='P&L', index=False)
print('Saved pl_data.xlsx')
# Save multiple sheets in one Excel file
with pd.ExcelWriter('financial_report.xlsx', engine='openpyxl') as writer:
pl_df.to_excel(writer, sheet_name='Monthly P&L', index=False)
transactions.to_excel(writer, sheet_name='Transactions', index=False)
quarterly.to_excel(writer, sheet_name='Quarterly Summary')
print('Saved financial_report.xlsx (3 sheets)')
Saved pl_data.csv
Saved pl_data.xlsx
Saved financial_report.xlsx (3 sheets)
# Reading back the files
# Read CSV
df_from_csv = pd.read_csv('pl_data.csv')
print('Read from CSV β shape:', df_from_csv.shape)
print(df_from_csv.head(3))
# Read Excel
df_from_excel = pd.read_excel('pl_data.xlsx', sheet_name='P&L')
print('\nRead from Excel β shape:', df_from_excel.shape)
Read from CSV β shape: (12, 8)
Month Quarter Revenue COGS Gross_Profit Opex Net_Profit \
0 Shrawan Q1 922000 349000 573000 160000 413000
1 Bhadra Q1 1471000 706000 765000 160000 605000
2 Ashwin Q1 932000 337000 595000 235000 360000
Margin_Pct
0 44.79
1 41.13
2 38.63
Read from Excel β shape: (12, 8)
# Common read_csv / read_excel options you'll use in practice
# Skip first 2 rows (e.g., if Excel has a title in row 1)
# df = pd.read_csv('file.csv', skiprows=2)
# Read only specific columns
# df = pd.read_csv('file.csv', usecols=['Date', 'Amount', 'Party'])
# Parse date columns automatically
# df = pd.read_csv('file.csv', parse_dates=['Date'])
# Handle encoding issues (common with Indian data files)
# df = pd.read_csv('file.csv', encoding='latin-1')
print('Common read options demonstrated above (commented out).')
print('Uncomment and adapt for your actual files.')
Common read options demonstrated above (commented out).
Uncomment and adapt for your actual files.
Section 19: String Operations on Columns
Pandas has powerful text processing via .str accessor β extremely useful for cleaning imported data.
# Simulating messy imported data (very common in practice)
raw_data = pd.DataFrame({
'Party_Name': [' ALPHA LTD ', 'beta corp', 'GAMMA INC.', ' Delta Pvt Ltd', 'ZETA CO'],
'PAN': ['aaact0001a', 'BBBBB0002B', 'ccccc0003c', 'DDDDD0004d', 'eeeee0005E'],
'City': ['KATHMANDU', 'lalitpur', 'Pokhara', 'BIRATNAGAR', 'Biratnagar'],
'Amount_str': ['NPR 45,000', 'NPR 82,000', 'NPR 1,20,000', 'NPR 67,500', 'NPR 95,000'], # Amount as text!
})
print('Raw (messy) data:')
print(raw_data)
print()
# Clean it up using .str accessor
raw_data['Party_Name'] = raw_data['Party_Name'].str.strip().str.title() # Remove spaces, proper case
raw_data['PAN'] = raw_data['PAN'].str.upper() # PAN must be uppercase
raw_data['City'] = raw_data['City'].str.title() # Proper case
# Convert amount string to number
raw_data['Amount'] = raw_data['Amount_str'].str.replace('NPR ', '').str.replace(',', '').astype(int)
raw_data = raw_data.drop('Amount_str', axis=1) # Remove old column
print('Cleaned data:')
print(raw_data)
Raw (messy) data:
Party_Name PAN City Amount_str
0 ALPHA LTD aaact0001a KATHMANDU NPR 45,000
1 beta corp BBBBB0002B lalitpur NPR 82,000
2 GAMMA INC. ccccc0003c Pokhara NPR 1,20,000
3 Delta Pvt Ltd DDDDD0004d BIRATNAGAR NPR 67,500
4 ZETA CO eeeee0005E Biratnagar NPR 95,000
Cleaned data:
Party_Name PAN City Amount
0 Alpha Ltd AAACT0001A Kathmandu 45000
1 Beta Corp BBBBB0002B Lalitpur 82000
2 Gamma Inc. CCCCC0003C Pokhara 120000
3 Delta Pvt Ltd DDDDD0004D Biratnagar 67500
4 Zeta Co EEEEE0005E Biratnagar 95000
# More string operations useful in audit/finance
parties = pd.Series(['Nepal Telecom (NTC)', 'Nabil Bank Ltd.',
'CG Corp Global', 'Yomari Digital Services', 'Nepal Life Insurance Co. Ltd.'])
# Check if string contains something
print('Contains "Ltd":')
print(parties[parties.str.contains('Ltd', case=False)])
# Extract first word (could be group name)
print('\nFirst word of each name:')
print(parties.str.split().str[0].tolist())
# Count characters
print('\nName lengths:', parties.str.len().tolist())
Contains "Ltd":
1 Nabil Bank Ltd.
4 Nepal Life Insurance Co. Ltd.
dtype: object
First word of each name:
['Nepal', 'Nabil', 'CG', 'Yomari', 'Nepal']
Name lengths: [19, 15, 14, 23, 29]
Section 20: Practice Exercises
ποΈ Exercise 1 β GST VAT-401 Summary
You have an invoice register. Create a VAT-401 style summary showing: - Total taxable value, VAT, VAT_Local, and total invoice value by GST rate slab - The top 3 clients by invoice value - All pending (unpaid) invoices over NPR 50,000
import pandas as pd
import numpy as np
invoice_register = pd.DataFrame({
'Invoice_No': ['INV-001','INV-002','INV-003','INV-004','INV-005',
'INV-006','INV-007','INV-008','INV-009','INV-010'],
'Client': ['Alpha Ltd','Beta Corp','Alpha Ltd','Everest Imports Pvt. Ltd.','Delta Pvt',
'Beta Corp','Everest Imports Pvt. Ltd.','Alpha Ltd','Delta Pvt','Beta Corp'],
'Date': ['05-Shrawan','08-Shrawan','12-Shrawan','15-Shrawan','18-Shrawan',
'22-Shrawan','25-Shrawan','28-Shrawan','01-Bhadra','05-Bhadra'],
'Taxable_Value': [85000, 42000, 120000, 67000, 95000,
55000, 145000, 38000, 82000, 110000],
'VAT_Rate': [13, 0, 13, 13, 0, 13, 13, 0, 13, 13],
'Status': ['Paid','Pending','Paid','Paid','Pending',
'Paid','Pending','Paid','Paid','Pending'],
})
# Write your VAT-401 analysis here
ποΈ Exercise 2 β Trial Balance Validation
A trial balance should always balance: Total Debits = Total Credits.
From the data below:
1. Check if the trial balance is balanced
2. Identify accounts with unusually high balances (> NPR 10 lakhs)
3. Summarise totals by account type (Assets, Liabilities, Income, Expenses)
4. Export a clean formatted Excel file named trial_balance_clean.xlsx
import pandas as pd
trial_balance = pd.DataFrame({
'Account': ['Cash & Bank','Trade Debtors','Inventory','Fixed Assets',
'Trade Creditors','Loans Payable','Share Capital','Retained Earnings',
'Sales Revenue','Other Income','COGS','Salaries','Rent','Depreciation','Finance Cost'],
'Type': ['Asset','Asset','Asset','Asset',
'Liability','Liability','Liability','Liability',
'Income','Income','Expense','Expense','Expense','Expense','Expense'],
'Debit': [485000, 1250000, 2100000, 8500000,
0, 0, 0, 0,
0, 0, 1800000, 960000, 420000, 240000, 150000],
'Credit': [0, 0, 0, 0,
980000, 2500000, 5000000, 650000,
5200000, 325000, 0, 0, 0, 0, 0],
})
# Write your trial balance analysis here
π‘ Solutions
# SOLUTION β Exercise 1: VAT-401 Summary
import pandas as pd
import numpy as np
invoice_register = pd.DataFrame({
'Invoice_No': ['INV-001','INV-002','INV-003','INV-004','INV-005',
'INV-006','INV-007','INV-008','INV-009','INV-010'],
'Client': ['Alpha Ltd','Beta Corp','Alpha Ltd','Everest Imports Pvt. Ltd.','Delta Pvt',
'Beta Corp','Everest Imports Pvt. Ltd.','Alpha Ltd','Delta Pvt','Beta Corp'],
'Taxable_Value': [85000, 42000, 120000, 67000, 95000, 55000, 145000, 38000, 82000, 110000],
'VAT_Rate': [13, 0, 13, 13, 0, 13, 13, 0, 13, 13],
'Status': ['Paid','Pending','Paid','Paid','Pending','Paid','Pending','Paid','Paid','Pending'],
})
invoice_register['VAT_Amount'] = invoice_register['Taxable_Value'] * invoice_register['VAT_Rate'] / 100
invoice_register['VAT'] = invoice_register['VAT_Amount'] / 2
invoice_register['VAT_Local'] = invoice_register['VAT_Amount'] / 2
invoice_register['Total'] = invoice_register['Taxable_Value'] + invoice_register['VAT_Amount']
# VAT-401 by rate
gstr1 = invoice_register.groupby('VAT_Rate').agg(
Taxable_Value = ('Taxable_Value','sum'),
VAT = ('VAT','sum'),
VAT_Local = ('VAT_Local','sum'),
Total = ('Total','sum')
)
print('VAT-401 Summary by VAT Rate:')
print(gstr1)
# Top 3 clients
print('\nTop 3 Clients by Invoice Value:')
print(invoice_register.groupby('Client')['Total'].sum().nlargest(3))
# Pending over NPR 50K
print('\nPending Invoices > NPR 50,000:')
pending = invoice_register[(invoice_register['Status']=='Pending') & (invoice_register['Taxable_Value']>50000)]
print(pending[['Invoice_No','Client','Taxable_Value','Status']].to_string(index=False))
VAT-401 Summary by VAT Rate:
Taxable_Value VAT VAT_Local Total
VAT_Rate
0 175000 0.0 0.0 175000.0
13 664000 43160.0 43160.0 750320.0
Top 3 Clients by Invoice Value:
Client
Alpha Ltd 269650.0
Everest Imports Pvt. Ltd. 239560.0
Beta Corp 228450.0
Name: Total, dtype: float64
Pending Invoices > NPR 50,000:
Invoice_No Client Taxable_Value Status
INV-005 Delta Pvt 95000 Pending
INV-007 Everest Imports Pvt. Ltd. 145000 Pending
INV-010 Beta Corp 110000 Pending
# SOLUTION β Exercise 2: Trial Balance Validation
import pandas as pd
trial_balance = pd.DataFrame({
'Account': ['Cash & Bank','Trade Debtors','Inventory','Fixed Assets',
'Trade Creditors','Loans Payable','Share Capital','Retained Earnings',
'Sales Revenue','Other Income','COGS','Salaries','Rent','Depreciation','Finance Cost'],
'Type': ['Asset','Asset','Asset','Asset','Liability','Liability','Liability','Liability',
'Income','Income','Expense','Expense','Expense','Expense','Expense'],
'Debit': [485000,1250000,2100000,8500000,0,0,0,0,0,0,1800000,960000,420000,240000,150000],
'Credit': [0,0,0,0,980000,2500000,5000000,650000,5200000,325000,0,0,0,0,0],
})
trial_balance['Balance'] = trial_balance['Debit'] - trial_balance['Credit']
# Check balance
total_debit = trial_balance['Debit'].sum()
total_credit = trial_balance['Credit'].sum()
print(f'Total Debits: NPR {total_debit:>12,}')
print(f'Total Credits: NPR {total_credit:>12,}')
print(f'Difference: NPR {total_debit - total_credit:>12,}')
print('Balanced?', 'YES β' if total_debit == total_credit else 'NO β β Check data!')
# High balance accounts
print('\nAccounts with Balance > NPR 10 Lakhs:')
high_bal = trial_balance[abs(trial_balance['Balance']) > 1000000]
print(high_bal[['Account', 'Type', 'Debit', 'Credit', 'Balance']].to_string(index=False))
# By type
print('\nSummary by Account Type:')
type_summary = trial_balance.groupby('Type')[['Debit','Credit']].sum()
print(type_summary)
# Export
trial_balance.to_excel('trial_balance_clean.xlsx', index=False)
print('\nExported to trial_balance_clean.xlsx')
Total Debits: NPR 15,905,000
Total Credits: NPR 14,655,000
Difference: NPR 1,250,000
Balanced? NO β β Check data!
Accounts with Balance > NPR 10 Lakhs:
Account Type Debit Credit Balance
Trade Debtors Asset 1250000 0 1250000
Inventory Asset 2100000 0 2100000
Fixed Assets Asset 8500000 0 8500000
Loans Payable Liability 0 2500000 -2500000
Share Capital Liability 0 5000000 -5000000
Sales Revenue Income 0 5200000 -5200000
COGS Expense 1800000 0 1800000
Summary by Account Type:
Debit Credit
Type
Asset 12335000 0
Expense 3570000 0
Income 0 5525000
Liability 0 9130000
Exported to trial_balance_clean.xlsx
π Module 3 Complete!
What you've learned
| Concept | Real-world Use |
|---|---|
| Series & DataFrame | Working with financial tables |
| Exploring data | Understanding imported files quickly |
| Filtering | Transaction sampling, audit selection |
| GroupBy | VAT-401, party-wise summary, P&L by segment |
| Pivot tables | Quarterly dashboards |
| Missing values | Cleaning imported data |
| Merge | VLOOKUP replacement |
| Read/Write Excel | Real workflow automation |
| String operations | Data standardisation |
Next up β Module 4: Matplotlib β Visualise all this data with professional charts!
Python for CA Professionals β Module 3: Pandas