DEV Community

VBC Risk Analytics
VBC Risk Analytics

Posted on

How to Efficiently Compute RAF Scores for 100,000+ Members in a Nightly Batch Job

The bottleneck is almost never the math — RAF scoring per member is cheap. The real pain is owning the model tables, plus a job that can't resume when it dies at 3 a.m. If you're scoring against CMS-HCC, the practical answer is to push the population through a managed Batch RAF API and keep your code as an orchestrator. That API is a 3-step async job, not a "POST a file, get JSON back" call — get that shape right and the rest is easy.

1. Submit, poll, download (3 steps)

Base URL https://www.vbcriskanalytics.com/raf-batch-api. Every request needs two headers — ApiKey: <your-batch-key> and an empty X-CSRF-TOKEN: (it's not Bearer).

# Step 1: submit as multipart/form-data -> returns a raf_batch_id
curl -X POST 'https://www.vbcriskanalytics.com/raf-batch-api/getPreProspectScore' \
  -H 'accept: */*' -H "ApiKey: $RAF_BATCH_API_KEY" -H 'X-CSRF-TOKEN: ' \
  -F 'risk_model=CMS-HCC-V28 Continuing Enrollee' \
  -F 'risk_factor=Community NonDual Aged' \
  -F 'file=@input_pre_prospective.csv;type=text/csv'
# -> {"code":201,"raf_batch_id":3400,"status":"Queued",
#     "check_status_url":".../raf-batch-api/check-status/3400"}

# Step 2: poll until Completed (Queued 201 -> Running 202 -> Completed 200)
curl '.../raf-batch-api/check-status/3400' -H "ApiKey: $RAF_BATCH_API_KEY" -H 'X-CSRF-TOKEN: '

# Step 3: get the signed download URL
curl '.../raf-batch-api/download/3400' -H "ApiKey: $RAF_BATCH_API_KEY" -H 'X-CSRF-TOKEN: '
# -> {"download_url":"https://...s3.amazonaws.com/...zip?X-Amz-...","status":"Completed"}

# Step 4: GET that S3 URL directly — NO ApiKey header, it EXPIRES in ~120s.
#          You get a .zip with a timestamped .xlsx of scored members.
Enter fullscreen mode Exit fullscreen mode

Submit endpoints map to score types: /getPreProspectScore, /getPostProspectScore, /getPostCncntScore.

2. Get the CSV schema right — one row per (member, diagnosis)

This trips people up: it's not one row per member. Rows are grouped by ID:

ID,Gender,Age,ICD-10 CM Code,Flag
1,Male,65,E1122,Last_Year
1,Male,65,J449,Current_Year
2,Male,84,E1142,Last_Year
Enter fullscreen mode Exit fullscreen mode

Gender is Male/Female, Age ≤ 125, the dot in the ICD-10 code is optional, and valid Flag values depend on score type (Pre-Prospective Last_Year/Current_Year; Post-Prospective New/Billed/Missed; Post-Concurrent No_Changes/Deletion/Addition plus a Modification_To column).

3. For throughput, fan out jobs and poll asynchronously

Split the population into CSV chunks, submit them all, then poll each to Completed:

import os, time, requests
BASE = "https://www.vbcriskanalytics.com/raf-batch-api"
H = {"ApiKey": os.environ["RAF_BATCH_API_KEY"], "X-CSRF-TOKEN": ""}

def submit(path):
    with open(path, "rb") as f:
        r = requests.post(f"{BASE}/getPreProspectScore", headers=H,
            data={"risk_model": "CMS-HCC-V28 Continuing Enrollee",
                  "risk_factor": "Community NonDual Aged"},
            files={"file": ("input.csv", f, "text/csv")})
    return r.json()["raf_batch_id"]

jobs = [submit(p) for p in chunk_paths]          # submit all chunks
for jid in jobs:                                 # then poll each
    while requests.get(f"{BASE}/check-status/{jid}", headers=H).json()["status"] != "Completed":
        time.sleep(5)
    url = requests.get(f"{BASE}/download/{jid}", headers=H).json()["download_url"]
    open(f"scored_{jid}.zip", "wb").write(requests.get(url).content)  # signed URL, no ApiKey
Enter fullscreen mode Exit fullscreen mode

4. Make it idempotent

The API returns a durable raf_batch_id and a check-status URL, so persist those per chunk. A re-run can reattach to in-flight jobs instead of resubmitting them — a 3 a.m. failure becomes a cheap retry, not a full restart, and it doesn't waste your available hits quota.

A couple of gotchas

  • Validate before you submit so you don't burn quota on bad rows. The API returns specific codes: 418 invalid gender, 419 age > 125, 420 columns missing, 427 only CSV allowed, 429 invalid flag, 425/426 invalid risk model/factor, 430 license/limit.
  • Store the risk_model/risk_factor you submitted with each output so historical scores stay reproducible for audits.

Wrapping up

If you'd rather not maintain the model tables and hierarchies yourself, there's a good writeup on a managed approach to batch RAF scoring for large member populations, and the batch RAF API that handles the overnight throughput.


VBC Risk Analytics. Educational only — not coding, billing, or clinical advice; verify against the current CMS Rate Announcement. Synthetic data only.

Top comments (0)