audit-labs/tutorials
Learn how to perform data analysis, scripting, automation, and more!
clone: git clone https://gitbay.org/audit-labs/tutorials.git
fa836e350724abb7354004df1de488d8fd3decb8
verified · cmc
author: Christian Cleberg <hello@cleberg.net> · 2026-02-22T00:06:51Z
.../sampling/Sampling_Workpaper_Stratified.xlsx | Bin 0 -> 8906 bytes notebooks/sampling/sampling.ipynb | 1089 ++++++++++++++++++++ notebooks/sampling/transactions.csv | 101 ++ 3 files changed, 1190 insertions(+) new file mode 100644 Binary files /dev/null and b/notebooks/sampling/Sampling_Workpaper_Stratified.xlsx differ new file mode 100644 @@ -0,0 +1,1089 @@ +{ + "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": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th>Transaction_ID</th>\n", + " <th>Date</th>\n", + " <th>Vendor</th>\n", + " <th>Amount</th>\n", + " <th>Department</th>\n", + " <th>Approved_By</th>\n", + " <th>Payment_Method</th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <th>0</th>\n", + " <td>T0001</td>\n", + " <td>2024-01-03</td>\n", + " <td>Staples</td>\n", + " <td>124.50</td>\n", + " <td>Marketing</td>\n", + " <td>J. Rivera</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>1</th>\n", + " <td>T0002</td>\n", + " <td>2024-01-05</td>\n", + " <td>AWS</td>\n", + " <td>3200.00</td>\n", + " <td>Engineering</td>\n", + " <td>S. Patel</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " <tr>\n", + " <th>2</th>\n", + " <td>T0003</td>\n", + " <td>2024-01-07</td>\n", + " <td>Office Depot</td>\n", + " <td>87.25</td>\n", + " <td>HR</td>\n", + " <td>M. Chen</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>3</th>\n", + " <td>T0004</td>\n", + " <td>2024-01-08</td>\n", + " <td>Delta Airlines</td>\n", + " <td>1450.00</td>\n", + " <td>Sales</td>\n", + " <td>J. Rivera</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>4</th>\n", + " <td>T0005</td>\n", + " <td>2024-01-10</td>\n", + " <td>Adobe</td>\n", + " <td>599.99</td>\n", + " <td>Marketing</td>\n", + " <td>M. Chen</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "</div>" + ], + "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": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th>Transaction_ID</th>\n", + " <th>Date</th>\n", + " <th>Vendor</th>\n", + " <th>Amount</th>\n", + " <th>Department</th>\n", + " <th>Approved_By</th>\n", + " <th>Payment_Method</th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <th>0</th>\n", + " <td>T0001</td>\n", + " <td>2024-01-03</td>\n", + " <td>Staples</td>\n", + " <td>124.50</td>\n", + " <td>Marketing</td>\n", + " <td>J. Rivera</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>1</th>\n", + " <td>T0004</td>\n", + " <td>2024-01-08</td>\n", + " <td>Delta Airlines</td>\n", + " <td>1450.00</td>\n", + " <td>Sales</td>\n", + " <td>J. Rivera</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>2</th>\n", + " <td>T0005</td>\n", + " <td>2024-01-10</td>\n", + " <td>Adobe</td>\n", + " <td>599.99</td>\n", + " <td>Marketing</td>\n", + " <td>M. Chen</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " <tr>\n", + " <th>3</th>\n", + " <td>T0006</td>\n", + " <td>2024-01-11</td>\n", + " <td>Zoom</td>\n", + " <td>149.00</td>\n", + " <td>IT</td>\n", + " <td>S. Patel</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " <tr>\n", + " <th>4</th>\n", + " <td>T0007</td>\n", + " <td>2024-01-14</td>\n", + " <td>FedEx</td>\n", + " <td>62.10</td>\n", + " <td>Operations</td>\n", + " <td>L. Gomez</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>...</th>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " </tr>\n", + " <tr>\n", + " <th>75</th>\n", + " <td>T0096</td>\n", + " <td>2024-05-20</td>\n", + " <td>Slack</td>\n", + " <td>320.00</td>\n", + " <td>IT</td>\n", + " <td>S. Patel</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " <tr>\n", + " <th>76</th>\n", + " <td>T0097</td>\n", + " <td>2024-05-21</td>\n", + " <td>FedEx</td>\n", + " <td>41.70</td>\n", + " <td>Operations</td>\n", + " <td>L. Gomez</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>77</th>\n", + " <td>T0098</td>\n", + " <td>2024-05-22</td>\n", + " <td>Delta Airlines</td>\n", + " <td>1730.00</td>\n", + " <td>Sales</td>\n", + " <td>J. Rivera</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>78</th>\n", + " <td>T0099</td>\n", + " <td>2024-05-23</td>\n", + " <td>Office Depot</td>\n", + " <td>119.50</td>\n", + " <td>HR</td>\n", + " <td>M. Chen</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>79</th>\n", + " <td>T0100</td>\n", + " <td>2024-05-24</td>\n", + " <td>Salesforce</td>\n", + " <td>6500.00</td>\n", + " <td>Sales</td>\n", + " <td>J. Rivera</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "<p>80 rows × 7 columns</p>\n", + "</div>" + ], + "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": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th>Transaction_ID</th>\n", + " <th>Date</th>\n", + " <th>Vendor</th>\n", + " <th>Amount</th>\n", + " <th>Department</th>\n", + " <th>Approved_By</th>\n", + " <th>Payment_Method</th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <th>0</th>\n", + " <td>T0001</td>\n", + " <td>2024-01-03</td>\n", + " <td>Staples</td>\n", + " <td>124.50</td>\n", + " <td>Marketing</td>\n", + " <td>J. Rivera</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>1</th>\n", + " <td>T0002</td>\n", + " <td>2024-01-05</td>\n", + " <td>AWS</td>\n", + " <td>3200.00</td>\n", + " <td>Engineering</td>\n", + " <td>S. Patel</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " <tr>\n", + " <th>2</th>\n", + " <td>T0003</td>\n", + " <td>2024-01-07</td>\n", + " <td>Office Depot</td>\n", + " <td>87.25</td>\n", + " <td>HR</td>\n", + " <td>M. Chen</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>3</th>\n", + " <td>T0004</td>\n", + " <td>2024-01-08</td>\n", + " <td>Delta Airlines</td>\n", + " <td>1450.00</td>\n", + " <td>Sales</td>\n", + " <td>J. Rivera</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>4</th>\n", + " <td>T0005</td>\n", + " <td>2024-01-10</td>\n", + " <td>Adobe</td>\n", + " <td>599.99</td>\n", + " <td>Marketing</td>\n", + " <td>M. Chen</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " <tr>\n", + " <th>...</th>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " <td>...</td>\n", + " </tr>\n", + " <tr>\n", + " <th>75</th>\n", + " <td>T0076</td>\n", + " <td>2024-04-22</td>\n", + " <td>UPS</td>\n", + " <td>47.30</td>\n", + " <td>Operations</td>\n", + " <td>L. Gomez</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>76</th>\n", + " <td>T0077</td>\n", + " <td>2024-04-23</td>\n", + " <td>Staples</td>\n", + " <td>73.20</td>\n", + " <td>Marketing</td>\n", + " <td>M. Chen</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " <tr>\n", + " <th>77</th>\n", + " <td>T0078</td>\n", + " <td>2024-04-24</td>\n", + " <td>GitHub</td>\n", + " <td>420.00</td>\n", + " <td>Engineering</td>\n", + " <td>S. Patel</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " <tr>\n", + " <th>78</th>\n", + " <td>T0079</td>\n", + " <td>2024-04-25</td>\n", + " <td>Adobe</td>\n", + " <td>599.99</td>\n", + " <td>Engineering</td>\n", + " <td>S. Patel</td>\n", + " <td>ACH</td>\n", + " </tr>\n", + " <tr>\n", + " <th>79</th>\n", + " <td>T0080</td>\n", + " <td>2024-04-26</td>\n", + " <td>Delta Airlines</td>\n", + " <td>1560.00</td>\n", + " <td>Sales</td>\n", + " <td>J. Rivera</td>\n", + " <td>Credit Card</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "<p>80 rows × 7 columns</p>\n", + "</div>" + ], + "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": [ + "<div>\n", + "<style scoped>\n", + " .dataframe tbody tr th:only-of-type {\n", + " vertical-align: middle;\n", + " }\n", + "\n", + " .dataframe tbody tr th {\n", + " vertical-align: top;\n", + " }\n", + "\n", + " .dataframe thead th {\n", + " text-align: right;\n", + " }\n", + "</style>\n", + "<table border=\"1\" class=\"dataframe\">\n", + " <thead>\n", + " <tr style=\"text-align: right;\">\n", + " <th></th>\n", + " <th>Method</th>\n", + " <th>Sample Size</th>\n", + " <th>Avg Amount</th>\n", + " <th>Unique Departments</th>\n", + " </tr>\n", + " </thead>\n", + " <tbody>\n", + " <tr>\n", + " <th>0</th>\n", + " <td>Random</td>\n", + " <td>80</td>\n", + " <td>1258.68</td>\n", + " <td>6</td>\n", + " </tr>\n", + " <tr>\n", + " <th>1</th>\n", + " <td>Systematic</td>\n", + " <td>80</td>\n", + " <td>1244.90</td>\n", + " <td>6</td>\n", + " </tr>\n", + " <tr>\n", + " <th>2</th>\n", + " <td>Stratified</td>\n", + " <td>80</td>\n", + " <td>1287.08</td>\n", + " <td>6</td>\n", + " </tr>\n", + " </tbody>\n", + "</table>\n", + "</div>" + ], + "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 +} new file mode 100644 @@ -0,0 +1,101 @@ +Transaction_ID,Date,Vendor,Amount,Department,Approved_By,Payment_Method +T0001,2024-01-03,Staples,124.50,Marketing,J. Rivera,Credit Card +T0002,2024-01-05,AWS,3200.00,Engineering,S. Patel,ACH +T0003,2024-01-07,Office Depot,87.25,HR,M. Chen,Credit Card +T0004,2024-01-08,Delta Airlines,1450.00,Sales,J. Rivera,Credit Card +T0005,2024-01-10,Adobe,599.99,Marketing,M. Chen,ACH +T0006,2024-01-11,Zoom,149.00,IT,S. Patel,ACH +T0007,2024-01-14,FedEx,62.10,Operations,L. Gomez,Credit Card +T0008,2024-01-15,Marriott,980.00,Sales,J. Rivera,Credit Card +T0009,2024-01-16,AWS,4100.00,Engineering,S. Patel,ACH +T0010,2024-01-17,Uber,43.75,HR,M. Chen,Credit Card +T0011,2024-01-18,Slack,320.00,IT,S. Patel,ACH +T0012,2024-01-21,United Airlines,2200.00,Sales,J. Rivera,Credit Card +T0013,2024-01-22,Staples,55.40,Operations,L. Gomez,Credit Card +T0014,2024-01-23,GitHub,420.00,Engineering,S. Patel,ACH +T0015,2024-01-24,Hilton,760.00,Marketing,J. Rivera,Credit Card +T0016,2024-01-25,UPS,38.90,Operations,L. Gomez,Credit Card +T0017,2024-01-28,Salesforce,6500.00,Sales,J. Rivera,ACH +T0018,2024-01-29,Office Depot,112.80,HR,M. Chen,Credit Card +T0019,2024-01-30,Zoom,149.00,IT,S. Patel,ACH +T0020,2024-01-31,AWS,2875.00,Engineering,S. Patel,ACH +T0021,2024-02-01,FedEx,74.20,Operations,L. Gomez,Credit Card +T0022,2024-02-02,Delta Airlines,1890.00,Sales,J. Rivera,Credit Card +T0023,2024-02-05,Staples,99.95,Marketing,M. Chen,Credit Card +T0024,2024-02-06,Adobe,599.99,Marketing,M. Chen,ACH +T0025,2024-02-07,Marriott,1100.00,HR,M. Chen,Credit Card +T0026,2024-02-08,GitHub,420.00,Engineering,S. Patel,ACH +T0027,2024-02-09,Uber,31.50,Sales,J. Rivera,Credit Card +T0028,2024-02-12,Slack,320.00,IT,S. Patel,ACH +T0029,2024-02-13,UPS,55.60,Operations,L. Gomez,Credit Card +T0030,2024-02-14,Salesforce,6500.00,Sales,J. Rivera,ACH +T0031,2024-02-15,AWS,3750.00,Engineering,S. Patel,ACH +T0032,2024-02-16,Office Depot,143.20,HR,M. Chen,Credit Card +T0033,2024-02-19,United Airlines,1650.00,Marketing,J. Rivera,Credit Card +T0034,2024-02-20,Zoom,149.00,IT,S. Patel,ACH +T0035,2024-02-21,FedEx,88.40,Operations,L. Gomez,Credit Card +T0036,2024-02-22,Staples,67.30,HR,M. Chen,Credit Card +T0037,2024-02-23,Adobe,599.99,Engineering,S. Patel,ACH +T0038,2024-02-26,Hilton,840.00,Sales,J. Rivera,Credit Card +T0039,2024-02-27,GitHub,420.00,Engineering,S. Patel,ACH +T0040,2024-02-28,AWS,5100.00,Engineering,S. Patel,ACH +T0041,2024-03-01,Uber,28.90,HR,M. Chen,Credit Card +T0042,2024-03-04,Delta Airlines,2100.00,Sales,J. Rivera,Credit Card +T0043,2024-03-05,UPS,44.75,Operations,L. Gomez,Credit Card +T0044,2024-03-06,Salesforce,6500.00,Sales,J. Rivera,ACH +T0045,2024-03-07,Slack,320.00,IT,S. Patel,ACH +T0046,2024-03-08,Marriott,920.00,Marketing,J. Rivera,Credit Card +T0047,2024-03-11,Office Depot,78.60,Operations,L. Gomez,Credit Card +T0048,2024-03-12,AWS,3300.00,Engineering,S. Patel,ACH +T0049,2024-03-13,Staples,115.40,Marketing,M. Chen,Credit Card +T0050,2024-03-14,Adobe,599.99,Marketing,M. Chen,ACH +T0051,2024-03-15,United Airlines,1750.00,Sales,J. Rivera,Credit Card +T0052,2024-03-18,GitHub,420.00,Engineering,S. Patel,ACH +T0053,2024-03-19,FedEx,51.30,Operations,L. Gomez,Credit Card +T0054,2024-03-20,Zoom,149.00,IT,S. Patel,ACH +T0055,2024-03-21,Hilton,1050.00,Sales,J. Rivera,Credit Card +T0056,2024-03-22,Uber,37.40,HR,M. Chen,Credit Card +T0057,2024-03-25,UPS,62.80,Operations,L. Gomez,Credit Card +T0058,2024-03-26,AWS,4400.00,Engineering,S. Patel,ACH +T0059,2024-03-27,Salesforce,6500.00,Sales,J. Rivera,ACH +T0060,2024-03-28,Office Depot,95.20,HR,M. Chen,Credit Card +T0061,2024-04-01,Delta Airlines,1320.00,Marketing,J. Rivera,Credit Card +T0062,2024-04-02,Slack,320.00,IT,S. Patel,ACH +T0063,2024-04-03,Staples,88.75,Operations,L. Gomez,Credit Card +T0064,2024-04-04,Adobe,599.99,Marketing,M. Chen,ACH +T0065,2024-04-05,GitHub,420.00,Engineering,S. Patel,ACH +T0066,2024-04-08,Marriott,1200.00,Sales,J. Rivera,Credit Card +T0067,2024-04-09,AWS,2950.00,Engineering,S. Patel,ACH +T0068,2024-04-10,FedEx,69.50,Operations,L. Gomez,Credit Card +T0069,2024-04-11,United Airlines,1980.00,Sales,J. Rivera,Credit Card +T0070,2024-04-12,Uber,22.60,HR,M. Chen,Credit Card +T0071,2024-04-15,Zoom,149.00,IT,S. Patel,ACH +T0072,2024-04-16,Office Depot,131.40,HR,M. Chen,Credit Card +T0073,2024-04-17,Salesforce,6500.00,Sales,J. Rivera,ACH +T0074,2024-04-18,AWS,3600.00,Engineering,S. Patel,ACH +T0075,2024-04-19,Hilton,890.00,Marketing,J. Rivera,Credit Card +T0076,2024-04-22,UPS,47.30,Operations,L. Gomez,Credit Card +T0077,2024-04-23,Staples,73.20,Marketing,M. Chen,Credit Card +T0078,2024-04-24,GitHub,420.00,Engineering,S. Patel,ACH +T0079,2024-04-25,Adobe,599.99,Engineering,S. Patel,ACH +T0080,2024-04-26,Delta Airlines,1560.00,Sales,J. Rivera,Credit Card +T0081,2024-04-29,Slack,320.00,IT,S. Patel,ACH +T0082,2024-04-30,FedEx,83.10,Operations,L. Gomez,Credit Card +T0083,2024-05-01,Marriott,1040.00,Sales,J. Rivera,Credit Card +T0084,2024-05-02,AWS,4800.00,Engineering,S. Patel,ACH +T0085,2024-05-03,Office Depot,108.90,HR,M. Chen,Credit Card +T0086,2024-05-06,Uber,45.20,Sales,J. Rivera,Credit Card +T0087,2024-05-07,Salesforce,6500.00,Sales,J. Rivera,ACH +T0088,2024-05-08,United Airlines,2350.00,Marketing,J. Rivera,Credit Card +T0089,2024-05-09,Zoom,149.00,IT,S. Patel,ACH +T0090,2024-05-10,GitHub,420.00,Engineering,S. Patel,ACH +T0091,2024-05-13,UPS,58.40,Operations,L. Gomez,Credit Card +T0092,2024-05-14,Staples,92.60,HR,M. Chen,Credit Card +T0093,2024-05-15,Adobe,599.99,Marketing,M. Chen,ACH +T0094,2024-05-16,AWS,3100.00,Engineering,S. Patel,ACH +T0095,2024-05-17,Hilton,780.00,Sales,J. Rivera,Credit Card +T0096,2024-05-20,Slack,320.00,IT,S. Patel,ACH +T0097,2024-05-21,FedEx,41.70,Operations,L. Gomez,Credit Card +T0098,2024-05-22,Delta Airlines,1730.00,Sales,J. Rivera,Credit Card +T0099,2024-05-23,Office Depot,119.50,HR,M. Chen,Credit Card +T0100,2024-05-24,Salesforce,6500.00,Sales,J. Rivera,ACH \ No newline at end of file