Skip to content

Analytics with Polars

Read a folder of remittances into Polars frames and answer the usual questions: who denies, which adjustments cost the most, how charges compare with payments.

Load a folder

Tables join across a file by row number, and row numbers restart in each file, so add the file name to every frame and join on it. Text columns are bytes in the tables. Cast them to String where you want to read or group by them.

from pathlib import Path

import oxedi
import polars as pl

parts = {}
for path in sorted(Path("remits").glob("*.835")):
    result = oxedi.parse_file(path)
    for name in result.tables.keys():
        frame = result.tables[name].to_polars().with_columns(file=pl.lit(path.name))
        parts.setdefault(name, []).append(frame)

tables = {name: pl.concat(frames) for name, frames in parts.items()}

payments = tables["payments"].select(
    "file", payment=pl.col("row"), payer=pl.col("payer_name").cast(pl.String)
)
claims = tables["claims"].join(payments, on=["file", "payment"])

claims now has the payer on every row.

Denials by payer

Claim status 4 is a denial. Count them next to the total.

denials = claims.group_by("payer").agg(
    claims=pl.len(),
    denied=(pl.col("claim_status") == b"4").sum(),
    denied_charges=pl.col("charge_amount").filter(pl.col("claim_status") == b"4").sum(),
)
print(denials.with_columns(rate=pl.col("denied") / pl.col("claims")).sort("rate", descending=True))

CO and PR adjustments

An adjustment belongs to a claim (when service is empty) or to one service line. Its group_code says who bears the amount: CO contractual obligation, PR patient responsibility, OA other adjustment, PI payer-initiated reduction (and CR correction, in 4010 files). Join the claims to get the payer, then total by group and reason.

adjustments = tables["adjustments"].join(
    claims.select("file", "payer", claim=pl.col("row")), on=["file", "claim"]
)

by_group = (
    adjustments.with_columns(
        group=pl.col("group_code").cast(pl.String),
        reason=pl.col("reason_code").cast(pl.String),
    )
    .group_by("payer", "group", "reason")
    .agg(amount=pl.col("amount").sum(), adjustments=pl.len())
    .sort("amount", descending=True)
)
print(by_group.filter(pl.col("group").is_in(["CO", "PR"])))

Charged versus paid

Money columns are exact decimals, so sums do not drift.

summary = claims.group_by("payer").agg(
    charged=pl.col("charge_amount").sum(),
    paid=pl.col("payment_amount").sum(),
    patient=pl.col("patient_responsibility").sum(),
)
print(summary.with_columns(paid_share=(pl.col("paid").cast(pl.Float64) / pl.col("charged").cast(pl.Float64)).round(3)))

For large folders, use oxedi.stream per file to keep memory bounded. See Reading large files. When the data already lives in DuckDB or Parquet, see a Parquet lakehouse and read the result with pl.read_parquet.