aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <hello@cleberg.net>2025-05-28 13:02:35 -0500
committerGitHub <noreply@github.com>2025-05-28 13:02:35 -0500
commitae0b864a92cefc33593f5817fe4a6afe75e395d1 (patch)
tree09fd29b974b6d4a6064e6c027d15769da6280e37
parent6f6c450e140045bca15c2fada70ae436b58c41be (diff)
downloadaudit-tools-ae0b864a92cefc33593f5817fe4a6afe75e395d1.tar.gz
audit-tools-ae0b864a92cefc33593f5817fe4a6afe75e395d1.tar.bz2
audit-tools-ae0b864a92cefc33593f5817fe4a6afe75e395d1.zip
feat: add jupyter notebooks folder (#11)
* feat: add jupyter notebooks folder * Commit from GitHub Actions (Ruff) --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
-rw-r--r--notebooks/basic_data_analysis.ipynb688
-rw-r--r--notebooks/racf_access_analysis.ipynb526
-rw-r--r--notebooks/sample_racf_data.txt54
3 files changed, 1268 insertions, 0 deletions
diff --git a/notebooks/basic_data_analysis.ipynb b/notebooks/basic_data_analysis.ipynb
new file mode 100644
index 0000000..6609858
--- /dev/null
+++ b/notebooks/basic_data_analysis.ipynb
@@ -0,0 +1,688 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "2bc817b1",
+ "metadata": {},
+ "source": [
+ "\n",
+ "# πŸ“Š Basic Data Analysis & Visualization\n",
+ "\n",
+ "This notebook demonstrates how to load a sample dataset, perform quick exploratory analysis, group and pivot the data, and create visualizations.\n",
+ "\n",
+ "We’re using a CSV file from: https://sample-files.com/downloads/data/csv/basic-data.csv\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a92fdfd1",
+ "metadata": {},
+ "source": [
+ "\n",
+ "## πŸ“¦ Install Dependencies\n",
+ "\n",
+ "If you haven't installed the required libraries, run:\n",
+ "\n",
+ "```bash\n",
+ "pip install pandas matplotlib seaborn\n",
+ "```\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "b5d7308a",
+ "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.2.3)\n",
+ "Requirement already satisfied: matplotlib in /Users/cmc/Library/Python/3.9/lib/python/site-packages (3.9.4)\n",
+ "Requirement already satisfied: seaborn in /Users/cmc/Library/Python/3.9/lib/python/site-packages (0.13.2)\n",
+ "Requirement already satisfied: numpy>=1.22.4 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from pandas) (1.26.4)\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.2)\n",
+ "Requirement already satisfied: contourpy>=1.0.1 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (1.3.0)\n",
+ "Requirement already satisfied: cycler>=0.10 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (0.12.1)\n",
+ "Requirement already satisfied: fonttools>=4.22.0 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (4.58.1)\n",
+ "Requirement already satisfied: kiwisolver>=1.3.1 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (1.4.7)\n",
+ "Requirement already satisfied: packaging>=20.0 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (24.2)\n",
+ "Requirement already satisfied: pillow>=8 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (11.2.1)\n",
+ "Requirement already satisfied: pyparsing>=2.3.1 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (3.2.3)\n",
+ "Requirement already satisfied: importlib-resources>=3.2.0 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from matplotlib) (6.5.2)\n",
+ "Requirement already satisfied: zipp>=3.1.0 in /Users/cmc/Library/Python/3.9/lib/python/site-packages (from importlib-resources>=3.2.0->matplotlib) (3.21.0)\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",
+ "\n",
+ "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m25.0.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.1.1\u001b[0m\n",
+ "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49m/Library/Developer/CommandLineTools/usr/bin/python3 -m pip install --upgrade pip\u001b[0m\n",
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ }
+ ],
+ "source": [
+ "pip install pandas matplotlib seaborn"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "0632140a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "import matplotlib.pyplot as plt\n",
+ "import seaborn as sns\n",
+ "\n",
+ "# Set plot style\n",
+ "sns.set(style=\"whitegrid\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d326b5b6",
+ "metadata": {},
+ "source": [
+ "## πŸ“‚ Load Dataset"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "5d5de24b",
+ "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>ID</th>\n",
+ " <th>Name</th>\n",
+ " <th>Age</th>\n",
+ " <th>Country</th>\n",
+ " <th>Email</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>1</td>\n",
+ " <td>Name_1</td>\n",
+ " <td>62</td>\n",
+ " <td>Country_1</td>\n",
+ " <td>email_1@example.com</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>1</th>\n",
+ " <td>2</td>\n",
+ " <td>Name_2</td>\n",
+ " <td>48</td>\n",
+ " <td>Country_2</td>\n",
+ " <td>email_2@example.com</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>2</th>\n",
+ " <td>3</td>\n",
+ " <td>Name_3</td>\n",
+ " <td>61</td>\n",
+ " <td>Country_3</td>\n",
+ " <td>email_3@example.com</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>3</th>\n",
+ " <td>4</td>\n",
+ " <td>Name_4</td>\n",
+ " <td>32</td>\n",
+ " <td>Country_4</td>\n",
+ " <td>email_4@example.com</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>4</th>\n",
+ " <td>5</td>\n",
+ " <td>Name_5</td>\n",
+ " <td>69</td>\n",
+ " <td>Country_5</td>\n",
+ " <td>email_5@example.com</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " ID Name Age Country Email\n",
+ "0 1 Name_1 62 Country_1 email_1@example.com\n",
+ "1 2 Name_2 48 Country_2 email_2@example.com\n",
+ "2 3 Name_3 61 Country_3 email_3@example.com\n",
+ "3 4 Name_4 32 Country_4 email_4@example.com\n",
+ "4 5 Name_5 69 Country_5 email_5@example.com"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Load the dataset from URL\n",
+ "url = \"https://sample-files.com/downloads/data/csv/basic-data.csv\"\n",
+ "df = pd.read_csv(url, skiprows=1)\n",
+ "df.columns = df.columns.str.strip()\n",
+ "\n",
+ "# Preview the data\n",
+ "df.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e7f7c2e7",
+ "metadata": {},
+ "source": [
+ "## πŸ“Š Basic Exploration"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "aca7309e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "(100, 5)\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>ID</th>\n",
+ " <th>Name</th>\n",
+ " <th>Age</th>\n",
+ " <th>Country</th>\n",
+ " <th>Email</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>count</th>\n",
+ " <td>100.000000</td>\n",
+ " <td>100</td>\n",
+ " <td>100.000000</td>\n",
+ " <td>100</td>\n",
+ " <td>100</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>unique</th>\n",
+ " <td>NaN</td>\n",
+ " <td>100</td>\n",
+ " <td>NaN</td>\n",
+ " <td>10</td>\n",
+ " <td>100</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>top</th>\n",
+ " <td>NaN</td>\n",
+ " <td>Name_1</td>\n",
+ " <td>NaN</td>\n",
+ " <td>Country_1</td>\n",
+ " <td>email_1@example.com</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>freq</th>\n",
+ " <td>NaN</td>\n",
+ " <td>1</td>\n",
+ " <td>NaN</td>\n",
+ " <td>10</td>\n",
+ " <td>1</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>mean</th>\n",
+ " <td>50.500000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>44.530000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>NaN</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>std</th>\n",
+ " <td>29.011492</td>\n",
+ " <td>NaN</td>\n",
+ " <td>15.190012</td>\n",
+ " <td>NaN</td>\n",
+ " <td>NaN</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>min</th>\n",
+ " <td>1.000000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>18.000000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>NaN</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>25%</th>\n",
+ " <td>25.750000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>32.000000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>NaN</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>50%</th>\n",
+ " <td>50.500000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>43.500000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>NaN</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>75%</th>\n",
+ " <td>75.250000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>59.250000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>NaN</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>max</th>\n",
+ " <td>100.000000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>69.000000</td>\n",
+ " <td>NaN</td>\n",
+ " <td>NaN</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " ID Name Age Country Email\n",
+ "count 100.000000 100 100.000000 100 100\n",
+ "unique NaN 100 NaN 10 100\n",
+ "top NaN Name_1 NaN Country_1 email_1@example.com\n",
+ "freq NaN 1 NaN 10 1\n",
+ "mean 50.500000 NaN 44.530000 NaN NaN\n",
+ "std 29.011492 NaN 15.190012 NaN NaN\n",
+ "min 1.000000 NaN 18.000000 NaN NaN\n",
+ "25% 25.750000 NaN 32.000000 NaN NaN\n",
+ "50% 50.500000 NaN 43.500000 NaN NaN\n",
+ "75% 75.250000 NaN 59.250000 NaN NaN\n",
+ "max 100.000000 NaN 69.000000 NaN NaN"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Check shape and summary stats\n",
+ "print(df.shape)\n",
+ "df.describe(include=\"all\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "e775e42f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " ID Name Age Country Email\n",
+ "0 1 Name_1 62 Country_1 email_1@example.com\n",
+ "1 2 Name_2 48 Country_2 email_2@example.com\n",
+ "2 3 Name_3 61 Country_3 email_3@example.com\n",
+ "3 4 Name_4 32 Country_4 email_4@example.com\n",
+ "4 5 Name_5 69 Country_5 email_5@example.com\n",
+ ".. ... ... ... ... ...\n",
+ "95 96 Name_96 60 Country_6 email_96@example.com\n",
+ "96 97 Name_97 26 Country_7 email_97@example.com\n",
+ "97 98 Name_98 52 Country_8 email_98@example.com\n",
+ "98 99 Name_99 24 Country_9 email_99@example.com\n",
+ "99 100 Name_100 55 Country_0 email_100@example.com\n",
+ "\n",
+ "[100 rows x 5 columns]\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(df)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dc10be72",
+ "metadata": {},
+ "source": [
+ "## πŸ“ˆ Grouping and Pivoting Data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "f87ea8af",
+ "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>Country</th>\n",
+ " <th>RecordCount</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>Country_1</td>\n",
+ " <td>10</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>1</th>\n",
+ " <td>Country_2</td>\n",
+ " <td>10</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>2</th>\n",
+ " <td>Country_3</td>\n",
+ " <td>10</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>3</th>\n",
+ " <td>Country_4</td>\n",
+ " <td>10</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>4</th>\n",
+ " <td>Country_5</td>\n",
+ " <td>10</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " Country RecordCount\n",
+ "0 Country_1 10\n",
+ "1 Country_2 10\n",
+ "2 Country_3 10\n",
+ "3 Country_4 10\n",
+ "4 Country_5 10"
+ ]
+ },
+ "execution_count": 24,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Example: Group by 'Country' and count number of records\n",
+ "country_counts = df[\"Country\"].value_counts().reset_index()\n",
+ "country_counts.columns = [\"Country\", \"RecordCount\"]\n",
+ "country_counts.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f156d6de",
+ "metadata": {},
+ "source": [
+ "## πŸ“Š Visualizing Record Counts by Country"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "d644fc06",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/var/folders/pf/tt0qdz214wn0q90g989_0r0h0000gn/T/ipykernel_79108/1394705902.py:3: FutureWarning: \n",
+ "\n",
+ "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n",
+ "\n",
+ " sns.barplot(data=country_counts, x='Country', y='RecordCount', palette='viridis')\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAA00AAAJUCAYAAADJrc3LAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8ekN5oAAAACXBIWXMAAA9hAAAPYQGoP6dpAABLd0lEQVR4nO3dB5hU5dk/4BcpAnZR0VhRbBi72A12jb3X2GtsGDR2JbFi+RT7Z+899hhbRKOxa4waRQ0KdqygKCjF+V/P+f6z7uJy5BwWt8x9X9e6uzPD7DuP786e33nLaVepVCoJAACARk3T+M0AAAAEoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwDU45rvk6Y2QK0SmgBagF122SUtuuiiDT4WW2yxtNxyy6Wtttoq3X333aml++CDD7J233HHHT/72E8++SSdccYZacMNN0xLL710Wn311dP++++fXnjhhdScHnnkkXTkkUfmPuaoo45Ka6+99lRrw/fff5+uvvrqtPXWW6fll18+rbjiimmHHXZId911V7OGlv/+979pxx13bLafD9CcOjTrTwegTq9evVL//v3rvp8wYUIaPnx4dgB9xBFHpJlnnjn16dMntXYvvvhiOvDAA9Mss8ySdt1119SjR480cuTIdMstt2Th8bTTTktbbLFFs7Qtat2cPv/887T33nunjz/+OKvFUkstlX744Yf06KOPZmEtQuVJJ52U2rVr94u37YEHHkgvvfTSL/5zAVoCoQmghZh++unTMsss85Pbf/Ob36RVVlklG8Fp7aEpwtGhhx6aFlhggXTVVVelLl261N23wQYbpH333TedcMIJ2cjTbLPNlmpNjHJFUI4AGTWqWnPNNdOvfvWrdPbZZ6e11lorrbPOOs3aToBaY3oeQAs37bTTpk6dOjUYXYjRh0svvTStt9566de//nUWOK677rqf/NuY0rXllltmU+DiwPt//ud/0tixY+vuf/XVV9Nee+2VVlpppWwqYEyRi2lYVc8++2w25e7mm2/ODtbjMU8++WR230MPPZQ222yzbDQkfsYbb7zxs68l2vPpp5+mY445pkFgCtNMM006/PDD084775y++eabutvj5+20007ZVLVo52GHHZaNxFSdf/75WRsnFrfFffWnDt5///3pkEMOScsuu2w27e24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fs6f//znrEbbbbddtqtfvJ7YeS5ef2zKEDsbVte3Re1jDVtcIDj+38X/w7gwbgSuqFuIvhKbL8SmFfE80d54zMRT8Sa3NiFCbGOhbmJFXztASyY0AbRwMWoU21m/+eabWTgKMVIToSGunxQHwbHtc0yBuvLKK+sW80c4ioASoWS//fbLprzts88+dTvOxUhArJuKEZ84yI4QFmElduaLTSjyxGhXjIjEGps4eI/wE9uWT46YhnbbbbdlI10xehJtit3xYjpiPE+s1aqKEYoIYxFkIsjF64lRrpiGGKNQIUakImjF1tsRAGN0I4Li5KyxmljULAJqtCnC4KTEtuQR+iKcRT1jc4obb7yxwfS8aqCLKXKTO9ISa76iBhGaIpzEa47txGOdU9Qofl79kan4mbGWKrZij9AcI3/x+mMzhhB9IeoXtY3/xzHNM+q7/vrrF65N/JsYwTrqqKOynQt/TtHXDtCStauUWQELAPys2Ho7QlWMkk1qU462qpZfO9D2uE4TADSxmAYXm13ESGCMtNRSaKjl1w60XabnAUATi6mCsc4rpiDGtLlaUsuvHWi7TM8DAADIYaQJAAAgh9AEAACQQ2gCAADIUXO758UV22MZV1yHAwAAqF3jxo3LrikX1wDMU3OhKQKTvS8AAIDKZOaCmgtN1RGmuKo5AABQu1599dXJepw1TQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAAC0ltB0ySWXpF122aXBbYMHD06/+93v0jLLLJPWXnvtdO211zZb+wAAgNrTYkLTDTfckAYOHNjgthEjRqQ99tgjzTfffOn2229PBx54YDrrrLOyrwEAAH4JHVIz++STT1L//v3Ts88+mxZYYIEG9916662pY8eO6cQTT0wdOnRICy20UHr33XfTpZdemrbeeutmazMAAFA7mn2k6bXXXsuC0T333JOWXnrpBve98MILacUVV8wCU9XKK6+chg0blj7//PNmaC0AAFBrmn2kKdYpxUdjhg8fnhZZZJEGt80xxxzZ548//jjNNttspX5mpVJJo0ePnuT97dq1S7UqalOGmpWjbsWpWTnqVpyalaNuxalZOepWnJo1fvvk1KXZQ1Oe7777LnXq1KnBbdNOO232+fvvvy/9vOPGjcs2mGhMjHr16tWrwehWrRg/fnx6/fXXs/oUoWbFa/Zj3ZZIHTq0T7Vm/PgJ6fXXXyvZ19SsKHUr2deW6JU6tK/B97UJ49Prr03B+5q6Ffp3ala+ry2xRK/UvgbrNmHC+PRayb6mZuMavX/ivNGYFl21zp07p7Fjxza4rRqWunbtWvp5o9P07Nmz0fsiacbB/xlHXZnef2d4qhXzLjhnOmLAnmnhhRcufPaiWrMzT78tvf/+Z6lWzDvv7OmPR25bqmY/1q19OvXCO9N7H9XOdNP5fjVbOubALaegr7VPf7r6zjRseO3UbIE5Z0t/2r1czerX7bhb70hDP6uduvWYfbZ08nZble9r7Tuko+/7S3rni9qp2YLdZkunbbzNlPW19h3Snx+/Nb07snb+Hsw/8+yp/2+2m6K+dsnL16ePv/kk1Yq5pu+e9lv6d1PU1+Lg/943L0pfjP4o1YpuXX+VNl30gNJ9LWr2ryF/Tt+MeTfVium7zJ+W69l/kjUbMmTIZD1Piw5Nc845Z/r0008b3Fb9vnv37qWfNzrNz4WuCExvD34/1ZouXbqU/rcRmN4e8nGqNVNSsxCBaciw2gnoTVG3CExvfaBmRUVgeuMjdSsiAtMbn3pfKyoC01tf1s6BbFPULQLTu19/mGrNlPa1CEyffDss1ZopqVsEpq9Gv5VqTZdJ1Gxypyw2+0YQeXr37p1efPHFNGHChLrbnnnmmdSjR4/UrVu3Zm0bAABQG1p0aIptxb/55pt07LHHZkNnd9xxR7r66qvTfvvt19xNAwAAakSLDk0xmnT55ZenoUOHpi233DJdcMEF6Ygjjsi+BgAA+CW0qDVNAwYM+MltSy21VLrllluapT0AAAAteqQJAACguQlNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABAaw9N48ePT+eee25aa6210rLLLpt23nnn9O9//7u5mwUAANSAVhGaLr744nTbbbelk046Kd11112pR48eae+9906ffvppczcNAABo41pFaPr73/+eNtlkk7T66qun+eefPx111FFp1KhRRpsAAICprlWEpm7duqVHH300ffDBB2nChAnplltuSZ06dUqLLbZYczcNAABo4zqkVuDYY49Nffv2Teuss05q3759mmaaadL555+f5ptvvlLPV6lU0ujRoxu9r127dqlLly6pVo0ZMyarTxFqVrxmQd30taL0tXL0teL0tXL0teL0tXL0taarWdwWtWkToWnIkCFphhlmSBdeeGHq3r17tr7p8MMPT9dff31afPHFCz/fuHHj0uDBgxu9LzpTr169Uq0aOnRo1qmKULPiNQvqpq8Vpa+Vo68Vp6+Vo68Vp6+Vo681bc1iBlurD00ff/xxOuyww9LVV1+dVlhhhey2JZdcMgtSMdp00UUXFX7Ojh07pp49ezZ63+QkzbYsNtkoc+ailpWpWVA3fa0ofa0cfa04fa0cfa04fa0cfa3pahaZYnK0+ND08ssvZyNDEZTqW3rppdPjjz9e6jmj03Tt2rWJWti21PKwbVlqVo66Fadm5ahbcWpWjroVp2blqFvT1Wxyw2SL3whizjnnzD6/+eabDW5/66230gILLNBMrQIAAGpFiw9NSy21VFp++eXTkUcemZ555pk0bNiwNHDgwPT000+nfffdt7mbBwAAtHEtfnpe7JQXF7eNoHT00Uenr776Ki2yyCLZGqeYogcAAFDToSnMNNNMqX///tkHAADAL6nFT88DAABoTkITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAA0NSh6eijj07vv/9+o/e98847af/99y/ztAAAAC1Oh8l94EcffVT39V133ZXWXXfd1L59+5887vHHH09PPfVU07UQAACgNYSmP//5z1kgqjrooIMafVylUkmrrbZa07QOAACgtYSmE088MRtBilB0zDHHpN///vdpvvnma/CYaaaZJs0444xppZVWmhptBQAAaLmhqXv37mnLLbfMvm7Xrl3q06dPmnXWWadm2wAAAFpPaKovwlOMOL3++utp9OjR2dcT6927d1O0DwAAoPWFpldeeSX17ds3DR8+PPu+GppiBCq+js+DBw9u2pYCAAC0ltB02mmnpQ4dOmSf55xzzmwtEwAAQFtUKjS99tpr6eyzz862Hf+lxDbnl156aXZ9qNiAInbv++1vf/uL/XwAAKA2lRoi6tatW6PXaJpa7r777nTsscemnXfeOd13331pk002Sf369UsvvfTSL9YGAACgNpUKTTvttFO65JJLsk0gprZYI3XuueemXXfdNQtNMcoU252vuuqq6bnnnpvqPx8AAKhtpabnvfvuu+ntt9/OLmK78MILp86dOze4PzaCuOaaa5qkgUOHDk0ffvhh2nTTTRvcfsUVVzTJ8wMAAEyV0LTYYovVfT/xluONbUE+JaEpxKjWXnvtlW1zPs8882SjTWuvvXap54z2TWqULAJfly5dUq0aM2ZM4f9/ala8ZkHd9LWi9LVy9LXi9LVy9LXi9LVy9LWmq1l15++pEpquu+669Ev55ptvss9HHnlktvnD4Ycfnh588MF0wAEHpKuuuiqtssoqhZ9z3Lhxk9wSPTpTr169Uq2KkBqdqgg1K16zoG76WlH6Wjn6WnH6Wjn6WnH6Wjn6WtPWrFOnTlMnNP2SOnbsmH2OUaa4qG5YfPHFsxGnsqEpnrNnz56N3jc5SbMt69GjR6kzF7WsTM2CuulrRelr5ehrxelr5ehrxelr5ehrTVezIUOGTNa/LxWaYlrczxX+kUceSU2he/fu2edFFlmkwe0Reh577LFSzxlt79q1a5O0r62p5WHbstSsHHUrTs3KUbfi1KwcdStOzcpRt6ar2eSGyVKhacUVV/zJD/j222/Tq6++mr7//vu02267paayxBJLpOmmmy69/PLLaYUVVqi7/a233sp20gMAAJiaSoWmAQMGTHKtUKw1KjM3dVJiZ7699947XXjhhdmo01JLLZVdq+nJJ59MV199dZP9HAAAgKm+pinWCsX1lI4++uh06KGHNtnzRhCLIbVzzjknffLJJ2mhhRZK559/flpppZWa7GcAAAD8IhtBfPXVV9lUvaa2xx57ZB8AAAAtPjTdddddP7ltwoQJafjw4en6669vsPYIAACg5kLTUUcdNcn7ll122XT88cdPSZsAAABad2hqbDvx2E1v+umnTzPOOGNTtAsAAKD1hqa555677uu33347jRo1Ks0yyywCEwAA0OaU3gjir3/9azr99NPT559/XnfbbLPNlg477LC0xRZbNFX7AAAAWl9oGjRoUPrjH/+YVl555dSvX78sLH366afpnnvuybYbn3nmmdOaa67Z9K0FAABoDaHp4osvThtuuGF23aT6tt566/SHP/whXXLJJUITAADQJkxT5h+99dZbacstt2z0vrj9jTfemNJ2AQAAtN7QFJs+xEVsGzNy5MjUqVOnKW0XAABA6w1Nq6yySrrggguyi9nW9/HHH6cLL7wwrbbaak3VPgAAgNa3pik2f4j1S+uvv352MdvYCCJ20XvppZfSTDPNlO2gBwAAULMjTbPPPnu688470y677JLGjBmT/vOf/2Sf4/u4vf51nAAAAGryOk3dunXLth0HAABoywqNNI0fPz5df/316eGHH25w+4QJE7Jd866++ur0ww8/NHUbAQAAWn5oGjduXDrggAPSKaeckq1dqu/LL7/MwtKAAQPSQQcdlIUoAACAmgpNt9xyS3rmmWfSWWedlY444oifrHG6++67s9D0+OOPp9tvv31qtBUAAKDlhqY77rgj7b777mnjjTee5GO22GKLtO2226bbbrutqdoHAADQOkLTu+++m1ZeeeWffVyfPn3SsGHDprRdAAAArSs0dejQIVvXNDmPa9eu3ZS2CwAAoHWFpoUXXjg9++yzP/u45557Ls0zzzxT2i4AAIDWFZo233zzdNNNN6VXXnllko957bXX0g033JB++9vfNlX7AAAAWsfFbbfZZpv017/+Ne2yyy7Z12uuuWY2ohRbjX/44YfZrnm33nprWnTRRbPHAAAA1FRoinVKl1xySTr11FOz7cdvvPHGuvsqlUq2lil2zuvXr1/q3Lnz1GovAABAywxNIcLQiSeemA499ND09NNPp+HDh6f27dunueeeO9tZb4YZZph6LQUAAGjpoalq1llnzb1eEwAAQM2FprvuuqvQE8eFbgEAAGomNB111FENvq9eiynWM018WxCaAACAmgpNjzzySN3XgwcPTn/84x/TAQcckG0vPsccc6QRI0akQYMGpfPPPz+ddtppU6u9AAAALTM0xWYPVQcffHAWmPbZZ5+627p375523HHHNHbs2HTmmWemPn36NH1rAQAAWurFbet7++23U69evRq9b8EFF0wffPDBlLYLAACg9YamBRZYIN17772N3hfXcFpkkUWmtF0AAACtd8vxAw88MPXt2zcNGzYsrbXWWmmWWWZJn3/+eXrooYfSkCFD0mWXXdb0LQUAAGgtoWn99ddPF154YbrooovSwIEDsx30pplmmrTsssumq6++Oq2wwgpN31IAAIDWEpqefvrptOqqq6a11147ff/99+mrr75KM888c+rUqVPTtxAAAKC1rWmK3fNiKl6Ydtppsy3HBSYAAKAtKhWaZpxxxtS5c+embw0AAEBbmJ633377pZNPPjkNHTo0LbbYYqlr164/eUzv3r2bon0AAACtLzT1798/+3zOOedkn9u1a1d3X2wKEd8PHjy4qdoIAADQukLTtdde2/QtAQAAaCuhacUVV2z6lgAAALSV0BRiPdN5552XnnvuufT1119nF7iN6zPFhW8XWmihpm0lAABAawpNQ4YMSTvssENq3759dq2m2WabLX322Wfp0UcfTY899li67bbbBCcAAKB2Q9NZZ52V5plnnnTdddelGWaYoe72UaNGpd122y3bIOKCCy5oynYCAAC0nus0Pf/882n//fdvEJhCfL/vvvtm9wMAANRsaOrQoUOadtppG72vU6dOaezYsVPaLgAAgNYbmpZccsl04403Ztdkqi++v+GGG9Kvf/3rpmofAABAsyq1pqlv375pxx13TJtttlnacMMN0+yzz55tBPHAAw9ku+pdddVVTd9SAACA1hKaYqTp8ssvT//zP/+TbfgQI0zt2rXLRpguu+yy1Lt376ZvKQAAQGu6TtPKK6+cbr755mz9UlynacYZZ0zjx4//yeYQAAAANbemady4cal///5pu+22S126dEndu3dPL730UlpllVXS6aefnn744YembykAAEBrCU3nn39+uueee9LGG29cd1uvXr3S4Ycfnm699dZs6h4AAEDNTs+7995705FHHpl22GGHuttmnnnmtPvuu2fbkV977bXZ9ZoAAABqcqRpxIgRad555230vgUXXDANHz58StsFAADQekNTBKMHH3yw0fsGDRqU5p9//iltFwAAQOudnrfrrrumo446Ko0cOTKtu+66qVu3bunLL79Mjz76aLr//vvTaaed1vQtBQAAaC2haYsttkjffvttuuiii9JDDz1Ud/sss8ySjj/++Ox+AACAmr5O084775x22mmnNHTo0GzEKa7TFNP2ppmm1Iw/AACAFmmKEk5c1DZC05tvvplmnXXWNGzYsFSpVJqudQAAAK11pOniiy9Ol1xySfruu+9Su3bt0lJLLZUGDhyY7ax35ZVXZiNPAAAANTnSdP3112cXuN1jjz2yi9lWR5d+97vfpffffz+de+65Td1OAACA1hOarrvuuuzitX379k1LLLFE3e19+vRJhx56aLbtOAAAQM2Gpo8++iituOKKjd4Xm0F8/vnnU9ouAACA1hua5pprrvTSSy81et9//vOf7H4AAICa3Qhim222ydY0de7cOa255prZbaNHj04PPvhgtjlErHUCAACo2dC0zz77pA8++CCdddZZ2UfYddddsw0hNttss7Tffvs1dTsBAABaT2iKLcZPPPHEtOeee6Znnnkmu7jtDDPMkHr37p169uyZbrrppuzitwAAADUVmh5//PF05513ZqFp8803z3bLW2CBBeruf+GFF9JWW22VXexWaAIAAGoqNN1zzz3piCOOSB07dkydOnVK999/fzrvvPPSeuutl400nXzyyem+++5L7du3t6YJAACovdB0zTXXpKWXXjpdccUVWWg6+uij04UXXpgWXnjhLCR9/PHHaY011kjHHHNM6tGjx9RtNQAAQEsLTcOGDUsnnXRSmn766bPvDzrooLTRRhulAw44II0dOzade+65aYMNNpiabQUAAGi5oSm2FK9//aW555472y2vQ4cO2dS9bt26Ta02AgAAtPyL20ZAivVKVdWv//CHPwhMAABAmzXZoWlS5phjjqZpCQAAQFsMTbH9OAAAQFtV6DpNf/rTn+o2gojpeuH4449P00033U+CVOy2BwAAUDOhqXfv3g3C0qRua+x7AACANh+arrvuutTchg4dmrbaaqtsdCs+AwAAtPg1Tb+UcePGpcMPPzzb+hwAAOCX0mpC0/nnn1+3ngoAAOCX0ipC0/PPP59uueWWNGDAgOZuCgAAUGMK7Z7XHL7++ut0xBFHpOOOOy7NNddcTfKcsVHFpKb5xc5/Xbp0SbVqzJgxhTfyULPiNQvqpq8Vpa+Vo68Vp6+Vo68Vp6+Vo681Xc3itsm5hFKLD02xzfmyyy6bNt100yZdHzV48OBG74vO1KtXr1SrYrON6FRFqFnxmgV109eK0tfK0deK09fK0deK09fK0deatmadOnVq3aHprrvuSi+88EK69957m/R5O3bsmHr27NnofbV+sd4ePXqUOnNRy8rULKibvlaUvlaOvlacvlaOvlacvlaOvtZ0NRsyZMhk/fsWHZpuv/329MUXX6Q111yzwe39+/dPf/vb39Lll19e6nmj03Tt2rWJWtm21PKwbVlqVo66Fadm5ahbcWpWjroVp2blqFvT1Wxyw2SLDk1nnXVW+u677xrctv7666dDDjkkbbbZZs3WLgAAoHa06NDUvXv3Rm/v1q3bJO8DAACouS3HAQAAmkuLHmlqzJtvvtncTQAAAGqIkSYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAANDaQ9PIkSPTCSeckH7zm9+k5ZZbLu24447phRdeaO5mAQAANaBVhKZ+/fqll156KZ199tnp9ttvT4svvnjaa6+90jvvvNPcTQMAANq4Fh+a3n333fTkk0+mP/3pT2mFFVZIPXr0SMcff3yaY4450r333tvczQMAANq4Fh+aZplllnTppZemJZdcsu62du3aZR9ff/11s7YNAABo+zqkFm7GGWdMffr0aXDbgw8+mI1AHXPMMaWes1KppNGjRzd6X4SxLl26pFo1ZsyYrD5FqFnxmgV109eK0tfK0deK09fK0deK09fK0dearmZxW9Sm1Yemif3rX/9KRx99dFp//fXTmmuuWeo5xo0blwYPHtzofdGZevXqlWrV0KFDs05VhJoVr1lQN32tKH2tHH2tOH2tHH2tOH2tHH2taWvWqVOnthWa/v73v6fDDz8820HvrLPOKv08HTt2TD179mz0vslJmm1ZrBkrc+ailpWpWVA3fa0ofa0cfa04fa0cfa04fa0cfa3pajZkyJDJ+vetJjRdf/316ZRTTkkbbrhhOv300ycrEeZ1mq5duzZp+9qKWh62LUvNylG34tSsHHUrTs3KUbfi1KwcdStuUjWb3DDZ4jeCCDfeeGM66aST0s4775xtOz4lgQkAAKCIDq1h/uGpp56a1ltvvbTffvulzz//vO6+zp07pxlmmKFZ2wcAALRtLT40xU55sXHDww8/nH3Ut+WWW6YBAwY0W9sAAIC2r8WHpv333z/7AAAAaA6tYk0TAABAcxGaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACAHEITAABADqEJAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAEAAOQQmgAAAHIITQAAADmEJgAAgBxCEwAAQA6hCQAAIIfQBAAAkENoAgAAyCE0AQAA5BCaAAAAcghNAAAAOYQmAACA1h6afvjhh3TeeeelNdZYIy2zzDJpn332Se+//35zNwsAAKgBrSI0XXTRRenGG29MJ510Urr55puzELX33nunsWPHNnfTAACANq7Fh6YIRldeeWU65JBD0pprrpkWW2yxdM4556Thw4enhx56qLmbBwAAtHEtPjS98cYb6dtvv02rrLJK3W0zzjhj6tWrV3r++eebtW0AAEDb165SqVRSCxajSQcffHB6+eWXU+fOnetu79u3b/ruu+/SJZdcUuj5/vWvf6V4yR07dpzkY9q1a5e++nJUGj9+QqoVHTq0TzPNOkNWmzKymo38No2fUEM1a98+zTTzdKVrVq3byK9H11zdZp6x6xT1tRHfjK65389Zpi9fs7q6ffttGjfhh1QrOrafJs0yXfnf0ajZl6O/TeN/qKG+Nk37NGvXKX9fG/Fd7dVtls5T1tdGjf2m5mo2Q6fpp7ivjR73dfqhUjt1m6Zd+9S144xT1NfGjhuRfqiMT7VimnYdUqeOs0yyZuPGjcvqstxyy+U+T4fUwo0ZMyb73KlTpwa3TzvttOmrr74q/HxRlPqfJyUCRC36ubrkiQBRi6akZiECRC2akrpFgKhFU9rXIkDUoimpWwSIWjTFfa2zuhUVAaIWTWlfiwBRi6akbhEgalG7SdQsbp+cerb40FQdXYq1TfVHmr7//vvUpUuXws+37LLLNmn7AACAtq3Fr2maa665ss+ffvppg9vj++7duzdTqwAAgFrR4kNT7JY3/fTTp2effbbutq+//jq9/vrrqXfv3s3aNgAAoO1r8dPzYi3T7373u3TWWWelWWedNc0999zpzDPPTHPOOWdaf/31m7t5AABAG9fiQ1OIazSNHz8+HXfccdmOeTHCdMUVV+TugAcAAFATW44DAAA0pxa/pgkAAKA5CU0AAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAmIgrsgD1CU0AtAoOYvmlfPnll6ldu3bN3YxWye8pbZXQBPAL++6775q7Ca3OySefnIYOHdrczWh1fvjhh+ZuQqvzhz/8IZ1zzjnZ1wJAccImbVWH5m4ATevaa69NK664YlpsscWauymtyoUXXpg+/fTT9PXXX6cddtghLbnkkqlr167N3awW39e++uqr9P3332cHGe3bt88OMPzBzNe3b98022yzpYMPPjjNPPPMzd2cVmHHHXdMX3zxRTriiCOauymtyqmnnpo++eST9M0336Rdd901rbrqqqljx47N3awWbZdddknPP/986t27d/a997PJc/HFF2e/o3FC6KCDDkpzzjmnvwc/46677kqjRo3Kfj+32GKL1K1bt9SpU6fmblaLd8MNN2R1mzBhQtp2223TLLPM8ou9rwlNbcjbb7+drr766vSf//wn7b///mnBBRds7ia1CnvuuWc2FaNXr17ZmezDDjssHX300WnjjTdu7qa16AOLCEzxh/Hll19O77zzTrrooov8gZwMo0ePzt70Z5111qyOM844Y3M3qcUHprFjx2YHGHFA4UBs8uyxxx7ZQezaa6+dnnvuueykkMCUL06YjR8/Pv35z39Of/nLX7ITQlGzaaYxKSfP7rvvnkaOHJkWXnjh9OKLL6bBgwen22+/Pfs99fs66d/POPCfaaaZ0nvvvZduu+229Lvf/S5ts802/ibkiL+Z8b4Wfz8//PDDdOedd2YnhDbddNMsPE1tQlMbMsccc2TJ+4UXXkj/+7//m/bbb7+00EILNXezWrRLLrkke+O66aabUpcuXbLbdtppp3TNNdcITZNw7rnnpm+//TY7qIg/iPHHMT7iDGPnzp2bu3ktXowCR9C84IILspHNGHGafvrpm7tZLfbAIk5oPPjgg3W3jRs3Lnuf69ChQza6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+NlSSy3V4DniukJnnnlmdsGvDTbYINUCdStOzcpRt+LUrBx1K07NylG34tSsRuv2i0Y0Jsuzzz5b2XLLLStDhgypS9ZPPPFE5eyzz64sueSS2XzPpZdeOkvhY8eOrUvtjz76aHZ7LZ2xqE/dilOzctStODUrR92KU7Ny1K04NautuglNzWz48OENhioffvjhyoorrljZfffdK99++21l3LhxDR7/4YcfVs4999zK9ttvX1l55ZUrX3/9dd19I0eOrLzwwguVWqBuxalZOepWnJqVo27FqVk56lacmpXTlupmy/FmNHbs2DRw4MB04403Zt+/8cYb2dDlwgsvnF555ZX0+eefpw4dOmSPq269+Ktf/SodeOCB6dprr03zzjtvuuiii7L7xo8fn2aaaaa0/PLLp7ZO3YpTs3LUrTg1K0fdilOzctStODUrp63VTWhqRp06dco6x8knn5zN4Yw5miuuuGLq27dvmnvuudNee+2Vvvnmm+xx0VmqF+2Kz3Fbr1690siRI7PbotPVCnUrTs3KUbfi1KwcdStOzcpRt+LUrJy2VjehqZkdfPDBafHFF0///ve/0yKLLJJmnnnmtNxyy6XjjjsuTTvttGnrrbfOOlR0luhQVSNGjMgW0H3wwQdZQq+1/TzUrTg1K0fdilOzctStODUrR92KU7Ny2lLdhKZmUP9//JdffpkNP26zzTbpqquuSldffXWWsGP48YQTTsiS9rbbbptGjRrVIGVX/92xxx6bPaZ6ZeS2TN2KU7Ny1K04NStH3YpTs3LUrTg1K6fSVuvWbKupalT9xXDVxW2xM0g455xzKosuumjlqquuqnvM888/X1ljjTUqhxxyyE+e6/vvv6/UCnUrTs3KUbfi1KwcdStOzcpRt+LUrJwJbbhuzT9BsIZE8q7uT3/FFVdke9J37do1m+e5yiqrZHM8I0mffvrp2eeddtopffvtt9kiuqWXXvonzxfJuxaoW3FqVo66Fadm5ahbcWpWjroVp2blVNp63Zo7tdWK+lcyHjhwYHYl45NOOqmy/vrrV9Zee+3KrbfemiXqeFxstRhJvHfv3pUDDzyw7t+OHz++UmvUrTg1K0fdilOzctStODUrR92KU7NyfqiBuglNv7DYf36fffapvPTSS3W37bfffpUNNtigrkNV97E/77zz6vavr98Za5G6Fadm5ahbcWpWjroVp2blqFtxalbOh224bkLTL+iaa66prLnmmpXNNtus8t5779XdHnM963eouNhXfRNf+KvWqFtxalaOuhWnZuWoW3FqVo66Fadm5VzTxutm97ypaOLtETfddNPUuXPn9Oabb9Zd4Ct07NgxXXDBBWnBBRdMZ5xxRnrqqaca/LuWsDf9L0ndilOzctStODUrR92KU7Ny1K04NSunUmt1a+7UVguuu+66yv333599/dVXX1XWXXfdykYbbZTtGFJ/ODKS9hlnnNHi53T+UtStODUrR92KU7Ny1K04NStH3YpTs3Kuq5G6CU1T2fDhwyt77LFHZbXVVqsMGjQou23EiBGVtdZaq7Lpppv+pENVtdYO1VTUrTg1K0fdilOzctStODUrR92KU7NyhtdQ3YSmqbQ/ff0OEovh+vbtW+nTp0/lkUceqetQsZvIFltsUXnqqadaxQK4qUndilOzctStODUrR92KU7Ny1K04NStnQg3XTWiaism7vn//+9+Vgw8+OOtQ1SQ+cuTIylJLLVU54ogjmqmVLY+6Fadm5ahbcWpWjroVp2blqFtxalbO8Bqsm9A0Fdx3332V5ZdfPhuSrC+S+N577135zW9+U3nyySez20aNGtUqhyinBnUrTs3KUbfi1KwcdStOzcpRt+LUrJz7arRuds+bCmaeeebsysbHHntseuGFF+puX2aZZdJGG22UPvnkk7TvvvumZ599Nk0//fSpffv2acKECanWqVtxalaOuhWnZuWoW3FqVo66Fadm5cxcq3Vr7tTWVuZ2TuyVV16p7Lnnntl8zvpJ/MUXX6z069evctttt7WZ5F2GuhWnZuWoW3FqVo66Fadm5ahbcWpWjrr9qF38p7mDW2sV+89PM83/Ddbde++9adiwYemjjz5Kq622WlpvvfXS+++/nwYMGJDeeeed1L9//9SzZ890yimnpNlmmy2deOKJ2b+L5B0JvJaoW3FqVo66Fadm5ahbcWpWjroVp2blqFtDQlMTiAt13XHHHWmFFVZIb7/9dhozZkxaeOGF08CBA9MHH3yQzj777PSPf/wj/epXv0rTTTdd9ti40FeUvl27dqlWqVtxalaOuhWnZuWoW3FqVo66Fadm5ajb/1dv1IkSHn744ewiXq+99lrdbTfddFNls802y4Yn40JeX375ZeWJJ57ILvxVHaqM22uZuhWnZuWoW3FqVo66Fadm5ahbcWpWjrr9qEM1PPHzvvjii/TZZ59lw4yRsMN7772X5pprrrTQQgulcePGZcl6q622Sl999VW6+eabs6HMGK5cffXV654nhio7dKid0qtbcWpWjroVp2blqFtxalaOuhWnZuWoWz67502mY445JvXt2zdtscUW2cdVV12V3R6dJTrOtNNOm3WksWPHpk6dOqWtt946DR8+PBvGnFhbmds5OdStODUrR92KU7Ny1K04NStH3YpTs3LU7ecJTZNhjz32yBa57bnnnunaa6/N5nDGlophk002yTrMJZdckn0fHSlEB1tsscWy+Z21St2KU7Ny1K04NStH3YpTs3LUrTg1K0fdJk/bGztrYueff3624O2iiy5Ks846a4PdRGKB21JLLZV22WWXdOWVV6Zvv/02bb/99umbb75J55xzTurcuXNaYoklUi1St+LUrBx1K07NylG34tSsHHUrTs3KUbfJJzT9jNdffz2tu+66dR0pVLdfjB1BosN06dIl7bPPPumKK67IEnr37t1Tt27dsq/jsfW3bKwV6lacmpWjbsWpWTnqVpyalaNuxalZOeo2+YSmSYgOEIvh/vWvf6Xddtut7raJO8WIESPSfffdl/r06ZPtYf/aa69lHalXr17ZY8ePH98mF8NNiroVp2blqFtxalaOuhWnZuWoW3FqVo66Fdf2Y2FJ0RHi4lzTTz99evrpp+tuqy+GLWeZZZbUo0eP9Mknn2SPj07161//ui5510pHqlK34tSsHHUrTs3KUbfi1KwcdStOzcpRt+KEpkmIjhJbKy644ILpmWeeSUOHDv3JY6oX7IqtFeNiXhOrhaHKialbcWpWjroVp2blqFtxalaOuhWnZuWoW3G19WoLqM7jPOigg7L5npdffnk2jDmxL7/8Mo0aNSobpkTdylCzctStODUrR92KU7Ny1K04NStH3Uqod6FbJuHWW2+tLLHEEpWDDz648tRTT9Vd6fjDDz+s7LfffpXtttuu7grI/EjdilOzctStODUrR92KU7Ny1K04NStH3SZPu/hPmbBVS6JEDz/8cOrfv392wa7YNSQSeszjjKHJa665JrvgVwxfttULepWhbsWpWTnqVpyalaNuxalZOepWnJqVo26TR2gq4KOPPkpPPfVUeuWVV7KFcYsuumjaYIMNsg5US7uHFKVuxalZOepWnJqVo27FqVk56lacmpWjbvmEpiZQ68m7LHUrTs3KUbfi1KwcdStOzcpRt+LUrBx1+z9CU0FRrupuIvW/Jp+6Fadm5ahbcWpWjroVp2blqFtxalaOuk2a0AQAAJDDluMAAAA5hCYAAIAcQhMAAEAOoQkAACCH0AQAAJBDaAIAAMghNAHAZHKVDoDa1KG5GwAAE3v11VfTtddem55//vn05ZdfpjnmmCOtssoqad99903zzjtvs7TpoosuSp06dUp77713s/x8AJqPkSYAWpQbbrgh7bDDDumLL75Ihx12WLrsssuysPTcc8+lbbbZJr3xxhvN0q5zzz03jRkzpll+NgDNy0gTAC3Giy++mE455ZS08847p2OPPbbu9pVWWimtu+66aYsttkjHHHNMuuOOO5q1nQDUFiNNALQYV1xxRZphhhlSv379fnLfrLPOmo466qi0zjrrpNGjR6cJEyZko1KbbrppWmqppdKaa66ZzjrrrPT999/X/Ztddtkl+6jv2WefTYsuumj2OUQA69WrV3r55ZfT9ttvn5Zccsm01lprZW2piseHCy64oO7r888/P6233nrZbSuuuGJaffXV03HHHZe1ZdSoUT+Z2rf88ssbqQJopYQmAFrMJgv//Oc/s7VLXbp0afQxG220UTrwwANT165d0wknnJBOO+20bATq4osvzkanrr/++nTAAQcU3rDhhx9+SIceemj2/Jdeemlabrnl0hlnnJGeeOKJ7P5bbrkl+xzTA6tfh48++ij94x//SOecc046+uij0x577JGFtgceeKDB8999993Zc0/qdQHQspmeB0CLMGLEiCxwzDPPPD/72CFDhqS//OUv2ZqnWO8UVltttWzDiCOOOCI9/vjjqU+fPpP9syNkRdjadttts+9jVOjhhx9Ojz32WFpjjTXSMsssk90+55xz1n0dxo8fn4488si0wgor1N227LLLZiGp+lz/+te/0rBhw9KAAQMKVAOAlsRIEwAtQvv27bPPMe3u58SmEGHjjTducHt8H89TnXpXRISdqtglL6YDxjTAn7P44os3+H7rrbdOL7zwQvrwww+z7++8887Uo0ePBs8PQOsiNAHQIsw000xpuummy6a8TUqEmK+++ir7CLPPPnuD+zt06JBmmWWWn6wpmhydO3du8P0000wzWdP8os31VafhxWhTjJzdf//9aauttircHgBaDqEJgBYjNlOIUaL6mznUd+utt6aVV1657vvPPvuswf3jxo3LpvlFcKqaeORqckaPpkSEqA033DALS7EmKn7e5ptvPlV/JgBTl9AEQIux5557ppEjR6aBAwf+5L4ISFdeeWXq2bNntmtduO+++xo8Jr6PkBRrksL000+fhg8f/pNtzcuIkafJFRtGvPXWW+maa65Jq666aurevXupnwlAy2AjCABajNhkoW/fvlloevvtt7PrMsWo0X//+99sC/AYgYr7FlpoobTlllum8847L9vGu3fv3mnw4MHZ9t9xTafYvCHE1uGDBg3Kdtlbe+21s7VGd911V6m2zTjjjNmmDs8//3yDjR8aE6Et1jHF2qvYWQ+A1k1oAqBF+f3vf59dNymuwXTqqadm65fmmmuu7DpM+++/f/Z1iIvgzj///On2229Pl112WbZz3q677prtglcdFYpNGd57771sM4abb745C1cRtHbcccfC7YqfHddb2meffdLf/va3n318tPfLL7/MtkQHoHVrVyl6MQsAIFf8aY2d/GKN1jHHHNPczQFgChlpAoAm8s0336Srr746vfrqq+n9999Pu+yyS3M3CYAmIDQBQBOJbctjGuAPP/yQTS2cd955m7tJADQB0/MAAABy2HIcAAAgh9AEAACQQ2gCAADIITQBAADkEJoAAAByCE0AAAA5hCYAAIAcQhMAAEAOoQkAACBN2v8DXW28pR6Q0MgAAAAASUVORK5CYII=",
+ "text/plain": [
+ "<Figure size 1000x600 with 1 Axes>"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# Plot bar chart\n",
+ "plt.figure(figsize=(10, 6))\n",
+ "sns.barplot(data=country_counts, x=\"Country\", y=\"RecordCount\", palette=\"viridis\")\n",
+ "plt.title(\"Record Count by Country\")\n",
+ "plt.xticks(rotation=45)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "887b2525",
+ "metadata": {},
+ "source": [
+ "## πŸ“ˆ Pivot Table Example"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "1e26e06b",
+ "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>Age</th>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country</th>\n",
+ " <th></th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>Country_0</th>\n",
+ " <td>43.7</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_1</th>\n",
+ " <td>43.8</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_2</th>\n",
+ " <td>51.0</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_3</th>\n",
+ " <td>38.2</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_4</th>\n",
+ " <td>44.3</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_5</th>\n",
+ " <td>48.3</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_6</th>\n",
+ " <td>47.3</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_7</th>\n",
+ " <td>41.1</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_8</th>\n",
+ " <td>40.5</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>Country_9</th>\n",
+ " <td>47.1</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " Age\n",
+ "Country \n",
+ "Country_0 43.7\n",
+ "Country_1 43.8\n",
+ "Country_2 51.0\n",
+ "Country_3 38.2\n",
+ "Country_4 44.3\n",
+ "Country_5 48.3\n",
+ "Country_6 47.3\n",
+ "Country_7 41.1\n",
+ "Country_8 40.5\n",
+ "Country_9 47.1"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "# If 'Age' exists, average age by country (example only if dataset has relevant column)\n",
+ "if \"Age\" in df.columns:\n",
+ " age_pivot = df.pivot_table(index=\"Country\", values=\"Age\", aggfunc=\"mean\")\n",
+ " display(age_pivot)\n",
+ "else:\n",
+ " print(\"No 'Age' column in dataset to pivot on.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d9793c4e",
+ "metadata": {},
+ "source": [
+ "\n",
+ "## πŸŽ‰ Summary\n",
+ "\n",
+ "In this notebook, we:\n",
+ "- Loaded a sample CSV dataset\n",
+ "- Explored its structure\n",
+ "- Grouped and counted records by country\n",
+ "- Visualized results with a bar chart\n",
+ "- Created a pivot table (if applicable)\n",
+ "\n",
+ "πŸ–₯️ Try modifying this notebook for your own datasets or audit use cases!\n",
+ " "
+ ]
+ }
+ ],
+ "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
+}
diff --git a/notebooks/racf_access_analysis.ipynb b/notebooks/racf_access_analysis.ipynb
new file mode 100644
index 0000000..48bea8d
--- /dev/null
+++ b/notebooks/racf_access_analysis.ipynb
@@ -0,0 +1,526 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "4c3a8609",
+ "metadata": {},
+ "source": [
+ "\n",
+ "# πŸ›‘οΈ RACF Access Report Analysis\n",
+ "\n",
+ "This notebook demonstrates how to parse a RACF-like mainframe access report stored in a fixed-width text format, extract user access details, identify unusual access configurations, and summarize the results for follow-up.\n",
+ "\n",
+ "We'll be working with the file `sample_racf_data.txt`.\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a0b3e2b2",
+ "metadata": {},
+ "source": [
+ "\n",
+ "## πŸ“¦ Install Dependencies\n",
+ "\n",
+ "If you haven't already installed `pandas`, run:\n",
+ "\n",
+ "```bash\n",
+ "pip install pandas\n",
+ "```\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "ca527b49",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pandas as pd\n",
+ "import re"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d1694149",
+ "metadata": {},
+ "source": [
+ "## πŸ“‚ Load RACF Report"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "4891d98b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "LISTGRP *\n",
+ "INFORMATION FOR GROUP PAYROLLB\n",
+ "SUPERIOR GROUP=RESEARCH OWNER=IBMUSER CREATED=06.123\n",
+ "NO INSTALLATION DATA\n",
+ "NO MODEL DATA SET\n",
+ "TERMUACC\n",
+ "NO SUBGROUPS\n",
+ "USER(S)= ACCESS= ACCESS COUNT= UNIVERSAL ACCESS=\n",
+ "IBMUSER JOIN 000000 ALTER\n",
+ "CONNECT ATTRIBUTES=NONE\n",
+ "REVOKE DATE=NONE RESUME DATE=NONE\n",
+ "DAF0 CREATE 000000 READ\n",
+ "CONNECT ATTRIBUTES=NONE\n",
+ "REVOKE DATE=NONE RESUME DATE=NONE\n",
+ "IA0 CREATE 000000 READ\n",
+ "CONNECT ATTRIBUTES=ADSP SPECIAL OPERATIONS\n",
+ "REVOKE DATE=NONE RESUME DATE=NONE\n",
+ "AEH0 CREATE 000000 READ\n",
+ "CONNECT ATTRIBUTES=NONE\n",
+ "REVOKE DATE=NONE RESUME DATE=NONE\n"
+ ]
+ }
+ ],
+ "source": [
+ "with open(\"sample_racf_data.txt\", \"r\") as file:\n",
+ " lines = file.readlines()\n",
+ "\n",
+ "# Preview first 20 lines\n",
+ "for line in lines[:20]:\n",
+ " print(line.strip())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9d5ee64a",
+ "metadata": {},
+ "source": [
+ "## πŸ“ Parse User Access Records"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "18f06bc9",
+ "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>Group</th>\n",
+ " <th>User</th>\n",
+ " <th>Access</th>\n",
+ " <th>Access Count</th>\n",
+ " <th>Universal Access</th>\n",
+ " <th>Attributes</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>PAYROLLB</td>\n",
+ " <td>IBMUSER</td>\n",
+ " <td>JOIN</td>\n",
+ " <td>0</td>\n",
+ " <td>ALTER</td>\n",
+ " <td>NONE</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>1</th>\n",
+ " <td>PAYROLLB</td>\n",
+ " <td>DAF0</td>\n",
+ " <td>CREATE</td>\n",
+ " <td>0</td>\n",
+ " <td>READ</td>\n",
+ " <td>NONE</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>2</th>\n",
+ " <td>PAYROLLB</td>\n",
+ " <td>IA0</td>\n",
+ " <td>CREATE</td>\n",
+ " <td>0</td>\n",
+ " <td>READ</td>\n",
+ " <td>ADSP SPECIAL OPERATIONS</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>3</th>\n",
+ " <td>PAYROLLB</td>\n",
+ " <td>AEH0</td>\n",
+ " <td>CREATE</td>\n",
+ " <td>0</td>\n",
+ " <td>READ</td>\n",
+ " <td>NONE</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>4</th>\n",
+ " <td>RESEARCH</td>\n",
+ " <td>IBMUSER</td>\n",
+ " <td>JOIN</td>\n",
+ " <td>0</td>\n",
+ " <td>ALTER</td>\n",
+ " <td>NONE</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " Group User Access Access Count Universal Access \\\n",
+ "0 PAYROLLB IBMUSER JOIN 0 ALTER \n",
+ "1 PAYROLLB DAF0 CREATE 0 READ \n",
+ "2 PAYROLLB IA0 CREATE 0 READ \n",
+ "3 PAYROLLB AEH0 CREATE 0 READ \n",
+ "4 RESEARCH IBMUSER JOIN 0 ALTER \n",
+ "\n",
+ " Attributes \n",
+ "0 NONE \n",
+ "1 NONE \n",
+ "2 ADSP SPECIAL OPERATIONS \n",
+ "3 NONE \n",
+ "4 NONE "
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Initialize lists to hold parsed records\n",
+ "records = []\n",
+ "current_group = \"\"\n",
+ "\n",
+ "for i, line in enumerate(lines):\n",
+ " if \"INFORMATION FOR GROUP\" in line:\n",
+ " current_group = line.strip().split()[-1]\n",
+ "\n",
+ " # Identify user lines: starts with a non-empty, non-space string followed by access keywords\n",
+ " match = re.match(r\"^\\s*(\\S+)\\s+(JOIN|CREATE|CONNECT|USE)\\s+(\\d{6})\\s+(\\S+)\", line)\n",
+ " if match:\n",
+ " user, access, access_count, universal_access = match.groups()\n",
+ "\n",
+ " # Look ahead for CONNECT ATTRIBUTES line\n",
+ " attr_line = lines[i + 1].strip() if (i + 1) < len(lines) else \"\"\n",
+ " attr_match = re.search(r\"CONNECT ATTRIBUTES=(.*)\", attr_line)\n",
+ " attributes = attr_match.group(1) if attr_match else \"NONE\"\n",
+ "\n",
+ " records.append(\n",
+ " {\n",
+ " \"Group\": current_group,\n",
+ " \"User\": user,\n",
+ " \"Access\": access,\n",
+ " \"Access Count\": int(access_count),\n",
+ " \"Universal Access\": universal_access,\n",
+ " \"Attributes\": attributes,\n",
+ " }\n",
+ " )\n",
+ "\n",
+ "# Convert to DataFrame\n",
+ "df = pd.DataFrame(records)\n",
+ "df.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ab5546a6",
+ "metadata": {},
+ "source": [
+ "## πŸ“Š Analyze Access Data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "d1b8269a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Access\n",
+ "CREATE 5\n",
+ "JOIN 4\n",
+ "USE 3\n",
+ "CONNECT 1\n",
+ "Name: count, dtype: int64\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>Group</th>\n",
+ " <th>User</th>\n",
+ " <th>Access</th>\n",
+ " <th>Access Count</th>\n",
+ " <th>Universal Access</th>\n",
+ " <th>Attributes</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>PAYROLLB</td>\n",
+ " <td>IBMUSER</td>\n",
+ " <td>JOIN</td>\n",
+ " <td>0</td>\n",
+ " <td>ALTER</td>\n",
+ " <td>NONE</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>2</th>\n",
+ " <td>PAYROLLB</td>\n",
+ " <td>IA0</td>\n",
+ " <td>CREATE</td>\n",
+ " <td>0</td>\n",
+ " <td>READ</td>\n",
+ " <td>ADSP SPECIAL OPERATIONS</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>4</th>\n",
+ " <td>RESEARCH</td>\n",
+ " <td>IBMUSER</td>\n",
+ " <td>JOIN</td>\n",
+ " <td>0</td>\n",
+ " <td>ALTER</td>\n",
+ " <td>NONE</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>6</th>\n",
+ " <td>RESEARCH</td>\n",
+ " <td>IA0</td>\n",
+ " <td>CONNECT</td>\n",
+ " <td>4</td>\n",
+ " <td>READ</td>\n",
+ " <td>ADSP SPECIAL OPERATIONS</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " Group User Access Access Count Universal Access \\\n",
+ "0 PAYROLLB IBMUSER JOIN 0 ALTER \n",
+ "2 PAYROLLB IA0 CREATE 0 READ \n",
+ "4 RESEARCH IBMUSER JOIN 0 ALTER \n",
+ "6 RESEARCH IA0 CONNECT 4 READ \n",
+ "\n",
+ " Attributes \n",
+ "0 NONE \n",
+ "2 ADSP SPECIAL OPERATIONS \n",
+ "4 NONE \n",
+ "6 ADSP SPECIAL OPERATIONS "
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Count users by Access type\n",
+ "access_summary = df[\"Access\"].value_counts()\n",
+ "print(access_summary)\n",
+ "\n",
+ "# Identify users with ALTER access or SPECIAL OPERATIONS attribute\n",
+ "anomalies = df[\n",
+ " (df[\"Universal Access\"] == \"ALTER\")\n",
+ " | (df[\"Attributes\"].str.contains(\"SPECIAL OPERATIONS\"))\n",
+ "]\n",
+ "\n",
+ "anomalies"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "38201382",
+ "metadata": {},
+ "source": [
+ "## πŸ“‘ Prepare Follow-Up Report"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "a885710c",
+ "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>Group</th>\n",
+ " <th>User</th>\n",
+ " <th>Access</th>\n",
+ " <th>Universal Access</th>\n",
+ " <th>Attributes</th>\n",
+ " <th>Notes</th>\n",
+ " </tr>\n",
+ " </thead>\n",
+ " <tbody>\n",
+ " <tr>\n",
+ " <th>0</th>\n",
+ " <td>PAYROLLB</td>\n",
+ " <td>IBMUSER</td>\n",
+ " <td>JOIN</td>\n",
+ " <td>ALTER</td>\n",
+ " <td>NONE</td>\n",
+ " <td>Review access appropriateness with system owner</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>1</th>\n",
+ " <td>PAYROLLB</td>\n",
+ " <td>IA0</td>\n",
+ " <td>CREATE</td>\n",
+ " <td>READ</td>\n",
+ " <td>ADSP SPECIAL OPERATIONS</td>\n",
+ " <td>Review access appropriateness with system owner</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>2</th>\n",
+ " <td>RESEARCH</td>\n",
+ " <td>IBMUSER</td>\n",
+ " <td>JOIN</td>\n",
+ " <td>ALTER</td>\n",
+ " <td>NONE</td>\n",
+ " <td>Review access appropriateness with system owner</td>\n",
+ " </tr>\n",
+ " <tr>\n",
+ " <th>3</th>\n",
+ " <td>RESEARCH</td>\n",
+ " <td>IA0</td>\n",
+ " <td>CONNECT</td>\n",
+ " <td>READ</td>\n",
+ " <td>ADSP SPECIAL OPERATIONS</td>\n",
+ " <td>Review access appropriateness with system owner</td>\n",
+ " </tr>\n",
+ " </tbody>\n",
+ "</table>\n",
+ "</div>"
+ ],
+ "text/plain": [
+ " Group User Access Universal Access Attributes \\\n",
+ "0 PAYROLLB IBMUSER JOIN ALTER NONE \n",
+ "1 PAYROLLB IA0 CREATE READ ADSP SPECIAL OPERATIONS \n",
+ "2 RESEARCH IBMUSER JOIN ALTER NONE \n",
+ "3 RESEARCH IA0 CONNECT READ ADSP SPECIAL OPERATIONS \n",
+ "\n",
+ " Notes \n",
+ "0 Review access appropriateness with system owner \n",
+ "1 Review access appropriateness with system owner \n",
+ "2 Review access appropriateness with system owner \n",
+ "3 Review access appropriateness with system owner "
+ ]
+ },
+ "execution_count": 6,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Create a concise follow-up report\n",
+ "follow_up = anomalies[\n",
+ " [\"Group\", \"User\", \"Access\", \"Universal Access\", \"Attributes\"]\n",
+ "].copy()\n",
+ "follow_up[\"Notes\"] = \"Review access appropriateness with system owner\"\n",
+ "\n",
+ "follow_up.reset_index(drop=True, inplace=True)\n",
+ "follow_up"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0158f787",
+ "metadata": {},
+ "source": [
+ "\n",
+ "## πŸŽ‰ Summary\n",
+ "\n",
+ "In this notebook, we:\n",
+ "- Parsed a RACF-like access report from a fixed-width text file\n",
+ "- Extracted key fields into a structured DataFrame\n",
+ "- Analyzed access configurations for high-risk permissions\n",
+ "- Summarized anomalies requiring follow-up with system owners\n",
+ "\n",
+ "πŸ–₯️ Use this as a starting point for mainframe audit automation projects!\n",
+ " "
+ ]
+ }
+ ],
+ "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
+}
diff --git a/notebooks/sample_racf_data.txt b/notebooks/sample_racf_data.txt
new file mode 100644
index 0000000..a3caa69
--- /dev/null
+++ b/notebooks/sample_racf_data.txt
@@ -0,0 +1,54 @@
+LISTGRP *
+INFORMATION FOR GROUP PAYROLLB
+ SUPERIOR GROUP=RESEARCH OWNER=IBMUSER CREATED=06.123
+ NO INSTALLATION DATA
+ NO MODEL DATA SET
+ TERMUACC
+ NO SUBGROUPS
+ USER(S)= ACCESS= ACCESS COUNT= UNIVERSAL ACCESS=
+ IBMUSER JOIN 000000 ALTER
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+ DAF0 CREATE 000000 READ
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+ IA0 CREATE 000000 READ
+ CONNECT ATTRIBUTES=ADSP SPECIAL OPERATIONS
+ REVOKE DATE=NONE RESUME DATE=NONE
+ AEH0 CREATE 000000 READ
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+INFORMATION FOR GROUP RESEARCH
+ SUPERIOR GROUP=SYS1 OWNER=IBMUSER CREATED=06.123
+ NO INSTALLATION DATA
+ NO MODEL DATA SET
+ TERMUACC
+ SUBGROUP(S)= PAYROLLB
+ USER(S)= ACCESS= ACCESS COUNT= UNIVERSAL ACCESS=
+ IBMUSER JOIN 000000 ALTER
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+ DAF0 JOIN 000002 READ
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+ IA0 CONNECT 000004 READ
+ CONNECT ATTRIBUTES=ADSP SPECIAL OPERATIONS
+ REVOKE DATE=NONE RESUME DATE=NONE
+ ESH25 USE 000000 READ
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+ PROJECTB USE 000000 READ
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+ RV2 CREATE 000002 READ
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+ RV3 CREATE 000000 READ
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE
+ ADM1 JOIN 000001 READ
+ CONNECT ATTRIBUTES=OPERATIONS
+ REVOKE DATE=NONE RESUME DATE=NONE
+ AEH0 USE 000000 READ
+ CONNECT ATTRIBUTES=NONE
+ REVOKE DATE=NONE RESUME DATE=NONE