Skip to main content

Data Quality Check Framework for Clinical and Analytical Data

Project description

pyCoreGage

version python license tests

Data Quality Check Framework for Clinical and Analytical Data

pyCoreGage is a configuration-driven Python package for running domain-level data quality checks and consolidating findings into structured Excel reports with role-based feedback routing. It is the Python port of the R package rCoreGage.


Table of Contents

  1. Why pyCoreGage
  2. Architecture — Two Layers
  3. Installation
  4. Quick Start
  5. Project Structure
  6. rule_registry.xlsx — Check Definitions
  7. How It Works
  8. Writing Check Scripts
  9. API Reference
  10. Console Output Reference
  11. Publishing to PyPI

1. Why pyCoreGage

Clinical data quality checking typically involves:

  • Running the same checks across dozens of domains (AE, LB, CM, VS …)
  • Separating trial-specific rules from study-wide rules
  • Routing findings to different roles (DM, MW, SDTM, ADaM) and tracking responses
  • Carrying reviewer notes forward across repeated runs without losing history
Problem pyCoreGage solution
Engine scattered across trials Engine installed once via pip install pyCoreGage
Hard-coded paths Single project_config.py per project
No role separation in reports Four separate report channels (DM / MW / SDTM / ADAM)
Feedback lost between runs Structured feedback folders merged on every re-run
R-only tooling Pure Python — works anywhere Python runs

2. Architecture — Two Layers

┌──────────────────────────────────────────────────────────────┐
│  LAYER 1 — pyCoreGage PACKAGE  (pip install once)            │
│                                                              │
│  pyCoreGage/                                                 │
│    setup.py      setup_coregage()   reads rule_registry      │
│    runner.py     run_checks()       loops and executes scripts│
│    reporter.py   build_reports()    merges feedback + xlsx   │
│    collector.py  collect_findings() appends findings to state│
│    counter.py    count_valid()      counts observations       │
│    project.py    create_project()   scaffolds new project    │
│    utils.py      load_inputs()      reads domain data files  │
│    state.py      CoreGageState      mutable run state        │
│    _cli.py       pycoregage CLI     command-line entry point │
│                                                              │
│  pyCoreGage/data/rule_registry.xlsx   blank registry template│
│  pyCoreGage/templates/                project file templates │
└──────────────────────────────────────────────────────────────┘
                        │
          create_project("TRIAL_ABC")
                        │
                        ▼
┌──────────────────────────────────────────────────────────────┐
│  LAYER 2 — USER PROJECT  (one folder per trial/study)        │
│                                                              │
│  TRIAL_ABC/                                                  │
│    run_coregage.py          driver — python run_coregage.py  │
│    rules/                                                    │
│      config/                                                 │
│        rule_registry.xlsx   check definitions (user fills)  │
│        project_config.py    all paths in one place          │
│      trial/                 trial-level check scripts       │
│        AE.py  LB.py  CM.py  check_AE(state, cfg) …         │
│      study/                 study-level check scripts       │
│        AE_study.py …        check_AE_study(state, cfg) …   │
│    inputs/                  drop domain CSV / SAS7BDAT here │
│    outputs/                                                  │
│      reports/               Excel reports written here      │
│      feedback/              reviewer feedback placed here   │
│        DM/  MW/  SDTM/  ADAM/                               │
└──────────────────────────────────────────────────────────────┘

The key separation: the engine (Layer 1) never changes between trials. Only check scripts, rule_registry.xlsx, and inputs/ change per trial.


3. Installation

From PyPI (stable)

pip install pyCoreGage

With SAS file support

pip install "pyCoreGage[sas]"

Development install

git clone https://github.com/ganeshbabunn/pyCoreGage
cd pyCoreGage
pip install -e ".[dev]"

Dependencies

Package Version Purpose
pandas >= 1.5.0 Data manipulation in check scripts
openpyxl >= 3.1.0 Read/write Excel reports

Optional:

Package Purpose
pyreadstat Read .sas7bdat files from inputs/

Requirements

  • Python >= 3.9
  • Works on Windows, macOS, Linux

4. Quick Start

# Step 1 — Install (once)
# pip install pyCoreGage

# Step 2 — Create a new project (once per trial)
from pyCoreGage import create_project

create_project(
    name = "TRIAL_ABC",
    path = "/my/projects",
)

# Step 3 — Fill in rules/config/rule_registry.xlsx
#           (Trial sheet + Study sheet — see Section 6)

# Step 4 — Write check scripts in rules/trial/ and rules/study/
#           (copy check_template.py and implement your logic)

# Step 5 — Drop domain data files into inputs/
#           AE.csv, LB.csv, CM.csv …  (CSV or .sas7bdat)

# Step 6 — Run
# python run_coregage.py
# — or —
# pycoregage run

Or via CLI:

pycoregage create TRIAL_ABC --path /my/projects
cd /my/projects/TRIAL_ABC
# … fill registry, write checks, drop data …
pycoregage run

Expected console output:

=== pyCoreGage : Starting Run ===
  Project : TRIAL_ABC
>> [setup] Starting CoreGage initialisation ...
  Sheet 'Trial' rows    : 5
  Sheet 'Study' rows    : 3
  Active: 8 ON  /  0 OFF
>> [setup] Initialisation complete.
   AE.csv -> domains['ae']  (81 rows)
   LB.csv -> domains['lb']  (1003 rows)
>>>>>>>>>>>>>>>>>>>> Executing: AE <<<<<<<<<<<<<<<<<<<<
  >> [collector] Appending 5 finding(s) for: AECHK001
  >> [collector] Appending 2 finding(s) for: AECHK002
>>>>>>>>>>>>>>>>>>>> Executing: LB <<<<<<<<<<<<<<<<<<<<
  >> [collector] Appending 12 finding(s) for: LBCHK001
>> [runner] All checks executed. Total findings: 19
>> [reporter] Starting consolidation ...
  -------------------------------------------------------
  Feedback summary:
    Notes  : analyst notes: 0  |  reviewer notes: 0
    Status : open: 19  |  queried: 0  |  closed: 0
  -------------------------------------------------------
  Writing: DM_issues.xlsx
  Writing: all_open.xlsx
=== pyCoreGage : Run Complete ===
>> Reports written to: /my/projects/TRIAL_ABC/outputs/reports

5. Project Structure

After create_project(), your folder contains:

TRIAL_ABC/
├── run_coregage.py              ← run this
├── .gitignore
├── rules/
│   ├── config/
│   │   ├── rule_registry.xlsx   ← fill in check definitions
│   │   └── project_config.py    ← edit paths if you move the folder
│   ├── trial/
│   │   └── check_template.py    ← copy and rename for each domain
│   └── study/
├── inputs/                      ← drop AE.csv, LB.csv … here
└── outputs/
    ├── reports/                 ← Excel reports appear here
    └── feedback/
        ├── DM/                  ← DM reviewer places updated files here
        ├── MW/
        ├── SDTM/
        └── ADAM/

6. rule_registry.xlsx — Check Definitions

The registry has two sheets: Trial and Study.

Column Type Description
Category str Domain category (e.g. "Adverse Events")
Subcategory str Sub-category (e.g. "Date Checks")
ID str Unique check identifier (e.g. AECHK001)
Active Yes/No Set to No to disable without deleting
DM_Report Yes/No Include in DM report
MW_Report Yes/No Include in MW report
SDTM_Report Yes/No Include in SDTM report
ADAM_Report Yes/No Include in ADaM report
Rule_Set str Script name without .py (e.g. AE)
Description str Human-readable check description
Notes str Implementation notes

Rule_Set determines which script is sourced. If Rule_Set = "AE", the engine loads rules/trial/AE.py and calls check_AE(state, cfg). For the Study sheet it loads from rules/study/.


7. How It Works

setup_coregage(cfg)
       │
       ▼  reads rule_registry.xlsx → CoreGageState
load_inputs(cfg)
       │
       ▼  reads inputs/*.csv → state.domains{"ae": df, "lb": df, …}
run_checks(cfg, state)
       │
       ├─ for each active Rule_Set in Trial sheet:
       │       import rules/trial/{Rule_Set}.py
       │       call check_{Rule_Set}(state, cfg)
       │               └─ collect_findings(state, df, id="AECHK001")
       │
       └─ for each active Rule_Set in Study sheet:
               import rules/study/{Rule_Set}.py
               call check_{Rule_Set}(state, cfg)
build_reports(cfg, state)
       │
       ├─ import previously saved all_open.xlsx + all_closed.xlsx
       ├─ read feedback from feedback/DM/, MW/, SDTM/, ADAM/
       ├─ merge: status tracking, auto-close, re-open
       └─ write: DM_issues, MW_issues, SDTM_issues, ADAM_issues,
                 all_open, all_closed

Smart status management

  • New findings → status open
  • Findings that disappear from data → auto-closed with tag [auto-closed — finding no longer present]
  • Findings closed by reviewer → permanently closed
  • Findings re-appearing after analyst closure → re-opened with tag [Was closed but re-appeared]

8. Writing Check Scripts

Minimal example — date check (trial level)

# rules/trial/AE.py

import pandas as pd
from pyCoreGage import collect_findings


def check_AE(state, cfg):
    ae = state.domains.get("ae")
    if ae is None or ae.empty:
        return state

    active_rules = state.active_rules

    if active_rules.get("AECHK001"):
        sub = ae.copy()
        sub["st"] = pd.to_datetime(sub["AESTDTC"], errors="coerce")
        sub["en"] = pd.to_datetime(sub["AEENDTC"], errors="coerce")
        result = sub[sub["en"].notna() & sub["st"].notna() & (sub["en"] < sub["st"])].copy()

        result["subj_id"]     = result["USUBJID"]
        result["vis_id"]      = float("nan")
        result["description"] = (
            "End (" + result["en"].dt.strftime("%d%b%Y") +
            ") before start (" + result["st"].dt.strftime("%d%b%Y") +
            ") for: " + result["AETERM"]
        )
        state = collect_findings(
            state,
            result[["subj_id", "vis_id", "description"]],
            id="AECHK001",
        )

    return state

Cross-domain study-level check

# rules/study/DM_study.py

import pandas as pd
from pyCoreGage import collect_findings


def check_DM_study(state, cfg):
    ae = state.domains.get("ae")
    dm = state.domains.get("dm")
    active_rules = state.active_rules

    if active_rules.get("DMPRJ001") and ae is not None and dm is not None:
        ae_subjects = set(ae["USUBJID"].dropna())
        dm_subjects = set(dm["USUBJID"].dropna())
        missing = ae_subjects - dm_subjects

        if missing:
            result = pd.DataFrame({
                "subj_id":     list(missing),
                "vis_id":      [float("nan")] * len(missing),
                "description": [f"Subject {s} has AE but no DM record" for s in missing],
            })
            state = collect_findings(state, result, id="DMPRJ001")

    return state

Rules for check scripts

  1. The file must be named {Rule_Set}.py — e.g. AE.py for Rule_Set = "AE"
  2. The function must be named check_{Rule_Set}(state, cfg) — e.g. check_AE
  3. Always return state at the end
  4. Call collect_findings() once per check ID
  5. The findings DataFrame must have columns: subj_id, vis_id, description

9. API Reference

setup_coregage(cfg) → CoreGageState

Reads rule_registry.xlsx, builds the active-rules switch dict, returns a fresh CoreGageState.

load_inputs(cfg) → dict[str, DataFrame]

Reads all .csv (and optionally .sas7bdat) files from cfg.inputs. Returns a dict keyed by lowercase filename stem: {"ae": df, "lb": df}.

run_checks(cfg, state) → CoreGageState

Iterates active rule sets, dynamically imports each check script, calls check_{Rule_Set}(state, cfg).

collect_findings(state, df, id, desc_col="description", sobs=True, unblind_codes=None) → CoreGageState

Validates and appends a findings DataFrame to state.issues.

Parameter Type Description
state CoreGageState Current run state
df DataFrame Findings with subj_id, vis_id, description
id str Check ID matching registry
desc_col str Alternate description column name
sobs bool Flag for subject-observation limiting
unblind_codes list[str] Topic codes for unblinding protection

count_valid(df, unblind_codes=None) → int

Returns row count, optionally excluding unblinding-risk rows.

build_reports(cfg, state) → None

Merges saved issues + feedback, writes six Excel reports to cfg.reports.

create_project(name, path, overwrite=False) → str

Scaffolds a complete project folder. Returns the project root path.

CoreGageConfig

from pyCoreGage import CoreGageConfig

cfg = CoreGageConfig(
    project_name  = "TRIAL_ABC",
    rule_registry = "/path/to/rules/config/rule_registry.xlsx",
    trial_checks  = "/path/to/rules/trial",
    study_checks  = "/path/to/rules/study",
    inputs        = "/path/to/inputs",
    reports       = "/path/to/outputs/reports",
    feedback      = "/path/to/outputs/feedback",
)

10. Console Output Reference

Message Meaning
Active: 8 ON / 0 OFF 8 checks enabled, 0 disabled
AE.csv -> domains['ae'] (81 rows) AE domain loaded with 81 rows
>> [collector] Appending 5 finding(s) for: AECHK001 5 findings collected
WARNING: Check script not found: AE.py -- skipping. Script missing — check Rule_Set in registry
ERROR in rule set AE: … Exception in check script — other checks continue
[auto-closed — finding no longer present] Finding disappeared from data on re-run
[Was closed but re-appeared] Previously closed finding is back in data


License

GPL-3.0-or-later © Ganesh Babu G

Citation

pyCoreGage: Data Quality Check Framework for Clinical and Analytical Data.
https://github.com/ganeshbabunn/pyCoreGage

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pycoregage-1.0.0.tar.gz (12.9 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pycoregage-1.0.0-py3-none-any.whl (50.4 kB view details)

Uploaded Python 3

File details

Details for the file pycoregage-1.0.0.tar.gz.

File metadata

  • Download URL: pycoregage-1.0.0.tar.gz
  • Upload date:
  • Size: 12.9 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.2

File hashes

Hashes for pycoregage-1.0.0.tar.gz
Algorithm Hash digest
SHA256 3487ca1a712d9c1bff734a381b888d4fed34b5585bae754a320ec68884a06d9c
MD5 a02d02065a1e9a6ab3bd3d2b3f15e3bc
BLAKE2b-256 b960d419de283ba5040386d96175692070bf721b2090a2dde3d0b37289177b92

See more details on using hashes here.

File details

Details for the file pycoregage-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: pycoregage-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 50.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.2

File hashes

Hashes for pycoregage-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 79b5f76f8f422c6e7471ca56efdb18869ca6319302697d0aa6af44f37838358d
MD5 4be4c077a186323d7acf481ec52cfabd
BLAKE2b-256 1bdda1056e94484a33078fb78188a032fad6bcc7d67025a4d4cc3b5b6a45b2b1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page