agentleFS
Sign inSign up

batch-processing-clinical-text

maziyarpanahi/openmed/skills/batch-processing-clinical-text/SKILL.md

Run large-scale batch NER, PII extraction, or de-identification over many clinical notes on-device with OpenMed, with sharding, checkpointing, resumability, and append-only JSONL output. Use when the user needs to process a corpus or folder of notes, de-identify a dataset, run NER over thousands of documents, build a resumable batch pipeline, or stream results to JSONL without holding everything in memory. Covers process_batch / BatchProcessor / BatchItem / BatchResult, the operation= selector (analyze_text | extract_pii | deidentify), iter_process streaming, the PHI-safe on_progress callback, chunking long documents, and no-PHI logging. Produces a resumable batch runner over an OpenMed model.

Skill5.4k starsChanged 47 days ago
---
name: batch-processing-clinical-text
description: "Run large-scale batch NER, PII extraction, or de-identification over many clinical notes on-device with OpenMed, with sharding, checkpointing, resumability, and append-only JSONL output. Use when the user needs to process a corpus or folder of notes, de-identify a dataset, run NER over thousands of documents, build a resumable batch pipeline, or stream results to JSONL without holding everything in memory. Covers process_batch / BatchProcessor / BatchItem / BatchResult, the operation= selector (analyze_text | extract_pii | deidentify), iter_process streaming, the PHI-safe on_progress callback, chunking long documents, and no-PHI logging. Produces a resumable batch runner over an OpenMed model."
license: Apache-2.0
metadata:
  project: OpenMed
  category: deployment-ops
  pairs: adjacent
  version: "1.0"
---

# Batch processing clinical text

`openmed.processing` runs OpenMed over many documents efficiently, with progress
tracking, per-item error isolation, and streaming. It runs **fully on-device**:
the corpus, the model, and the output never leave the host. This skill shows a
**resumable** runner — sharded, checkpointed, append-only JSONL — that you can
restart without reprocessing.

## When to use this skill

For corpora, folders, or datasets — anything beyond a handful of notes. For a
single note, just call `openmed.analyze_text` / `deidentify` directly
(`extracting-clinical-entities`, `deidentifying-clinical-text`). For an
always-on HTTP service, see `serving-openmed-rest-api`.

## Quick start

```python
from openmed import process_batch

texts = ["Patient has type 2 diabetes.", "No acute distress. BP 120/80."]
result = process_batch(texts, model_name="disease_detection_superclinical")

print(result.summary())          # PHI-safe counts + timing
print(result.successful_items, "/", result.total_items)
for item in result.get_successful_results():
    print(item.id, item.result.to_dict()["entities"])   # spans only; avoid raw text in logs
```

`process_batch(...)` is a thin wrapper over `BatchProcessor`. Real signatures
(`openmed/processing/batch.py`):

- `process_batch(texts, model_name="disease_detection_superclinical", ids=None, config=None, progress_callback=None, on_progress=None, **kwargs) -> BatchResult`
- `BatchProcessor(model_name=..., operation="analyze_text", batch_size=8, continue_on_error=True, **analyze_kwargs)`
  with `operation ∈ {"analyze_text", "extract_pii", "deidentify"}`.
- `BatchItem(id, text, source=None, metadata=None)`
- `BatchResult` — `.items`, `.total_items`, `.successful_items`, `.failed_items`,
  `.success_rate`, `.average_processing_time`, `.summary()`, `.to_dict()`,
  `.get_successful_results()`, `.get_failed_results()`.
- `BatchItemResult` — `.id`, `.result` (a `PredictionResult`/`DeidentificationResult`),
  `.error`, `.processing_time`, `.source`, `.success`, `.to_dict()`.

## Choosing the operation

```python
from openmed import BatchProcessor

# NER (default)
ner = BatchProcessor(model_name="disease_detection_superclinical")          # operation="analyze_text"
# Detect PHI spans
pii = BatchProcessor(operation="extract_pii", model_name="OpenMed/OpenMed-PII-SuperClinical-Small-44M-v1")
# De-identify (rewrites text)
deid = BatchProcessor(operation="deidentify", method="mask", confidence_threshold=0.7)
```

`BatchProcessor` reuses one model loader across items (and a cached privacy
filter for PII ops), so a single processor over many texts is far cheaper than
many one-off calls.

## Streaming + PHI-safe progress

```python
from openmed.processing import BatchProcessor, BatchProgress

proc = BatchProcessor(operation="deidentify", method="mask", batch_size=16)

def on_progress(p: BatchProgress) -> None:   # frozen record: completed/total/current_index/elapsed
    if p.completed % 500 == 0:
        print(f"{p.completed}/{p.total} ({p.elapsed:.1f}s)")   # NO PHI

for item in proc.iter_process(texts, ids=doc_ids, on_progress=on_progress):
    write_jsonl(item)   # one result at a time — constant memory for huge corpora
```

`on_progress` receives only counts/timing (never text), so it is safe to log.
`iter_process` yields `BatchItemResult`s without holding the whole corpus.

## Workflow

1. **De-identify upstream** if the corpus has PHI, or key everything by stable
   internal `ids` so logs/output never carry identifiers.
2. **Pick the operation + model** — one `BatchProcessor` per
   `operation` (`analyze_text` / `extract_pii` / `deidentify`); reuse it across
   all items so the loader is shared.
3. **Shard the corpus** into independent partitions writing to separate JSONL
   files; run shards as separate processes for parallelism.
4. **Stream with `iter_process`** and append each `BatchItemResult` to JSONL
   (flush per item) — that append-only file is your checkpoint.
5. **Track progress** with the PHI-safe `on_progress` callback (counts/timing
   only).
6. **Re-run to resume:** skip ids already present in the output; with
   `continue_on_error=True`, failures are recorded, not raised.
7. **Reconcile** via `result.get_failed_results()` / a failures pass, then hand
   JSONL to the downstream consumer.

## A resumable batch runner

```python
import json
from pathlib import Path
from openmed.processing import BatchProcessor

def run_resumable(texts, ids, out_path: Path, *, operation="deidentify", **kw):
    out_path.parent.mkdir(parents=True, exist_ok=True)
    # 1) checkpoint = ids already written (append-only JSONL is the source of truth)
    done = set()
    if out_path.exists():
        with out_path.open() as f:
            done = {json.loads(line)["id"] for line in f if line.strip()}
    todo = [(i, t) for i, t in zip(ids, texts) if i not in done]
    if not todo:
        return
    pending_ids, pending_texts = zip(*todo)

    proc = BatchProcessor(operation=operation, continue_on_error=True, **kw)
    # 2) append each result as it completes -> safe to kill/restart anytime
    with out_path.open("a") as f:
        for item in proc.iter_process(list(pending_texts), ids=list(pending_ids)):
            record = {"id": item.id, "ok": item.success,
                      "result": item.result.to_dict() if item.success else None,
                      "error": item.error}            # item.error is a message, keep PHI out
            f.write(json.dumps(record) + "\n")
            f.flush()
```

Re-running skips finished ids (`continue_on_error=True` keeps one bad doc from
killing the run; failures are recorded, not raised). Shard a corpus by writing
to `out/shard_000.jsonl`, `shard_001.jsonl`, … and run shards in separate
processes.

## Chunking long documents

`BatchProcessor` does not split single documents. For notes longer than the
model's max sequence length, pre-split into sentences/windows
(`openmed.processing.sentences`) — or for `analyze_text`, rely on built-in
sentence handling — then re-stitch entities by adding the chunk offset back to
each entity's `start`/`end` so spans point into the original document.

## Hand-off to / from OpenMed

- **Per-item engine:** each operation calls the same `openmed.analyze_text` /
  `extract_pii` / `deidentify` you'd call directly — same results, batched.
- **Downstream:** JSONL feeds `building-patient-timelines`, `etl-to-omop-cdm`,
  and `exporting-to-fhir`. De-id JSONL feeds `evaluating-with-leakage-gates`.
- **Service vs batch:** for request/response use `serving-openmed-rest-api`; for
  corpora use this batch path.

## Edge cases & gotchas

- **No PHI in logs/JSONL keys.** Use stable `ids` (`BatchItem.id`), offsets, and
  labels. `BatchItemResult.error` is a message — keep raw text out of inputs to
  exceptions you log.
- **`continue_on_error=True` is the default** and recommended for corpora; check
  `result.get_failed_results()` afterward. Set `False` only when one failure
  should abort everything.
- **`batch_size` is a throughput dial**, not correctness — tune to memory/CPU.
  Larger isn't always faster on CPU.
- **Append-only is the checkpoint.** Don't buffer results in memory and write at
  the end; you lose progress on a crash. Append + flush per item.
- **`process_files`/`process_directory`** read files for you and set
  `BatchItem.source`; unreadable files become failed items (not crashes) under
  `continue_on_error`.
- **Mixed languages:** pass `lang=` per run for PII ops; don't run an English
  PII model across other languages (`deidentifying-multilingual-text`).

## Standards & references

- JSON Lines (newline-delimited JSON): https://jsonlines.org/
- HIPAA de-identification (when batching PHI removal), 45 CFR 164.514(b):
  https://www.hhs.gov/hipaa/for-professionals/privacy/special-topics/de-identification/index.html
- OpenMed source: `openmed/processing/batch.py` (`process_batch`,
  `BatchProcessor`, `BatchItem`, `BatchItemResult`, `BatchResult`,
  `BatchProgress`, `iter_process`).

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.