{
"cells": [
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000001",
"metadata": {},
"source": [
"# Audit Sampling\n",
"\n",
"Auditors rarely test every transaction or record in a population. Testing everything is time-consuming and often unnecessary — a well-chosen sample can give you the same level of confidence at a fraction of the effort.\n",
"\n",
"This notebook covers:\n",
"1. Why auditors sample, and key concepts\n",
"2. How to calculate an appropriate sample size\n",
"3. Three sampling methods: random, systematic, and stratified\n",
"4. How to export your sample with metadata for documentation\n",
"\n",
"We'll use a dataset of 100 vendor transactions as our population throughout."
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000002",
"metadata": {},
"source": [
"## 1. Why Auditors Sample\n",
"\n",
"**Population** — the full set of records you're auditing (e.g., all 5,000 invoices processed this year).\n",
"\n",
"**Sample** — a subset of that population you actually test.\n",
"\n",
"**Confidence level** — how certain you want to be that your sample reflects the population. Audits typically use 90% or 95%.\n",
"\n",
"**Tolerable error rate** — the maximum rate of errors you'd accept before concluding a control has failed. Common values are 5% or 10%.\n",
"\n",
"The core tradeoff: a higher confidence level or lower tolerable error rate means you need a larger sample. The relationship isn't linear — going from 90% to 95% confidence increases your sample size more than you might expect.\n",
"\n",
"**When to sample vs. test everything:**\n",
"- Sample when the population is large and testing everything isn't practical\n",
"- Test everything (100%) when the population is small (e.g., only 10 journal entries), when the control only fires occasionally, or when the risk is very high"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000003",
"metadata": {},
"source": [
"## 2. Load the Population"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "7ab8b26e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Defaulting to user installation because normal site-packages is not writeable\n",
"Requirement already satisfied: pandas in /Users/cmc/Library/Python/3.9/lib/python/site-packages (2.3.3)\n",
"Collecting openpyxl\n",
" Downloading openpyxl-3.1.5-py2.py3-none-any.whl.metadata (2.5 kB)\n",
"Requirement already satisfied: numpy>=1.22.4 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (2.0.2)\n",
"Requirement already satisfied: python-dateutil>=2.8.2 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (2.9.0.post0)\n",
"Requirement already satisfied: pytz>=2020.1 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (2025.2)\n",
"Requirement already satisfied: tzdata>=2022.7 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (2025.3)\n",
"Collecting et-xmlfile (from openpyxl)\n",
" Downloading et_xmlfile-2.0.0-py3-none-any.whl.metadata (2.7 kB)\n",
"Requirement already satisfied: six>=1.5 in /Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/site-packages (from python-dateutil>=2.8.2->pandas) (1.15.0)\n",
"Downloading openpyxl-3.1.5-py2.py3-none-any.whl (250 kB)\n",
"Downloading et_xmlfile-2.0.0-py3-none-any.whl (18 kB)\n",
"Installing collected packages: et-xmlfile, openpyxl\n",
"\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m2/2\u001b[0m [openpyxl]\n",
"\u001b[1A\u001b[2KSuccessfully installed et-xmlfile-2.0.0 openpyxl-3.1.5\n",
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"%pip install pandas openpyxl"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "a1b2c3d4-0001-0001-0001-000000000004",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Population size: 100 records\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Transaction_ID | \n",
" Date | \n",
" Vendor | \n",
" Amount | \n",
" Department | \n",
" Approved_By | \n",
" Payment_Method | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" T0001 | \n",
" 2024-01-03 | \n",
" Staples | \n",
" 124.50 | \n",
" Marketing | \n",
" J. Rivera | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 1 | \n",
" T0002 | \n",
" 2024-01-05 | \n",
" AWS | \n",
" 3200.00 | \n",
" Engineering | \n",
" S. Patel | \n",
" ACH | \n",
"
\n",
" \n",
" | 2 | \n",
" T0003 | \n",
" 2024-01-07 | \n",
" Office Depot | \n",
" 87.25 | \n",
" HR | \n",
" M. Chen | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 3 | \n",
" T0004 | \n",
" 2024-01-08 | \n",
" Delta Airlines | \n",
" 1450.00 | \n",
" Sales | \n",
" J. Rivera | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 4 | \n",
" T0005 | \n",
" 2024-01-10 | \n",
" Adobe | \n",
" 599.99 | \n",
" Marketing | \n",
" M. Chen | \n",
" ACH | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Transaction_ID Date Vendor Amount Department \\\n",
"0 T0001 2024-01-03 Staples 124.50 Marketing \n",
"1 T0002 2024-01-05 AWS 3200.00 Engineering \n",
"2 T0003 2024-01-07 Office Depot 87.25 HR \n",
"3 T0004 2024-01-08 Delta Airlines 1450.00 Sales \n",
"4 T0005 2024-01-10 Adobe 599.99 Marketing \n",
"\n",
" Approved_By Payment_Method \n",
"0 J. Rivera Credit Card \n",
"1 S. Patel ACH \n",
"2 M. Chen Credit Card \n",
"3 J. Rivera Credit Card \n",
"4 M. Chen ACH "
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import pandas as pd\n",
"import math\n",
"\n",
"# Load the transaction population\n",
"df = pd.read_csv('transactions.csv')\n",
"\n",
"print(f\"Population size: {len(df)} records\")\n",
"df.head()"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "a1b2c3d4-0001-0001-0001-000000000005",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Transactions by Department:\n",
"Department\n",
"Sales 23\n",
"Engineering 20\n",
"Operations 16\n",
"Marketing 15\n",
"HR 14\n",
"IT 12\n",
"Name: count, dtype: int64\n",
"\n",
"Amount summary:\n",
"count 100.00\n",
"mean 1287.50\n",
"std 1856.76\n",
"min 22.60\n",
"25% 91.64\n",
"50% 420.00\n",
"75% 1670.00\n",
"max 6500.00\n",
"Name: Amount, dtype: float64\n"
]
}
],
"source": [
"# Get a quick overview of the population before sampling\n",
"print(\"Transactions by Department:\")\n",
"print(df['Department'].value_counts())\n",
"print()\n",
"print(\"Amount summary:\")\n",
"print(df['Amount'].describe().round(2))"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000006",
"metadata": {},
"source": [
"## 3. Calculate Sample Size\n",
"\n",
"A common formula for attribute sampling (testing whether something is present or absent, like an approval) is:\n",
"\n",
"$$n = \\frac{Z^2 \\times p \\times (1 - p)}{E^2}$$\n",
"\n",
"Where:\n",
"- **Z** = Z-score for your confidence level (1.645 for 90%, 1.96 for 95%)\n",
"- **p** = expected error rate in the population (use 0.5 if unknown — this gives the most conservative/largest sample)\n",
"- **E** = tolerable error rate (e.g., 0.05 for 5%)\n",
"\n",
"This formula assumes an infinite population. For smaller populations, apply the **finite population correction (FPC)**:\n",
"\n",
"$$n_{adjusted} = \\frac{n}{1 + \\frac{n - 1}{N}}$$\n",
"\n",
"Where **N** is the population size."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "a1b2c3d4-0001-0001-0001-000000000007",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Population size: 100\n",
"Confidence level: 95%\n",
"Tolerable error rate: 5%\n",
"Base sample size: 385\n",
"Adjusted sample size: 80 (after finite population correction)\n"
]
}
],
"source": [
"# --- Configure your sampling parameters here ---\n",
"CONFIDENCE_LEVEL = 0.95 # 90% = 0.90, 95% = 0.95\n",
"TOLERABLE_ERROR = 0.05 # 5% = 0.05, 10% = 0.10\n",
"EXPECTED_ERROR = 0.5 # Use 0.5 (most conservative) if unknown\n",
"# ------------------------------------------------\n",
"\n",
"POPULATION_SIZE = len(df)\n",
"\n",
"# Z-scores for common confidence levels\n",
"z_scores = {0.90: 1.645, 0.95: 1.96, 0.99: 2.576}\n",
"z = z_scores.get(CONFIDENCE_LEVEL)\n",
"\n",
"if z is None:\n",
" raise ValueError(\"Confidence level must be 0.90, 0.95, or 0.99\")\n",
"\n",
"# Base sample size (infinite population)\n",
"n_base = (z**2 * EXPECTED_ERROR * (1 - EXPECTED_ERROR)) / (TOLERABLE_ERROR**2)\n",
"\n",
"# Finite population correction\n",
"n_adjusted = n_base / (1 + (n_base - 1) / POPULATION_SIZE)\n",
"SAMPLE_SIZE = math.ceil(n_adjusted)\n",
"\n",
"print(f\"Population size: {POPULATION_SIZE}\")\n",
"print(f\"Confidence level: {int(CONFIDENCE_LEVEL * 100)}%\")\n",
"print(f\"Tolerable error rate: {int(TOLERABLE_ERROR * 100)}%\")\n",
"print(f\"Base sample size: {math.ceil(n_base)}\")\n",
"print(f\"Adjusted sample size: {SAMPLE_SIZE} (after finite population correction)\")"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000008",
"metadata": {},
"source": [
"Notice that the adjusted sample size is smaller than the base. When your population is small relative to the uncorrected sample size, the FPC has a meaningful impact. For very large populations, the correction is negligible.\n",
"\n",
"We'll use this `SAMPLE_SIZE` across all three sampling methods below."
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000009",
"metadata": {},
"source": [
"## 4. Method 1 — Random Sampling\n",
"\n",
"Every item in the population has an equal chance of being selected. This is the simplest method and appropriate for most general audit tests where the population is relatively homogeneous.\n",
"\n",
"We set a `random_state` so the sample is reproducible — running the notebook again will produce the same selection, which is important for documentation and review."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "a1b2c3d4-0001-0001-0001-000000000010",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Random sample: 80 records\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Transaction_ID | \n",
" Date | \n",
" Vendor | \n",
" Amount | \n",
" Department | \n",
" Approved_By | \n",
" Payment_Method | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" T0001 | \n",
" 2024-01-03 | \n",
" Staples | \n",
" 124.50 | \n",
" Marketing | \n",
" J. Rivera | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 1 | \n",
" T0004 | \n",
" 2024-01-08 | \n",
" Delta Airlines | \n",
" 1450.00 | \n",
" Sales | \n",
" J. Rivera | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 2 | \n",
" T0005 | \n",
" 2024-01-10 | \n",
" Adobe | \n",
" 599.99 | \n",
" Marketing | \n",
" M. Chen | \n",
" ACH | \n",
"
\n",
" \n",
" | 3 | \n",
" T0006 | \n",
" 2024-01-11 | \n",
" Zoom | \n",
" 149.00 | \n",
" IT | \n",
" S. Patel | \n",
" ACH | \n",
"
\n",
" \n",
" | 4 | \n",
" T0007 | \n",
" 2024-01-14 | \n",
" FedEx | \n",
" 62.10 | \n",
" Operations | \n",
" L. Gomez | \n",
" Credit Card | \n",
"
\n",
" \n",
" | ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
"
\n",
" \n",
" | 75 | \n",
" T0096 | \n",
" 2024-05-20 | \n",
" Slack | \n",
" 320.00 | \n",
" IT | \n",
" S. Patel | \n",
" ACH | \n",
"
\n",
" \n",
" | 76 | \n",
" T0097 | \n",
" 2024-05-21 | \n",
" FedEx | \n",
" 41.70 | \n",
" Operations | \n",
" L. Gomez | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 77 | \n",
" T0098 | \n",
" 2024-05-22 | \n",
" Delta Airlines | \n",
" 1730.00 | \n",
" Sales | \n",
" J. Rivera | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 78 | \n",
" T0099 | \n",
" 2024-05-23 | \n",
" Office Depot | \n",
" 119.50 | \n",
" HR | \n",
" M. Chen | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 79 | \n",
" T0100 | \n",
" 2024-05-24 | \n",
" Salesforce | \n",
" 6500.00 | \n",
" Sales | \n",
" J. Rivera | \n",
" ACH | \n",
"
\n",
" \n",
"
\n",
"
80 rows × 7 columns
\n",
"
"
],
"text/plain": [
" Transaction_ID Date Vendor Amount Department \\\n",
"0 T0001 2024-01-03 Staples 124.50 Marketing \n",
"1 T0004 2024-01-08 Delta Airlines 1450.00 Sales \n",
"2 T0005 2024-01-10 Adobe 599.99 Marketing \n",
"3 T0006 2024-01-11 Zoom 149.00 IT \n",
"4 T0007 2024-01-14 FedEx 62.10 Operations \n",
".. ... ... ... ... ... \n",
"75 T0096 2024-05-20 Slack 320.00 IT \n",
"76 T0097 2024-05-21 FedEx 41.70 Operations \n",
"77 T0098 2024-05-22 Delta Airlines 1730.00 Sales \n",
"78 T0099 2024-05-23 Office Depot 119.50 HR \n",
"79 T0100 2024-05-24 Salesforce 6500.00 Sales \n",
"\n",
" Approved_By Payment_Method \n",
"0 J. Rivera Credit Card \n",
"1 J. Rivera Credit Card \n",
"2 M. Chen ACH \n",
"3 S. Patel ACH \n",
"4 L. Gomez Credit Card \n",
".. ... ... \n",
"75 S. Patel ACH \n",
"76 L. Gomez Credit Card \n",
"77 J. Rivera Credit Card \n",
"78 M. Chen Credit Card \n",
"79 J. Rivera ACH \n",
"\n",
"[80 rows x 7 columns]"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"RANDOM_SEED = 42 # Change this to get a different random sample; document what seed you used\n",
"\n",
"random_sample = df.sample(n=SAMPLE_SIZE, random_state=RANDOM_SEED).copy()\n",
"random_sample = random_sample.sort_values('Transaction_ID').reset_index(drop=True)\n",
"\n",
"print(f\"Random sample: {len(random_sample)} records\")\n",
"random_sample"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000011",
"metadata": {},
"source": [
"## 5. Method 2 — Systematic Sampling\n",
"\n",
"Select every *k*th item from the population, starting from a random point. The interval *k* is calculated as `population size ÷ sample size`.\n",
"\n",
"Systematic sampling is useful when records are already ordered (e.g., by date or transaction ID) and you want even coverage across the full period. It's also easy to explain to a reviewer — \"we took every 4th transaction starting from record 2.\"\n",
"\n",
"**Caution:** Avoid systematic sampling if the data has a pattern that aligns with your interval (e.g., if transactions are grouped in batches of 4, every 4th record could always land on the same type)."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "a1b2c3d4-0001-0001-0001-000000000012",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Interval: every 1 records\n",
"Starting index: 0\n",
"Systematic sample: 80 records\n"
]
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Transaction_ID | \n",
" Date | \n",
" Vendor | \n",
" Amount | \n",
" Department | \n",
" Approved_By | \n",
" Payment_Method | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" T0001 | \n",
" 2024-01-03 | \n",
" Staples | \n",
" 124.50 | \n",
" Marketing | \n",
" J. Rivera | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 1 | \n",
" T0002 | \n",
" 2024-01-05 | \n",
" AWS | \n",
" 3200.00 | \n",
" Engineering | \n",
" S. Patel | \n",
" ACH | \n",
"
\n",
" \n",
" | 2 | \n",
" T0003 | \n",
" 2024-01-07 | \n",
" Office Depot | \n",
" 87.25 | \n",
" HR | \n",
" M. Chen | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 3 | \n",
" T0004 | \n",
" 2024-01-08 | \n",
" Delta Airlines | \n",
" 1450.00 | \n",
" Sales | \n",
" J. Rivera | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 4 | \n",
" T0005 | \n",
" 2024-01-10 | \n",
" Adobe | \n",
" 599.99 | \n",
" Marketing | \n",
" M. Chen | \n",
" ACH | \n",
"
\n",
" \n",
" | ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
" ... | \n",
"
\n",
" \n",
" | 75 | \n",
" T0076 | \n",
" 2024-04-22 | \n",
" UPS | \n",
" 47.30 | \n",
" Operations | \n",
" L. Gomez | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 76 | \n",
" T0077 | \n",
" 2024-04-23 | \n",
" Staples | \n",
" 73.20 | \n",
" Marketing | \n",
" M. Chen | \n",
" Credit Card | \n",
"
\n",
" \n",
" | 77 | \n",
" T0078 | \n",
" 2024-04-24 | \n",
" GitHub | \n",
" 420.00 | \n",
" Engineering | \n",
" S. Patel | \n",
" ACH | \n",
"
\n",
" \n",
" | 78 | \n",
" T0079 | \n",
" 2024-04-25 | \n",
" Adobe | \n",
" 599.99 | \n",
" Engineering | \n",
" S. Patel | \n",
" ACH | \n",
"
\n",
" \n",
" | 79 | \n",
" T0080 | \n",
" 2024-04-26 | \n",
" Delta Airlines | \n",
" 1560.00 | \n",
" Sales | \n",
" J. Rivera | \n",
" Credit Card | \n",
"
\n",
" \n",
"
\n",
"
80 rows × 7 columns
\n",
"
"
],
"text/plain": [
" Transaction_ID Date Vendor Amount Department \\\n",
"0 T0001 2024-01-03 Staples 124.50 Marketing \n",
"1 T0002 2024-01-05 AWS 3200.00 Engineering \n",
"2 T0003 2024-01-07 Office Depot 87.25 HR \n",
"3 T0004 2024-01-08 Delta Airlines 1450.00 Sales \n",
"4 T0005 2024-01-10 Adobe 599.99 Marketing \n",
".. ... ... ... ... ... \n",
"75 T0076 2024-04-22 UPS 47.30 Operations \n",
"76 T0077 2024-04-23 Staples 73.20 Marketing \n",
"77 T0078 2024-04-24 GitHub 420.00 Engineering \n",
"78 T0079 2024-04-25 Adobe 599.99 Engineering \n",
"79 T0080 2024-04-26 Delta Airlines 1560.00 Sales \n",
"\n",
" Approved_By Payment_Method \n",
"0 J. Rivera Credit Card \n",
"1 S. Patel ACH \n",
"2 M. Chen Credit Card \n",
"3 J. Rivera Credit Card \n",
"4 M. Chen ACH \n",
".. ... ... \n",
"75 L. Gomez Credit Card \n",
"76 M. Chen Credit Card \n",
"77 S. Patel ACH \n",
"78 S. Patel ACH \n",
"79 J. Rivera Credit Card \n",
"\n",
"[80 rows x 7 columns]"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import random\n",
"\n",
"random.seed(RANDOM_SEED)\n",
"\n",
"interval = math.floor(POPULATION_SIZE / SAMPLE_SIZE)\n",
"start = random.randint(0, interval - 1) # Random start within the first interval\n",
"\n",
"systematic_indices = list(range(start, POPULATION_SIZE, interval))[:SAMPLE_SIZE]\n",
"systematic_sample = df.iloc[systematic_indices].copy().reset_index(drop=True)\n",
"\n",
"print(f\"Interval: every {interval} records\")\n",
"print(f\"Starting index: {start}\")\n",
"print(f\"Systematic sample: {len(systematic_sample)} records\")\n",
"systematic_sample"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000013",
"metadata": {},
"source": [
"## 6. Method 3 — Stratified Sampling\n",
"\n",
"Divide the population into subgroups (strata) and sample from each group proportionally. Use this when the population has distinct segments that vary significantly — for example, departments with very different transaction volumes or risk levels.\n",
"\n",
"Stratified sampling ensures that smaller but important subgroups aren't missed entirely, which can happen with random sampling.\n",
"\n",
"Here we'll stratify by **Department**."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "a1b2c3d4-0001-0001-0001-000000000014",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Stratum breakdown:\n",
" Sales 23 records (23%) → 18 samples\n",
" Engineering 20 records (20%) → 16 samples\n",
" Operations 16 records (16%) → 12 samples\n",
" Marketing 15 records (15%) → 12 samples\n",
" HR 14 records (14%) → 11 samples\n",
" IT 12 records (12%) → 9 samples\n"
]
}
],
"source": [
"STRATIFY_COLUMN = 'Department'\n",
"\n",
"# Calculate each stratum's proportion of the population\n",
"strata_counts = df[STRATIFY_COLUMN].value_counts()\n",
"strata_proportions = strata_counts / POPULATION_SIZE\n",
"\n",
"print(\"Stratum breakdown:\")\n",
"for stratum, count in strata_counts.items():\n",
" proportion = strata_proportions[stratum]\n",
" allocated = math.floor(SAMPLE_SIZE * proportion)\n",
" print(f\" {stratum:<15} {count:>3} records ({proportion:.0%}) → {allocated} samples\")"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "a1b2c3d4-0001-0001-0001-000000000015",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Stratified sample: 80 records\n",
"\n",
"Sample breakdown by department:\n",
"Department\n",
"Sales 18\n",
"Engineering 16\n",
"Operations 13\n",
"Marketing 12\n",
"HR 11\n",
"IT 10\n",
"Name: count, dtype: int64\n"
]
}
],
"source": [
"strata_samples = []\n",
"\n",
"for stratum, proportion in strata_proportions.items():\n",
" n = math.floor(SAMPLE_SIZE * proportion)\n",
" if n == 0:\n",
" continue\n",
" stratum_df = df[df[STRATIFY_COLUMN] == stratum]\n",
" if n > len(stratum_df):\n",
" raise ValueError(f\"Stratum '{stratum}' has fewer records ({len(stratum_df)}) than required samples ({n})\")\n",
" strata_samples.append(stratum_df.sample(n=n, random_state=RANDOM_SEED))\n",
"\n",
"stratified_sample = pd.concat(strata_samples)\n",
"\n",
"# Fill any rounding gap by randomly selecting from the remaining records\n",
"shortfall = SAMPLE_SIZE - len(stratified_sample)\n",
"if shortfall > 0:\n",
" already_selected = stratified_sample.index\n",
" remaining = df.drop(index=already_selected)\n",
" extras = remaining.sample(n=shortfall, random_state=RANDOM_SEED)\n",
" stratified_sample = pd.concat([stratified_sample, extras])\n",
"\n",
"stratified_sample = stratified_sample.sort_values('Transaction_ID').reset_index(drop=True)\n",
"\n",
"print(f\"Stratified sample: {len(stratified_sample)} records\")\n",
"print()\n",
"print(\"Sample breakdown by department:\")\n",
"print(stratified_sample[STRATIFY_COLUMN].value_counts())"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000016",
"metadata": {},
"source": [
"## 7. Comparing the Methods\n",
"\n",
"It's worth looking at how the three samples differ before deciding which to use — or which to document."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "a1b2c3d4-0001-0001-0001-000000000017",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Method | \n",
" Sample Size | \n",
" Avg Amount | \n",
" Unique Departments | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" Random | \n",
" 80 | \n",
" 1258.68 | \n",
" 6 | \n",
"
\n",
" \n",
" | 1 | \n",
" Systematic | \n",
" 80 | \n",
" 1244.90 | \n",
" 6 | \n",
"
\n",
" \n",
" | 2 | \n",
" Stratified | \n",
" 80 | \n",
" 1287.08 | \n",
" 6 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Method Sample Size Avg Amount Unique Departments\n",
"0 Random 80 1258.68 6\n",
"1 Systematic 80 1244.90 6\n",
"2 Stratified 80 1287.08 6"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"comparison = pd.DataFrame({\n",
" 'Method': ['Random', 'Systematic', 'Stratified'],\n",
" 'Sample Size': [len(random_sample), len(systematic_sample), len(stratified_sample)],\n",
" 'Avg Amount': [\n",
" random_sample['Amount'].mean(),\n",
" systematic_sample['Amount'].mean(),\n",
" stratified_sample['Amount'].mean()\n",
" ],\n",
" 'Unique Departments': [\n",
" random_sample['Department'].nunique(),\n",
" systematic_sample['Department'].nunique(),\n",
" stratified_sample['Department'].nunique()\n",
" ]\n",
"})\n",
"\n",
"comparison['Avg Amount'] = comparison['Avg Amount'].round(2)\n",
"comparison"
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000018",
"metadata": {},
"source": [
"**Which method to use:**\n",
"\n",
"- **Random** — default choice; easy to implement and defend. Use when the population is homogeneous.\n",
"- **Systematic** — good for ordered populations (e.g., daily transactions); provides even coverage over time.\n",
"- **Stratified** — use when subgroups differ significantly in size or risk, and you want guaranteed representation from each group.\n",
"\n",
"All three methods are defensible to auditors and regulators as long as you document how the sample was selected."
]
},
{
"cell_type": "markdown",
"id": "a1b2c3d4-0001-0001-0001-000000000019",
"metadata": {},
"source": [
"## 8. Export the Sample\n",
"\n",
"Documentation is as important as the sample itself. When you hand off your sample to a reviewer or include it in a workpaper, they need to know:\n",
"- What population was tested\n",
"- What method was used\n",
"- What parameters were applied\n",
"- When the sample was selected\n",
"\n",
"We'll export the sample to Excel with a metadata sheet capturing all of this."
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "a1b2c3d4-0001-0001-0001-000000000020",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Exported: Sampling_Workpaper_Stratified.xlsx\n",
" Sheet 'Metadata' — sampling parameters\n",
" Sheet 'Sample' — 80 selected records\n"
]
}
],
"source": [
"from datetime import datetime\n",
"\n",
"# Choose which sample to export\n",
"EXPORT_METHOD = 'Stratified' # Change to 'Random' or 'Systematic' as needed\n",
"\n",
"samples_map = {\n",
" 'Random': random_sample,\n",
" 'Systematic': systematic_sample,\n",
" 'Stratified': stratified_sample\n",
"}\n",
"\n",
"export_sample = samples_map[EXPORT_METHOD]\n",
"\n",
"# Build a metadata summary\n",
"metadata = pd.DataFrame([\n",
" {'Parameter': 'Population File', 'Value': 'transactions.csv'},\n",
" {'Parameter': 'Population Size', 'Value': POPULATION_SIZE},\n",
" {'Parameter': 'Sampling Method', 'Value': EXPORT_METHOD},\n",
" {'Parameter': 'Confidence Level', 'Value': f\"{int(CONFIDENCE_LEVEL * 100)}%\"},\n",
" {'Parameter': 'Tolerable Error Rate', 'Value': f\"{int(TOLERABLE_ERROR * 100)}%\"},\n",
" {'Parameter': 'Sample Size', 'Value': len(export_sample)},\n",
" {'Parameter': 'Random Seed', 'Value': RANDOM_SEED},\n",
" {'Parameter': 'Date Generated', 'Value': datetime.today().strftime('%Y-%m-%d')},\n",
"])\n",
"\n",
"output_file = f'Sampling_Workpaper_{EXPORT_METHOD}.xlsx'\n",
"\n",
"with pd.ExcelWriter(output_file, engine='openpyxl') as writer:\n",
" metadata.to_excel(writer, sheet_name='Metadata', index=False)\n",
" export_sample.to_excel(writer, sheet_name='Sample', index=False)\n",
"\n",
"print(f\"Exported: {output_file}\")\n",
"print(f\" Sheet 'Metadata' — sampling parameters\")\n",
"print(f\" Sheet 'Sample' — {len(export_sample)} selected records\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}