Using SACRO for SDC
Overview
Every research output that leaves a Trusted Research Environment has to be checked for disclosure risk. In most TREs this is done manually by trained output checkers, and it is one of the slowest parts of the research lifecycle.
SACRO (Semi-Automated Checking of Research Outputs) is an open source toolkit that moves part of that check earlier, to the point at which the output is produced. The researcher-facing component is a Python package called ACRO, with front-end packages for R and Stata. Instead of calling pandas.crosstab() or the R equivalent directly, you call the ACRO version. The result is the same table, but ACRO also runs statistical disclosure control (SDC) checks on it, records the outcome, and applies suppression or rounding if the site configuration requires it.
At the end of a session you call a finalise function. This writes a folder containing every requested output, the checks that were run against it, the reason for any suppression, and any exceptions you have requested. That folder is what the output checker reviews.
SACRO does not approve anything. It produces evidence. The decision to release remains with the human checker and with the workspace airlock.
This guide covers installing and using ACRO inside a Workspace in both Python and R.
How ACRO works
An ACRO session follows four steps.
- Initialise a session. This loads the site configuration, which sets the disclosure thresholds.
- Run your analysis through ACRO functions. Each supported analysis is checked as it runs and given a status of pass, review or fail, together with a plain-English explanation.
- Manage your outputs. Rename them so a checker can follow them, add comments explaining what each one is, and request exceptions for anything flagged as review or fail that you still need released.
- Finalise. ACRO writes the release folder. Any output with a fail or review status that does not already have an exception will prompt you for one.
What is checked
ACRO implements principles-based SDC rather than a fixed rulebook. For tables it checks the number of contributors to each cell against a frequency threshold, and it checks for dominance using the N-K rule (whether the largest N contributors account for more than K per cent of a cell) and the p-percent rule. For regression models it checks that residual degrees of freedom exceed a threshold. Negative and missing values are flagged for human attention.
Mitigation
Two mitigation strategies are available.
Suppression replaces unsafe cells with NaN. Where margins have been requested, they are recalculated after suppression so that the totals do not leak the suppressed values.
Rounding rounds all cell values to the nearest multiple of a configurable base, with marginal totals recomputed from the rounded inner cells.
Suppression is enabled per session by the researcher. The thresholds themselves are not researcher-controlled: they belong to the TRE's risk appetite and are set in a YAML configuration file.
Configuration
The site configuration is a small YAML file. The defaults are as follows.
safe_threshold: 10 # minimum contributors per table cell
safe_dof_threshold: 10 # minimum residual degrees of freedom
safe_nk_n: 2 # N in the N-K dominance rule
safe_nk_k: 0.9 # K in the N-K dominance rule
safe_p_percent: 0.1 # p-percent rule threshold
check_missing_values: true
survival_safe_threshold: 10
zeros_are_disclosive: true
Agree the values with whoever owns output policy for your project before you start, put the file somewhere stable in the workspace, and use the same file for every analysis in the project. If you do not supply one, ACRO uses the defaults above.
Installing ACRO in Python
ACRO requires Python 3.10 or higher. It installs pandas, statsmodels, tabulate and PyYAML as dependencies.
From a terminal in your JupyterLab app:
pip install acro
Then check the install:
python -c "import acro; print(acro.__version__)"
Workspace internet access is restricted, so if PyPI is not reachable from your tenant you will need to download the wheel and its dependencies on your local machine and bring them in through the inbound airlock, then install from the local files:
pip install --no-index --find-links /home/workspace/files/acro_wheels acro
Speak to your workspace administrator if you are not sure which applies to your environment.
Installing ACRO in R
The R package is on CRAN and can be installed from the R console, a Built-in App or a Web App:
install.packages("acro")
Packages installed this way go to a persistent location under files/R/<R_version>, so you only need to install once per R version.
Nothing else is required. Load the library and initialise a session:
library(acro)
acro_init(suppress = TRUE)
Behind the scenes the R package is a front end: the checking itself runs in the Python ACRO package, reached through reticulate, which is pre-installed in Workspaces. The R package handles the Python environment for you, so you do not need to configure reticulate yourself.
Note that acro_init() defaults to no suppression. Pass suppress = TRUE for anything you intend to release, or call acro_enable_suppression() later in the session.
Trying it without touching real data
Confirm the installation end to end before you point ACRO at anything sensitive. The most reliable smoke test builds its own data in memory, so it depends on nothing but the package code:
library(acro)
acro_init(suppress = TRUE)
set.seed(42)
n <- 500
test_df <- data.frame(
region = sample(c("North", "South", "East", "West"), n, replace = TRUE),
band = sample(c("Low", "Medium", "High"), n, replace = TRUE),
stringsAsFactors = TRUE
)
acro_crosstab(index = test_df[, c("region")],
columns = test_df[, c("band")])
acro_finalise("/home/workspace/files/outputs/acro_smoke_test", "json")
If that produces a folder containing a table and a results file, the installation is working.
The package also bundles two datasets, nursery_data and lung, which appear in the published ACRO examples and are equally usable here.
Worked example in Python
The example below uses a synthetic admissions extract held in the workspace file store.
Set up the session
import pandas as pd
import statsmodels.api as sm
import acro
df = pd.read_csv("/home/workspace/files/data/admissions.csv")
session = acro.ACRO(
config="/home/workspace/files/config/acro_config.yaml",
suppress=True,
)
suppress=True removes unsafe cells automatically. Use suppress=False if you would rather see the full table and the warnings and decide yourself, but remember that anything you then want to release will need an exception.
Frequency and aggregate tables
# Counts by region and deprivation quintile
counts = session.crosstab(df.region, df.imd_quintile)
print(counts)
# Mean length of stay by region and admission type
mean_los = session.pivot_table(
df,
index=["region"],
columns=["admission_type"],
values="length_of_stay",
aggfunc="mean",
)
print(mean_los)
ACRO prints the outcome of the checks alongside the table. A typical message reports which cells failed which test, for example that four cells fall below the frequency threshold, or that a cell fails the dominance test because two contributors account for more than 90 per cent of the total.
Regression
y = df["length_of_stay"]
x = sm.add_constant(df[["age", "comorbidity_count"]])
model = session.ols(y, x)
print(model.summary())
Logistic and probit models are available as session.logit() and session.probit().
Review, annotate and finalise
session.print_outputs()
session.rename_output("output_0", "table1_admissions_by_region")
session.add_comments(
"table1_admissions_by_region",
"Descriptive table for paper, Table 1.",
)
session.add_exception(
"table1_admissions_by_region",
"Suppressed cells relate to small rural regions. "
"Aggregated counts are needed for the published table and "
"cannot be recovered from any other released output.",
)
session.finalise("/home/workspace/files/outputs/acro_release_2026_09")
Write the release folder somewhere under /home/workspace/files/ so that it appears in the Files tab and can be submitted through the airlock.
Worked example in R
The R interface mirrors the Python one, with function names prefixed acro_.
library(acro)
df <- read.csv("/home/workspace/files/data/admissions.csv")
acro_init(suppress = TRUE)
Tables
# Cross tabulation
counts <- acro_crosstab(index = df[, c("region")],
columns = df[, c("imd_quintile")])
print(counts)
# Pivot table with an aggregation function
mean_los <- acro_pivot_table(
df,
index = c("region"),
columns = c("admission_type"),
values = c("length_of_stay"),
aggfunc = list("mean")
)
print(mean_los)
acro_crosstab() takes index and columns arguments and expects column subsets rather than bare vectors. Take care to draw both from the same data frame: nothing stops you passing columns from two different frames, and neither R nor ACRO will warn you, but the resulting table will be meaningless.
R factors and NA handling differ from pandas categoricals, so the package provides create_factors() and to_pandas_categorical() for converting columns before tabulation. Use them if you see unexpected empty categories.
Models and plots
fit <- acro_lm(formula = "length_of_stay ~ age + comorbidity_count", data = df)
print(fit)
# Survival analysis, either as a table or a plot
acro_surv_func(time = df$follow_up_days, status = df$died, output = "plot")
acro_glm() fits logit and probit models. Argument names for the model functions have changed between versions, so check ?acro_lm and ?acro_glm against the version you have installed rather than copying signatures from older examples.
Review, annotate and finalise
acro_print_outputs()
acro_rename_output("output_0", "table1_admissions_by_region")
acro_add_comments("table1_admissions_by_region",
"Descriptive table for paper, Table 1.")
acro_add_exception("table1_admissions_by_region",
paste("Suppressed cells relate to small rural regions.",
"Aggregated counts are needed for the published table."))
acro_finalise("/home/workspace/files/outputs/acro_release_2026_09", "json")
acro_finalise() takes the destination folder and the format of the results file, either "json" or "xlsx". A relative folder name is written relative to the working directory, which in a Built-in App is not always where you expect, so give an absolute path under /home/workspace/files/.
Suppression can also be switched on and off during a session with acro_enable_suppression() and acro_disable_suppression().
Outputs that ACRO does not produce
Not everything you want to release will have come out of an ACRO function. A plot saved with ggsave(), a model object, or a CSV written by your own code has never been through a check.
Add these to the session explicitly so they appear in the release folder and the checker knows they exist:
session.add_custom_output(
"/home/workspace/files/outputs/survival_curve.png",
"Kaplan-Meier curve for the full cohort, no strata below 30 patients.",
)
acro_custom_output("/home/workspace/files/outputs/survival_curve.png",
"Kaplan-Meier curve for the full cohort.")
Custom outputs are flagged for the attention of a human checker, because ACRO cannot assess them.
What finalise produces
The release folder contains everything the output checker needs to make a decision:
- the requested outputs, tables written as CSV files
- a results file listing every output, the checks applied to it, its status, the reason for any suppression, and your comments and exceptions
- any custom outputs you added
The results file can be written as JSON for downstream tooling or as a spreadsheet for a human reader, depending on the format you pass to finalise. The JSON form is the one to choose if anything else in your workflow needs to consume the check results. For each output it records:
- the name of the output, both the one ACRO assigned and any name you set with
rename_output - the paths of the files that make up that output
- the type of analysis and the parameters used to produce it
- the status: pass, review or fail
- a summary of which checks were applied and which cells or terms failed them
- your comments and any exception you requested
The folder is also the input format for SACRO-Viewer, the checker-facing application from the same project, which displays each output alongside its check results and tracks the decision made.
Once the folder is complete, submit it through the Workspace airlock in the normal way.
Good practice
- Agree the configuration file with your project's output policy owner before you start analysing.
- Run your analysis on synthetic or dummy data first to confirm that the pipeline works. The bundled
nursery_dataandlungdatasets are enough to check that the installation and the release folder work end to end. - Use
suppress=Trueor enable rounding for anything you intend to release. NeitherACRO()in Python noracro_init()in R suppresses by default. - Rename outputs as you create them.
output_0throughoutput_47is not a helpful release folder. - Write the exception text while you still remember why you needed the output.
- Review every review and fail status before you finalise, rather than clicking through the prompts at the end.
- Keep the release folder separate from your working directory so that the airlock request contains only what you intend.
Limitations
ACRO is a well-scoped tool and it is worth being clear about what it does not do.
- It only sees what goes through it. Anything produced outside an ACRO call is unchecked unless you add it as a custom output.
- It does not check for differencing between tables. Because ACRO assesses each output at the moment it is produced, it cannot easily compare a table against others released earlier or elsewhere. Secondary disclosure by subtraction across outputs remains a matter for the checker.
- It does not cover machine learning models. Disclosure risk in trained models is handled by a separate tool from the same project, SACRO-ML, which runs attacks against a model and produces a risk report.
- It does not check the files themselves. The release folder is assessed by ACRO on the content of the analyses. Whether a file also carries embedded metadata, free text, or a stray identifier column is a different question.
Using SACRO with apps
A separate "SACRO Viewer" app is available. It allows reviewers to view the SACRO JSON output in a human readable way and record acceptance or rejection of the items highlighted. The SACRO Viewer is an open source tool available at https://github.com/AI-SDC/SACRO-Viewer and is also available as an app extension to a DRE Workspace.
Another example is the AIRAlock community app. You can read more about that here: https://www.aridhia.com/blog/ai/enhancing-airalock-with-acro-automated-checking-of-research-outputs-support-and-why-it-matters-where-the-ai-runs/
Community apps are helper applications that run inside a workspace alongside the platform's productised features. They can be deployed on request and tuned for a specific project's needs, but they do not carry the same support commitments as productised features. Ask your Aridhia contact if you would like to discuss deploying one.
Summary
ACRO lets researchers apply statistical disclosure control at the point of analysis rather than discovering problems at the airlock. Install it from PyPI for Python or CRAN for R, initialise a session against your project's configuration file, run tabulations and models through the ACRO wrappers, annotate and finalise, then submit the release folder through the airlock. The checker receives the outputs together with a full record of what was checked, what was suppressed and why, which makes their decision faster and easier to defend later.