{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "1709c055",
   "metadata": {},
   "source": [
    "# ACRO Demonstration Guide for Researchers\n",
    "## Overview\n",
    "- This guide provides a step-by-step walkthrough for researchers to safely generate and review statistical outputs using ACRO package.\n",
    "- The examples use simple OMOP-style data to demonstrate how ACRO automatically performs Statistical Disclosure Checking (SDC) while allowing flexible exploratory analysis.\n",
    "\n",
    "- The aim is to show that:\n",
    "    - Researchers can analyse OMOP data using familiar commands (pandas, stats models, etc)\n",
    "    - ACRO automatically reviews each output for disclosure risk\n",
    "    - Output checkers receive structures, documented results ready for review\n",
    "\n",
    "- What is ACRO?\n",
    "    - ACRO is a safe wrapper around familiar Python analytics (pandas, statsmodels, plotting). It runs Statistical Disclosure Control (SDC) automatically on Every output so you can explore data and still produce release-ready tables/figures.\n",
    "\n",
    "\n",
    "## Prerequisites & Files\n",
    "### Make sure you have OMOP-style CSVs in your working folder, e.g.:\n",
    "-\tperson.csv\n",
    "-\tvisit_occurrence.csv\n",
    "-\tcondition_occurrence.csv\n",
    "-\t(optional) drug_exposure.csv, procedure_occurrence.csv, measurement.csv\n",
    "## Let’s start with ACRO import, initialisation and loading the data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ad3b243a",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:version: 0.4.11\n",
      "INFO:acro:config: {'safe_threshold': 10, 'safe_dof_threshold': 10, 'safe_nk_n': 2, 'safe_nk_k': 0.9, 'safe_pratio_p': 0.1, 'check_missing_values': False, 'survival_safe_threshold': 10, 'zeros_are_disclosive': True}\n",
      "INFO:acro:automatic suppression: False\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>person_id</th>\n",
       "      <th>year_of_birth</th>\n",
       "      <th>month_of_birth</th>\n",
       "      <th>gender_concept_id</th>\n",
       "      <th>race_source_value</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>1968</td>\n",
       "      <td>9</td>\n",
       "      <td>8507</td>\n",
       "      <td>white</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2</td>\n",
       "      <td>1960</td>\n",
       "      <td>3</td>\n",
       "      <td>8532</td>\n",
       "      <td>white</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>1995</td>\n",
       "      <td>8</td>\n",
       "      <td>8532</td>\n",
       "      <td>black</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4</td>\n",
       "      <td>1933</td>\n",
       "      <td>4</td>\n",
       "      <td>8532</td>\n",
       "      <td>white</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5</td>\n",
       "      <td>1941</td>\n",
       "      <td>12</td>\n",
       "      <td>8532</td>\n",
       "      <td>white</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   person_id  year_of_birth  month_of_birth  gender_concept_id  \\\n",
       "0          1           1968               9               8507   \n",
       "1          2           1960               3               8532   \n",
       "2          3           1995               8               8532   \n",
       "3          4           1933               4               8532   \n",
       "4          5           1941              12               8532   \n",
       "\n",
       "  race_source_value  \n",
       "0             white  \n",
       "1             white  \n",
       "2             black  \n",
       "3             white  \n",
       "4             white  "
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "import pandas as pd\n",
    "from acro import ACRO\n",
    "from pathlib import Path\n",
    "import os\n",
    "\n",
    "\n",
    "#Initiate ACRO\n",
    "acro = ACRO(suppress=False)\n",
    "DATA_DIR = Path(\"<Enter_the_path>\")\n",
    "#Load Omop tables\n",
    "person = pd.read_csv(DATA_DIR/ \"person.csv\")\n",
    "condition = pd.read_csv(DATA_DIR/ \"condition_occurrence.csv\")\n",
    "\n",
    "#Display sample rows\n",
    "person.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3af5b01b",
   "metadata": {},
   "source": [
    "## Creating a Crosstab\n",
    "\n",
    "Researchers can summarise the relationships between two variables using ACRO's safe crosstab()\n",
    "\n",
    "- What ACRO checks: small checks, k-anonymity rules, missingness\n",
    "- If risky: ACRO flags/suppresses before showing the table."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "0d22a8b8",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:get_summary(): fail; threshold: 48 cells may need suppressing; \n",
      "INFO:acro:outcome_df:\n",
      "-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n",
      "Condition |0   |28060 |75036 |78272 |79740 |80502 |80809        |81151 |133834 |134438 |137613 |192279 |195588 |196456       |197500       |198199       |198809       |199076 |200970 |201826 |255848 |257012 |258780 |260139 |261325 |313217 |316866 |317009       |317576 |321042 |372328 |375671 |378001 |378419 |380378 |381316 |432867 |436096 |436676       |436940 |438130 |439393 |439777 |440086 |440448 |440674 |441267       |443597       |443601 |443614 |444362       |4001336 |4029498 |4035415 |4043241 |4048171 |4048695      |4051466 |4055754 |4056621 |4059173 |4066995      |4078393 |4079749 |4079750 |4084167 |4094814 |4109685 |4110591      |4112343 |4113008 |4115276 |4116491 |4120314 |4129902 |4132546      |4134304 |4142905 |4144583      |4146173 |4149245 |4152280      |4152936 |4155034 |4156265 |4166224 |4166590 |4180790 |4196262 |4208104 |4214376 |4217975 |4218106 |4218389 |4226121 |4229440 |4230399 |4237458 |4245614      |4262136 |4278672 |4280726 |4282096      |4283893 |4285898 |4294548 |4296204 |4296205 |4299128      |4309027 |4310703 |4311629 |4311765      |4316217      |4329847 |4341247      |37016354 |37017023 |37395648     |40318618 |40479422 |40479768 |40480160 |40481087 |40486433 |43530652 |43530656 |43530685     |44782520     |45757499     |45763582     |45769905 |45770830|\n",
      "Gender    |    |      |      |      |      |      |             |      |       |       |       |       |       |             |             |             |             |       |       |       |       |       |       |       |       |       |       |             |       |       |       |       |       |       |       |       |       |       |             |       |       |       |       |       |       |       |             |             |       |       |             |        |        |        |        |        |             |        |        |        |        |             |        |        |        |        |        |        |             |        |        |        |        |        |        |             |        |        |             |        |        |             |        |        |        |        |        |        |        |        |        |        |        |        |        |        |        |        |             |        |        |        |             |        |        |        |        |        |             |        |        |        |             |             |        |             |         |         |             |         |         |         |         |         |         |         |         |             |             |             |             |         |        |\n",
      "-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n",
      "Female    | ok | ok   | ok   | ok   | ok   | ok   |          ok | ok   | ok    | ok    | ok    | ok    | ok    | threshold;  | threshold;  | threshold;  | threshold;  | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | threshold;  | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | threshold;  | ok    | ok    | ok    | ok    | ok    | ok    | ok    | threshold;  | threshold;  | ok    | ok    | threshold;  | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | threshold;  | ok     | ok     |          ok | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     |          ok | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | threshold;  | threshold;  | ok     |          ok | ok      | ok      | threshold;  | ok      | ok      | ok      | ok      | ok      | ok      | ok      | ok      | threshold;  | threshold;  | threshold;  | threshold;  | ok      | ok     |\n",
      "Male      | ok | ok   | ok   | ok   | ok   | ok   | threshold;  | ok   | ok    | ok    | ok    | ok    | ok    | threshold;  |          ok | threshold;  | threshold;  | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | threshold;  | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | ok    | threshold;  | ok    | ok    | ok    | ok    | ok    | ok    | ok    | threshold;  | threshold;  | ok    | ok    | threshold;  | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | threshold;  | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | ok     | ok     | threshold;  | ok     | ok     | ok     | threshold;  | threshold;  | ok     | threshold;  | ok      | ok      | threshold;  | ok      | ok      | ok      | ok      | ok      | ok      | ok      | ok      |          ok | threshold;  | threshold;  | threshold;  | ok      | ok     |\n",
      "-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n",
      "\n",
      "INFO:acro:records:add(): output_1\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Condition  0         28060     75036     78272     79740     80502     \\\n",
      "Gender                                                                  \n",
      "Female         2447       464        77       152        23       165   \n",
      "Male           2415       478        78       148        27       171   \n",
      "\n",
      "Condition  80809     81151     133834    134438    ...  40481087  40486433  \\\n",
      "Gender                                             ...                       \n",
      "Female           11       332        63        15  ...      3488        91   \n",
      "Male              9       385        47        18  ...      3513        70   \n",
      "\n",
      "Condition  43530652  43530656  43530685  44782520  45757499  45763582  \\\n",
      "Gender                                                                  \n",
      "Female          149        45         8         4         3         0   \n",
      "Male            164        49        18         2         3         1   \n",
      "\n",
      "Condition  45769905  45770830  \n",
      "Gender                         \n",
      "Female           23        12  \n",
      "Male             26        20  \n",
      "\n",
      "[2 rows x 133 columns]\n"
     ]
    }
   ],
   "source": [
    "# Pandas (for comparison only)\n",
    "person[\"gender_concept_id\"] = person[\"gender_concept_id\"].replace({8507:\"Male\", 8532:\"Female\"})     \n",
    "\n",
    "\n",
    "pd.crosstab(person['gender_concept_id'], condition['condition_concept_id'])\n",
    "\n",
    "\n",
    "#ACRO Version crosstab\n",
    "safe_table = acro.crosstab(person['gender_concept_id'], condition['condition_concept_id'], rownames=['Gender'], colnames=['Condition'])\n",
    "print(safe_table.tail(5))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "725bbcbd",
   "metadata": {},
   "source": [
    "## Aggregated Output (Means Visits by Gender)\n",
    "- Shows average or total visits per gender safely"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "c7d55bb0",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:get_summary(): pass\n",
      "INFO:acro:outcome_df:\n",
      "----------------------|\n",
      "col_0             |All|\n",
      "gender_concept_id |   |\n",
      "----------------------|\n",
      "Female            | ok|\n",
      "Male              | ok|\n",
      "----------------------|\n",
      "\n",
      "INFO:acro:records:add(): output_1\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "col_0                All\n",
      "gender_concept_id       \n",
      "Female             24826\n",
      "Male               25174\n"
     ]
    }
   ],
   "source": [
    "visit =  pd.read_csv(DATA_DIR/ \"visit_occurrence.csv\")\n",
    "\n",
    "safe_mean = acro.crosstab(index=[person['gender_concept_id']], columns= ['All'], values=visit['visit_occurrence_id'], aggfunc='count')\n",
    "print(safe_mean)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4afeb12c",
   "metadata": {},
   "source": [
    "## ACRO Histogram"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "61ca841c",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:status: fail\n",
      "INFO:acro:records:add(): output_2\n"
     ]
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk0AAAHHCAYAAACiOWx7AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAS8RJREFUeJzt3QeYE+Xa//GbpS+9I1IVpEgHaYLSERBB8BzkICAgCAJSlHaOUo9SlKoUFSm+FooKSgepIr33oiCgSJEqvez8r/v5v5M3yWaXyZJ1N8n3c10hm2QymWcyJL88bZJYlmUJAAAAYhUR+8MAAABQhCYAAAAHCE0AAAAOEJoAAAAcIDQBAAA4QGgCAABwgNAEAADgAKEJAADAAUITAACAA4QmwKFBgwZJkiRJ/pbXql69urnYVq9ebV7766+//lte/+WXX5b8+fNLYnb16lV55ZVXJGfOnGbf9OjRI6E3yewz3XfByvu4c2L69Olm///666/xtl1AYkFoQliyP+jtS6pUqSRXrlxSr149GT9+vPz1118BeZ1Tp06ZsLVz505JbBLztjnx7rvvmvexc+fO8j//8z/SqlWrWMOM+/udPXt2qVatmsydO9fv112/fr3Zb5cuXZKEsGjRIvP693P27FlJliyZvPTSSzEuo8d56tSppWnTpgHdxokTJ5r3xh83b96UMWPGSMWKFSVDhgzm/+Rjjz0mXbt2lcOHD0tikNDvPRIBPfccEG6mTZum51y0hgwZYv3P//yPNXXqVOvdd9+16tatayVJksTKly+ftWvXLo/n3Llzx7px44Zfr7NlyxbzOvp6/rh165a52FatWmXWM2fOHL/WE9dtu337tnXz5k0rMatYsaL15JNPOlpW38/SpUub91ovI0aMsB555BFT/kmTJvn1uu+995553rFjx6I9pvtM91186tKli3l9J5555hkrbdq01rVr13w+Pn36dLOub775xudx58Tdu3fN/4uoqCjXfY8//rj19NNPO17HuXPnrHLlypltefbZZ62xY8daU6ZMsXr37m3lyZPHSp48uZUYxPbeIzwkS+jQBiSk+vXrS/ny5V23+/fvLytXrpRnn31WnnvuOTlw4ID5Ja70V7te4tP169clMjJSUqRIIQkpefLkkthpTUqxYsUcL//www971Lq0bt1aChYsaGo3OnXqFJBtSpkypSQm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AADAAUITAACAA4QmAAAABwhNAAAAiT00rV27Vho1aiS5cuWSJEmSyLx58zwetyxLBgwYIA899JCkTp1aateuLUeOHPFY5sKFC9KyZUtJnz69ZMyYUdq3by9Xr171WGb37t1SrVo1SZUqleTJk0dGjhwZbVvmzJkjRYoUMcuUKFFCFi1aFE+lBgAAwShBQ9O1a9ekVKlSMmHCBJ+Pa7gZP368TJ48WTZt2iRp0qSRevXqyc2bN13LaGDat2+fLF++XBYsWGCCWMeOHV2PX7lyRerWrSv58uWTbdu2yXvvvSeDBg2Sjz/+2LXM+vXrpUWLFiZw7dixQ5o0aWIue/fujec9AAAAgkWyhHzx+vXrm4svWss0duxYeeutt6Rx48bmvs8++0xy5MhhaqRefPFFOXDggCxZskS2bNki5cuXN8t88MEH0qBBA3n//fdNDdYXX3wht2/flqlTp0qKFCnk8ccfl507d8ro0aNd4WrcuHHyzDPPSO/evc3toUOHmhD24YcfmsAGAACQoKEpNseOHZPTp0+bJjlbhgwZpGLFirJhwwYTmvRam+TswKR0+YiICFMz9fzzz5tlnnrqKROYbFpbNWLECLl48aJkypTJLNOrVy+P19dlvJsL3d26dctc3Gu01J07d8wlUOx1pYywJNjEdT/YzwvkfkzsKHN4oMzhIRzLHMzl9md7E21o0sCktGbJnd62H9Pr7NmzezyeLFkyyZw5s8cyBQoUiLYO+zENTXod2+v4MmzYMBk8eHC0+5ctWyaRkZESaEPLR0mwedB+YVrbF24oc3igzOEhHMscjOW+fv168IemxK5///4etVNa06SdzLX/lHZKD2QC1gPw7a0RcisqiQSTvYPqPVCZ69SpI8mTJ5dwQJkpc6iizOFR5mAut91SFNShKWfOnOb6zJkzZvScTW+XLl3atczZs2c9nnf37l0zos5+vl7rc9zZt++3jP24LylTpjQXb3qgxMfBooHp1r3gCk0Puh/ia18mZpQ5PFDm8BCOZQ7GcvuzrYl2niZtUtPQsmLFCo80qH2VKleubG7r9aVLl8yoONvKlSslKirK9H2yl9ERde5tlpqECxcubJrm7GXcX8dexn4dAACABA1NOp+SjmTTi935W/8+ceKEmbepR48e8t///le+//572bNnj7Ru3dqMiNPpAFTRokXNqLcOHTrI5s2b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",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "visit_count = visit.groupby(\"person_id\").size().reset_index(name=\"n_visits\")\n",
    "data = pd.merge(person, visit_count, on=\"person_id\", how=\"left\")\n",
    "\n",
    "data[\"n_visits\"] = data[\"n_visits\"].fillna(0)\n",
    "\n",
    "ax = acro.hist(data, column=\"n_visits\", bins=10)\n",
    "\n",
    "# Add labels\n",
    "plt.xlabel(\"Number of Visits per Patient\")\n",
    "plt.ylabel(\"Number of Patients\")\n",
    "plt.title(\"Distribution of Patient Visit Counts\")\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "379ce0ae",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:records:rename_output(): output_2 renamed to  Histogram of Patient Visit Counts\n"
     ]
    }
   ],
   "source": [
    "acro.rename_output(\"output_2\", \" Histogram of Patient Visit Counts\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f8550352",
   "metadata": {},
   "source": [
    "## ACRO Pivot Table\n",
    "- This example shows how researchers can summarise numeric OMOP data (like number of visits or conditions per patient) using ACRO's safe pivot_table method\n",
    "    - Following example computes average and standard deviation of visits per gender. ACRO confirms if the summary is safe and flags/suppresses risky cells.\n",
    "- What ACRO checks: ouput group sizes and disclosure thresholds for each cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "b0c3bd85",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:get_summary(): pass\n",
      "INFO:acro:outcome_df:\n",
      "------------------------------------|\n",
      "                  mean     |std     |\n",
      "                  n_visits |n_visits|\n",
      "gender_concept_id          |        |\n",
      "------------------------------------|\n",
      "Female             ok      | ok     |\n",
      "Male               ok      | ok     |\n",
      "------------------------------------|\n",
      "\n",
      "INFO:acro:records:add(): output_2\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                        mean        std\n",
      "                    n_visits   n_visits\n",
      "gender_concept_id                      \n",
      "Female             35.175627  23.019888\n",
      "Male               22.051056  21.237114\n"
     ]
    }
   ],
   "source": [
    "\n",
    "visit_count = visit.groupby(\"person_id\").size().reset_index(name=\"n_visits\")\n",
    "data = pd.merge(person, visit_count, on=\"person_id\", how=\"left\")\n",
    "\n",
    "data[\"gender_concept_id\"] = data[\"gender_concept_id\"].replace({8507:\"Male\", 8532:\"Female\"})     \n",
    "\n",
    "table = acro.pivot_table(\n",
    "    data,\n",
    "    index=[\"gender_concept_id\"],\n",
    "    values=[\"n_visits\"],\n",
    "    aggfunc=[\"mean\", \"std\"]\n",
    ")\n",
    "print(table)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9577e033",
   "metadata": {},
   "source": [
    "## ACRO OLS (Ordinary Least Squares Regression)\n",
    "- This performs a regression to check if older individuals tend to have more recorded conditions, with ACRO checking disclosure risk automatically\n",
    "- Question: does year_of_birth (proxy for age) relate to n_conditions?\n",
    "    - What ACRO Checks: degrees of freedom >= threshold, no tiny groups, safe to publish"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "id": "965eef54",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:ols() outcome: pass; dof=1128.0 >= 10\n",
      "INFO:acro:records:add(): output_3\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table class=\"simpletable\">\n",
       "<caption>OLS Regression Results</caption>\n",
       "<tr>\n",
       "  <th>Dep. Variable:</th>       <td>n_condition</td>   <th>  R-squared:         </th> <td>   0.109</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Model:</th>                   <td>OLS</td>       <th>  Adj. R-squared:    </th> <td>   0.108</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Method:</th>             <td>Least Squares</td>  <th>  F-statistic:       </th> <td>   138.0</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Date:</th>             <td>Wed, 22 Oct 2025</td> <th>  Prob (F-statistic):</th> <td>3.77e-30</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Time:</th>                 <td>11:47:40</td>     <th>  Log-Likelihood:    </th> <td> -3109.8</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>No. Observations:</th>      <td>  1130</td>      <th>  AIC:               </th> <td>   6224.</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Df Residuals:</th>          <td>  1128</td>      <th>  BIC:               </th> <td>   6234.</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Df Model:</th>              <td>     1</td>      <th>                     </th>     <td> </td>   \n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Covariance Type:</th>      <td>nonrobust</td>    <th>                     </th>     <td> </td>   \n",
       "</tr>\n",
       "</table>\n",
       "<table class=\"simpletable\">\n",
       "<tr>\n",
       "        <td></td>           <th>coef</th>     <th>std err</th>      <th>t</th>      <th>P>|t|</th>  <th>[0.025</th>    <th>0.975]</th>  \n",
       "</tr>\n",
       "<tr>\n",
       "  <th>const</th>         <td>   76.8871</td> <td>    5.950</td> <td>   12.922</td> <td> 0.000</td> <td>   65.212</td> <td>   88.562</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>year_of_birth</th> <td>   -0.0354</td> <td>    0.003</td> <td>  -11.749</td> <td> 0.000</td> <td>   -0.041</td> <td>   -0.030</td>\n",
       "</tr>\n",
       "</table>\n",
       "<table class=\"simpletable\">\n",
       "<tr>\n",
       "  <th>Omnibus:</th>       <td>204.349</td> <th>  Durbin-Watson:     </th> <td>   1.966</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Prob(Omnibus):</th> <td> 0.000</td>  <th>  Jarque-Bera (JB):  </th> <td>4087.349</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Skew:</th>          <td>-0.144</td>  <th>  Prob(JB):          </th> <td>    0.00</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Kurtosis:</th>      <td>12.313</td>  <th>  Cond. No.          </th> <td>1.04e+05</td>\n",
       "</tr>\n",
       "</table><br/><br/>Notes:<br/>[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.<br/>[2] The condition number is large, 1.04e+05. This might indicate that there are<br/>strong multicollinearity or other numerical problems."
      ],
      "text/latex": [
       "\\begin{center}\n",
       "\\begin{tabular}{lclc}\n",
       "\\toprule\n",
       "\\textbf{Dep. Variable:}    &   n\\_condition   & \\textbf{  R-squared:         } &     0.109   \\\\\n",
       "\\textbf{Model:}            &       OLS        & \\textbf{  Adj. R-squared:    } &     0.108   \\\\\n",
       "\\textbf{Method:}           &  Least Squares   & \\textbf{  F-statistic:       } &     138.0   \\\\\n",
       "\\textbf{Date:}             & Wed, 22 Oct 2025 & \\textbf{  Prob (F-statistic):} &  3.77e-30   \\\\\n",
       "\\textbf{Time:}             &     11:47:40     & \\textbf{  Log-Likelihood:    } &   -3109.8   \\\\\n",
       "\\textbf{No. Observations:} &        1130      & \\textbf{  AIC:               } &     6224.   \\\\\n",
       "\\textbf{Df Residuals:}     &        1128      & \\textbf{  BIC:               } &     6234.   \\\\\n",
       "\\textbf{Df Model:}         &           1      & \\textbf{                     } &             \\\\\n",
       "\\textbf{Covariance Type:}  &    nonrobust     & \\textbf{                     } &             \\\\\n",
       "\\bottomrule\n",
       "\\end{tabular}\n",
       "\\begin{tabular}{lcccccc}\n",
       "                         & \\textbf{coef} & \\textbf{std err} & \\textbf{t} & \\textbf{P$> |$t$|$} & \\textbf{[0.025} & \\textbf{0.975]}  \\\\\n",
       "\\midrule\n",
       "\\textbf{const}           &      76.8871  &        5.950     &    12.922  &         0.000        &       65.212    &       88.562     \\\\\n",
       "\\textbf{year\\_of\\_birth} &      -0.0354  &        0.003     &   -11.749  &         0.000        &       -0.041    &       -0.030     \\\\\n",
       "\\bottomrule\n",
       "\\end{tabular}\n",
       "\\begin{tabular}{lclc}\n",
       "\\textbf{Omnibus:}       & 204.349 & \\textbf{  Durbin-Watson:     } &    1.966  \\\\\n",
       "\\textbf{Prob(Omnibus):} &   0.000 & \\textbf{  Jarque-Bera (JB):  } & 4087.349  \\\\\n",
       "\\textbf{Skew:}          &  -0.144 & \\textbf{  Prob(JB):          } &     0.00  \\\\\n",
       "\\textbf{Kurtosis:}      &  12.313 & \\textbf{  Cond. No.          } & 1.04e+05  \\\\\n",
       "\\bottomrule\n",
       "\\end{tabular}\n",
       "%\\caption{OLS Regression Results}\n",
       "\\end{center}\n",
       "\n",
       "Notes: \\newline\n",
       " [1] Standard Errors assume that the covariance matrix of the errors is correctly specified. \\newline\n",
       " [2] The condition number is large, 1.04e+05. This might indicate that there are \\newline\n",
       " strong multicollinearity or other numerical problems."
      ],
      "text/plain": [
       "<class 'statsmodels.iolib.summary.Summary'>\n",
       "\"\"\"\n",
       "                            OLS Regression Results                            \n",
       "==============================================================================\n",
       "Dep. Variable:            n_condition   R-squared:                       0.109\n",
       "Model:                            OLS   Adj. R-squared:                  0.108\n",
       "Method:                 Least Squares   F-statistic:                     138.0\n",
       "Date:                Wed, 22 Oct 2025   Prob (F-statistic):           3.77e-30\n",
       "Time:                        11:47:40   Log-Likelihood:                -3109.8\n",
       "No. Observations:                1130   AIC:                             6224.\n",
       "Df Residuals:                    1128   BIC:                             6234.\n",
       "Df Model:                           1                                         \n",
       "Covariance Type:            nonrobust                                         \n",
       "=================================================================================\n",
       "                    coef    std err          t      P>|t|      [0.025      0.975]\n",
       "---------------------------------------------------------------------------------\n",
       "const            76.8871      5.950     12.922      0.000      65.212      88.562\n",
       "year_of_birth    -0.0354      0.003    -11.749      0.000      -0.041      -0.030\n",
       "==============================================================================\n",
       "Omnibus:                      204.349   Durbin-Watson:                   1.966\n",
       "Prob(Omnibus):                  0.000   Jarque-Bera (JB):             4087.349\n",
       "Skew:                          -0.144   Prob(JB):                         0.00\n",
       "Kurtosis:                      12.313   Cond. No.                     1.04e+05\n",
       "==============================================================================\n",
       "\n",
       "Notes:\n",
       "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n",
       "[2] The condition number is large, 1.04e+05. This might indicate that there are\n",
       "strong multicollinearity or other numerical problems.\n",
       "\"\"\""
      ]
     },
     "execution_count": 44,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from statsmodels.api import add_constant\n",
    "\n",
    "condition_count = condition.groupby(\"person_id\").size().reset_index(name=\"n_condition\")\n",
    "\n",
    "df = pd.merge(person, condition_count, on=\"person_id\", how=\"left\")\n",
    "\n",
    "df[\"n_condition\"] = df[\"n_condition\"].fillna(0).astype(float)\n",
    "df[\"year_of_birth\"] = pd.to_numeric(df[\"year_of_birth\"], errors=\"coerce\")\n",
    "df = df.dropna(subset=[\"n_condition\", \"year_of_birth\"])\n",
    "\n",
    "\n",
    "y = df[\"n_condition\"]\n",
    "x = df[\"year_of_birth\"]\n",
    "x = add_constant(x)\n",
    "\n",
    "results = acro.ols(y, x)\n",
    "results.summary()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "8aa3a27d",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:olsr() outcome: pass; dof=1128.0 >= 10\n",
      "INFO:acro:records:add(): output_4\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                            OLS Regression Results                            \n",
      "==============================================================================\n",
      "Dep. Variable:            n_condition   R-squared:                       0.109\n",
      "Model:                            OLS   Adj. R-squared:                  0.108\n",
      "Method:                 Least Squares   F-statistic:                     138.0\n",
      "Date:                Wed, 22 Oct 2025   Prob (F-statistic):           3.77e-30\n",
      "Time:                        11:47:41   Log-Likelihood:                -3109.8\n",
      "No. Observations:                1130   AIC:                             6224.\n",
      "Df Residuals:                    1128   BIC:                             6234.\n",
      "Df Model:                           1                                         \n",
      "Covariance Type:            nonrobust                                         \n",
      "=================================================================================\n",
      "                    coef    std err          t      P>|t|      [0.025      0.975]\n",
      "---------------------------------------------------------------------------------\n",
      "Intercept        76.8871      5.950     12.922      0.000      65.212      88.562\n",
      "year_of_birth    -0.0354      0.003    -11.749      0.000      -0.041      -0.030\n",
      "==============================================================================\n",
      "Omnibus:                      204.349   Durbin-Watson:                   1.966\n",
      "Prob(Omnibus):                  0.000   Jarque-Bera (JB):             4087.349\n",
      "Skew:                          -0.144   Prob(JB):                         0.00\n",
      "Kurtosis:                      12.313   Cond. No.                     1.04e+05\n",
      "==============================================================================\n",
      "\n",
      "Notes:\n",
      "[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n",
      "[2] The condition number is large, 1.04e+05. This might indicate that there are\n",
      "strong multicollinearity or other numerical problems.\n"
     ]
    }
   ],
   "source": [
    "#ACRO OLSR (R -style Formula Syntax)\n",
    "results = acro.olsr(formula=\"n_condition ~ year_of_birth\", data=df)\n",
    "print(results.summary())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "883be163",
   "metadata": {},
   "source": [
    "## Probit - binary outcome demo\n",
    "- Derive a simple binary label directly from OMOP conditions (example diabetes concept IDs shown, replace with your study concepts)\n",
    "    - What ACRO Checks: model size/DoF, safe coefficient tables, no tiny strata implied."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "9fd725ac",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:probit() outcome: pass; dof=1128.0 >= 10\n",
      "INFO:acro:records:add(): output_5\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Optimization terminated successfully.\n",
      "         Current function value: 0.224946\n",
      "         Iterations 6\n",
      "                          Probit Regression Results                           \n",
      "==============================================================================\n",
      "Dep. Variable:           has_diabetes   No. Observations:                 1130\n",
      "Model:                         Probit   Df Residuals:                     1128\n",
      "Method:                           MLE   Df Model:                            1\n",
      "Date:                Wed, 22 Oct 2025   Pseudo R-squ.:                 0.02146\n",
      "Time:                        11:47:41   Log-Likelihood:                -254.19\n",
      "converged:                       True   LL-Null:                       -259.76\n",
      "Covariance Type:            nonrobust   LLR p-value:                 0.0008401\n",
      "=================================================================================\n",
      "                    coef    std err          z      P>|z|      [0.025      0.975]\n",
      "---------------------------------------------------------------------------------\n",
      "const             6.0009      2.118      2.834      0.005       1.850      10.151\n",
      "year_of_birth    -0.0038      0.001     -3.561      0.000      -0.006      -0.002\n",
      "=================================================================================\n"
     ]
    }
   ],
   "source": [
    "\n",
    "diabetes_codes = [201826, 201254, 44054006]\n",
    "\n",
    "has_diabetes = condition[condition[\"condition_concept_id\"].isin(diabetes_codes)]\n",
    "has_diabetes = has_diabetes.groupby(\"person_id\").size().reset_index(name=\"count\")\n",
    "has_diabetes[\"has_diabetes\"] = 1\n",
    "\n",
    "df = pd.merge(person, has_diabetes[[\"person_id\", \"has_diabetes\"]], on=\"person_id\", how=\"left\")\n",
    "df[\"has_diabetes\"] = df[\"has_diabetes\"].fillna(0)\n",
    "\n",
    "x = add_constant(df[\"year_of_birth\"])\n",
    "y = df[\"has_diabetes\"]\n",
    "\n",
    "results = acro.probit(y, x)\n",
    "print(results.summary())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "d016d0d8",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:logit() outcome: pass; dof=1128.0 >= 10\n",
      "INFO:acro:records:add(): output_6\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Optimization terminated successfully.\n",
      "         Current function value: 0.225864\n",
      "         Iterations 8\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<table class=\"simpletable\">\n",
       "<caption>Logit Regression Results</caption>\n",
       "<tr>\n",
       "  <th>Dep. Variable:</th>     <td>has_diabetes</td>   <th>  No. Observations:  </th>  <td>  1130</td> \n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Model:</th>                 <td>Logit</td>      <th>  Df Residuals:      </th>  <td>  1128</td> \n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Method:</th>                 <td>MLE</td>       <th>  Df Model:          </th>  <td>     1</td> \n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Date:</th>            <td>Wed, 22 Oct 2025</td> <th>  Pseudo R-squ.:     </th>  <td>0.01747</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Time:</th>                <td>11:47:41</td>     <th>  Log-Likelihood:    </th> <td> -255.23</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>converged:</th>             <td>True</td>       <th>  LL-Null:           </th> <td> -259.76</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>Covariance Type:</th>     <td>nonrobust</td>    <th>  LLR p-value:       </th> <td>0.002591</td>\n",
       "</tr>\n",
       "</table>\n",
       "<table class=\"simpletable\">\n",
       "<tr>\n",
       "        <td></td>           <th>coef</th>     <th>std err</th>      <th>z</th>      <th>P>|z|</th>  <th>[0.025</th>    <th>0.975]</th>  \n",
       "</tr>\n",
       "<tr>\n",
       "  <th>const</th>         <td>   14.2757</td> <td>    8.361</td> <td>    1.707</td> <td> 0.088</td> <td>   -2.111</td> <td>   30.663</td>\n",
       "</tr>\n",
       "<tr>\n",
       "  <th>year_of_birth</th> <td>   -0.0086</td> <td>    0.004</td> <td>   -2.033</td> <td> 0.042</td> <td>   -0.017</td> <td>   -0.000</td>\n",
       "</tr>\n",
       "</table>"
      ],
      "text/latex": [
       "\\begin{center}\n",
       "\\begin{tabular}{lclc}\n",
       "\\toprule\n",
       "\\textbf{Dep. Variable:}   &  has\\_diabetes   & \\textbf{  No. Observations:  } &     1130    \\\\\n",
       "\\textbf{Model:}           &      Logit       & \\textbf{  Df Residuals:      } &     1128    \\\\\n",
       "\\textbf{Method:}          &       MLE        & \\textbf{  Df Model:          } &        1    \\\\\n",
       "\\textbf{Date:}            & Wed, 22 Oct 2025 & \\textbf{  Pseudo R-squ.:     } &  0.01747    \\\\\n",
       "\\textbf{Time:}            &     11:47:41     & \\textbf{  Log-Likelihood:    } &   -255.23   \\\\\n",
       "\\textbf{converged:}       &       True       & \\textbf{  LL-Null:           } &   -259.76   \\\\\n",
       "\\textbf{Covariance Type:} &    nonrobust     & \\textbf{  LLR p-value:       } &  0.002591   \\\\\n",
       "\\bottomrule\n",
       "\\end{tabular}\n",
       "\\begin{tabular}{lcccccc}\n",
       "                         & \\textbf{coef} & \\textbf{std err} & \\textbf{z} & \\textbf{P$> |$z$|$} & \\textbf{[0.025} & \\textbf{0.975]}  \\\\\n",
       "\\midrule\n",
       "\\textbf{const}           &      14.2757  &        8.361     &     1.707  &         0.088        &       -2.111    &       30.663     \\\\\n",
       "\\textbf{year\\_of\\_birth} &      -0.0086  &        0.004     &    -2.033  &         0.042        &       -0.017    &       -0.000     \\\\\n",
       "\\bottomrule\n",
       "\\end{tabular}\n",
       "%\\caption{Logit Regression Results}\n",
       "\\end{center}"
      ],
      "text/plain": [
       "<class 'statsmodels.iolib.summary.Summary'>\n",
       "\"\"\"\n",
       "                           Logit Regression Results                           \n",
       "==============================================================================\n",
       "Dep. Variable:           has_diabetes   No. Observations:                 1130\n",
       "Model:                          Logit   Df Residuals:                     1128\n",
       "Method:                           MLE   Df Model:                            1\n",
       "Date:                Wed, 22 Oct 2025   Pseudo R-squ.:                 0.01747\n",
       "Time:                        11:47:41   Log-Likelihood:                -255.23\n",
       "converged:                       True   LL-Null:                       -259.76\n",
       "Covariance Type:            nonrobust   LLR p-value:                  0.002591\n",
       "=================================================================================\n",
       "                    coef    std err          z      P>|z|      [0.025      0.975]\n",
       "---------------------------------------------------------------------------------\n",
       "const            14.2757      8.361      1.707      0.088      -2.111      30.663\n",
       "year_of_birth    -0.0086      0.004     -2.033      0.042      -0.017      -0.000\n",
       "=================================================================================\n",
       "\"\"\""
      ]
     },
     "execution_count": 47,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "results = acro.logit(y, x)\n",
    "results.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cdd0adc2",
   "metadata": {},
   "source": [
    "## List current ACRO outputs\n",
    "- This will provide the list of all the outputs saved till now\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "6ae413db",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "uid: output_0\n",
      "status: fail\n",
      "type: table\n",
      "properties: {'method': 'crosstab'}\n",
      "sdc: {'summary': {'suppressed': False, 'negative': 0, 'missing': 0, 'threshold': 48, 'p-ratio': 0, 'nk-rule': 0, 'all-values-are-same': 0}, 'cells': {'negative': [], 'missing': [], 'threshold': [[0, 13], [0, 14], [0, 15], [0, 16], [0, 27], [0, 38], [0, 46], [0, 47], [0, 50], [0, 56], [0, 61], [0, 68], [0, 75], [0, 78], [0, 102], [0, 108], [0, 112], [0, 113], [0, 118], [0, 127], [0, 128], [0, 129], [0, 130], [1, 6], [1, 13], [1, 15], [1, 16], [1, 27], [1, 38], [1, 46], [1, 47], [1, 50], [1, 56], [1, 61], [1, 68], [1, 75], [1, 78], [1, 81], [1, 98], [1, 102], [1, 108], [1, 112], [1, 113], [1, 115], [1, 118], [1, 128], [1, 129], [1, 130]], 'p-ratio': [], 'nk-rule': [], 'all-values-are-same': []}}\n",
      "command: safe_table = acro.crosstab(person['gender_concept_id'], condition['condition_concept_id'], rownames=['Gender'], colnames=['Condition'])\n",
      "summary: fail; threshold: 48 cells may need suppressing; \n",
      "outcome: Condition   0 28060 75036 78272 79740 80502        80809 81151 133834 134438  \\\n",
      "Gender                                                                         \n",
      "Female     ok    ok    ok    ok    ok    ok           ok    ok     ok     ok   \n",
      "Male       ok    ok    ok    ok    ok    ok  threshold;     ok     ok     ok   \n",
      "\n",
      "Condition  ... 40481087 40486433 43530652 43530656     43530685     44782520  \\\n",
      "Gender     ...                                                                 \n",
      "Female     ...       ok       ok       ok       ok  threshold;   threshold;    \n",
      "Male       ...       ok       ok       ok       ok           ok  threshold;    \n",
      "\n",
      "Condition     45757499     45763582 45769905 45770830  \n",
      "Gender                                                 \n",
      "Female     threshold;   threshold;        ok       ok  \n",
      "Male       threshold;   threshold;        ok       ok  \n",
      "\n",
      "[2 rows x 133 columns]\n",
      "output: [Condition  0         28060     75036     78272     79740     80502     \\\n",
      "Gender                                                                  \n",
      "Female         2447       464        77       152        23       165   \n",
      "Male           2415       478        78       148        27       171   \n",
      "\n",
      "Condition  80809     81151     133834    134438    ...  40481087  40486433  \\\n",
      "Gender                                             ...                       \n",
      "Female           11       332        63        15  ...      3488        91   \n",
      "Male              9       385        47        18  ...      3513        70   \n",
      "\n",
      "Condition  43530652  43530656  43530685  44782520  45757499  45763582  \\\n",
      "Gender                                                                  \n",
      "Female          149        45         8         4         3         0   \n",
      "Male            164        49        18         2         3         1   \n",
      "\n",
      "Condition  45769905  45770830  \n",
      "Gender                         \n",
      "Female           23        12  \n",
      "Male             26        20  \n",
      "\n",
      "[2 rows x 133 columns]]\n",
      "timestamp: 2026-03-12T12:26:12.323461\n",
      "comments: []\n",
      "exception: \n",
      "\n",
      "uid: output_1\n",
      "status: fail\n",
      "type: table\n",
      "properties: {'method': 'crosstab'}\n",
      "sdc: {'summary': {'suppressed': False, 'negative': 0, 'missing': 0, 'threshold': 48, 'p-ratio': 0, 'nk-rule': 0, 'all-values-are-same': 0}, 'cells': {'negative': [], 'missing': [], 'threshold': [[0, 13], [0, 14], [0, 15], [0, 16], [0, 27], [0, 38], [0, 46], [0, 47], [0, 50], [0, 56], [0, 61], [0, 68], [0, 75], [0, 78], [0, 102], [0, 108], [0, 112], [0, 113], [0, 118], [0, 127], [0, 128], [0, 129], [0, 130], [1, 6], [1, 13], [1, 15], [1, 16], [1, 27], [1, 38], [1, 46], [1, 47], [1, 50], [1, 56], [1, 61], [1, 68], [1, 75], [1, 78], [1, 81], [1, 98], [1, 102], [1, 108], [1, 112], [1, 113], [1, 115], [1, 118], [1, 128], [1, 129], [1, 130]], 'p-ratio': [], 'nk-rule': [], 'all-values-are-same': []}}\n",
      "command: safe_table = acro.crosstab(person['gender_concept_id'], condition['condition_concept_id'], rownames=['Gender'], colnames=['Condition'])\n",
      "summary: fail; threshold: 48 cells may need suppressing; \n",
      "outcome: Condition   0 28060 75036 78272 79740 80502        80809 81151 133834 134438  \\\n",
      "Gender                                                                         \n",
      "Female     ok    ok    ok    ok    ok    ok           ok    ok     ok     ok   \n",
      "Male       ok    ok    ok    ok    ok    ok  threshold;     ok     ok     ok   \n",
      "\n",
      "Condition  ... 40481087 40486433 43530652 43530656     43530685     44782520  \\\n",
      "Gender     ...                                                                 \n",
      "Female     ...       ok       ok       ok       ok  threshold;   threshold;    \n",
      "Male       ...       ok       ok       ok       ok           ok  threshold;    \n",
      "\n",
      "Condition     45757499     45763582 45769905 45770830  \n",
      "Gender                                                 \n",
      "Female     threshold;   threshold;        ok       ok  \n",
      "Male       threshold;   threshold;        ok       ok  \n",
      "\n",
      "[2 rows x 133 columns]\n",
      "output: [Condition  0         28060     75036     78272     79740     80502     \\\n",
      "Gender                                                                  \n",
      "Female         2447       464        77       152        23       165   \n",
      "Male           2415       478        78       148        27       171   \n",
      "\n",
      "Condition  80809     81151     133834    134438    ...  40481087  40486433  \\\n",
      "Gender                                             ...                       \n",
      "Female           11       332        63        15  ...      3488        91   \n",
      "Male              9       385        47        18  ...      3513        70   \n",
      "\n",
      "Condition  43530652  43530656  43530685  44782520  45757499  45763582  \\\n",
      "Gender                                                                  \n",
      "Female          149        45         8         4         3         0   \n",
      "Male            164        49        18         2         3         1   \n",
      "\n",
      "Condition  45769905  45770830  \n",
      "Gender                         \n",
      "Female           23        12  \n",
      "Male             26        20  \n",
      "\n",
      "[2 rows x 133 columns]]\n",
      "timestamp: 2026-03-12T12:26:19.992362\n",
      "comments: []\n",
      "exception: \n",
      "\n",
      "uid:  Histogram of Patient Visit Counts\n",
      "status: fail\n",
      "type: histogram\n",
      "properties: {'method': 'histogram'}\n",
      "sdc: {}\n",
      "command: ax = acro.hist(data, column=\"n_visits\", bins=10)\n",
      "summary: Please check the minimum and the maximum values. The minimum value of the n_visits column is: 1. The maximum value of the n_visits column is: 1479\n",
      "outcome: Empty DataFrame\n",
      "Columns: []\n",
      "Index: []\n",
      "output: ['acro_artifacts\\\\histogram_2.png']\n",
      "timestamp: 2026-03-12T17:24:01.399689\n",
      "comments: []\n",
      "exception: \n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "results_str = acro.print_outputs()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "900802a6",
   "metadata": {},
   "source": [
    "## Remove some ACRO outputs before finalising\n",
    "\n",
    "You can print, remove and rename outputs, add exceptions, and make comments amongst other functionalities. As this example’s “output_1” failed SDC checks, this can be removed by running acro.remove_output(\"output_1\")."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "id": "4dd42c6c",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:records:remove(): output_1 removed\n",
      "INFO:acro:records:remove(): output_4 removed\n"
     ]
    }
   ],
   "source": [
    "acro.remove_output(\"output_1\")\n",
    "acro.remove_output(\"output_4\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3850e37d",
   "metadata": {},
   "source": [
    "## Request an exception for some of the outputs\n",
    "\n",
    "Using this, you can inform output checkers of any exceptions you want for a certain type of output."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6fe872a9",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:acro:records:exception request was added to output_0\n",
      "INFO:acro:records:exception request was added to output_3\n",
      "INFO:acro:records:exception request was added to output_5\n",
      "INFO:acro:records:exception request was added to output_6\n"
     ]
    }
   ],
   "source": [
    "acro.add_exception(\"output_0\", \"This output includes a small cell (c<10) because the subgroup is clinically essential for out analysis. The cohort is inherently small (rare disease) and further suppression would make the output useless. Please consider an exception as this does not increase re-identification risk in our study.\")\n",
    "acro.add_exception(\"output_3\", \"This table presents stratified results by age and treatment category. Some strate fall below the usual threshold. but the stratification is required to the study objective and was pre-specified in the protocol.\")\n",
    "acro.add_exception(\"output_5\", \"This visualisation contains a few poits representing rare outcome categories, while the counts are low, removing them would disrupt the overall trend and mislead interpretation.The output has been review to ensure no direct identfiers are present.\")\n",
    "acro.add_exception(\"output_6\", \"The regression includes a few small cells due to the inclusion of rare covariates. Although this may imply small sizes, they cannot link back to individuals. The model has been checked to ensure no overfitting or identifiability issues arise from these small cells.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "631a9305",
   "metadata": {},
   "source": [
    "## Finalise ACRO\n",
    "\n",
    "If the acro session is not finalised and work is unsaved, your outputs will not be accessible to checkers. You must use the acro.finalise function, where:\n",
    "\n",
    "-   The first argument specifies the file path. This is the folder where each registered output will be saved (as a CSV) and its SDC summary will be saved (as either a json or xlsx);\n",
    "\n",
    "-   The second argument is either “json” or “Excel”. This determines the file format for each output’s SDC summary.\n",
    "\n",
    "If potentially disclosive data is found in your outputs, executing this function may prompt you to explain why an exception should be granted."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "8dc02727",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING:acro:Results file can not be created. Directory ACRO_RES already exists. Please choose a different directory name.\n"
     ]
    }
   ],
   "source": [
    "SAVE_PATH = \"ACRO_RES\"\n",
    "\n",
    "# output = acro.finalise(SAVE_PATH, \"xlsx\")\n",
    "output = acro.finalise(SAVE_PATH, \"json\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b4c8c06",
   "metadata": {},
   "source": [
    "## List files generated\n",
    "\n",
    "- List all the files generated, at the time of egress you have to upload all the files along with results.json and checksum file. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "38555b1a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "output_0_0.csv\n",
      "output_2_0.csv\n",
      "output_3_0.csv\n",
      "output_3_1.csv\n",
      "output_3_2.csv\n",
      "output_5_0.csv\n",
      "output_5_1.csv\n",
      "output_6_0.csv\n",
      "output_6_1.csv\n",
      "output_7_0.csv\n",
      "output_7_1.csv\n",
      "results.json\n"
     ]
    }
   ],
   "source": [
    "files = []\n",
    "for name in os.listdir(SAVE_PATH):\n",
    "    if os.path.isfile(os.path.join(SAVE_PATH, name)):\n",
    "        files.append(name)\n",
    "files.sort()\n",
    "for f in files:\n",
    "    print(f)"
   ]
  }
 ],
 "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.13.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
