Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/bfd-model-idr/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ out/*
!out/ExplanationOfBenefit-Carrier.json
!out/ExplanationOfBenefit-DME.json
!out/ExplanationOfBenefit-Pharmacy.json
!out/ExplanationOfBenefit-PriorAuth.json
sushi/fsh-generated/*
validator_cli.jar
ReferenceTables/source-to-target-mappings
Expand Down
14 changes: 14 additions & 0 deletions apps/bfd-model-idr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ curl -X GET "http://localhost:8080/matchboxv3/actuator/health"

As new versions of IGs are released, they may have multiple nested dependencies. This takes up a significant amount of memory if loaded directly into Matchbox. To eliminate heap errors while still being able to accurately validate profiles and terminology, we download the FHIR Packages locally, untar them, and upload relevant resources directly to Matchbox. The list of resources + packages are in matchbox_profiles.txt. To add a new IG reference, follow the syntax in that file. Packages are only uploaded using docker compose up (by calling setup_matchbox.py), so restart the composition if adding more dependencies.

### Generating Sample JSON from Synthetic CSVs

To generate or update sample data files from the generated synthetic CSVs (located in `out/`), you can use the sample generation scripts. This only exists for prior auth data for now - it will be expanded to other use cases gradually.

#### Prior Authorization Sample Generator

To generate a prior authorization JSON sample from the synthetic CSVs based on tracking number.

```sh
python generate_prior_auth_sample.py --utn=<utn-here>
```

This will search for the specified UTN in `out/SYNTHETIC_PRAUC.csv`, collect the segments, and get it into the format that we map using FML.

### Create FHIR files with synthetic data

Requires Matchbox to be active.
Expand Down
104 changes: 73 additions & 31 deletions apps/bfd-model-idr/augment_sample_resources.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os
import sys
import yaml
from dataclasses import asdict, dataclass, field
Expand All @@ -15,6 +16,31 @@
cond_sk_info_file = "sample-data/CLM_RLT_COND_SGNTR_MBR_POC.csv"
cond_sk_df = pd.read_csv(cond_sk_info_file, dtype={"CLM_RLT_COND_SGNTR_SK": str})

synth_df = None
synth_prvdr_file = "out/SYNTHETIC_PRVDR_HSTRY.csv"
if os.path.exists(synth_prvdr_file):
try:
synth_df = pd.read_csv(synth_prvdr_file, dtype={"PRVDR_SK": str})
except Exception:
pass


def lookup_provider_history(npi_num: str | int | None) -> dict | None:
if not npi_num:
return None
npi_str = str(npi_num).strip()
if not npi_str or npi_str == "None":
return None
matching_rows = df[df["PRVDR_SK"] == npi_str]
if not matching_rows.empty:
return json.loads(matching_rows.iloc[0].to_json())
if synth_df is not None:
matching_rows = synth_df[synth_df["PRVDR_SK"] == npi_str]
if not matching_rows.empty:
return json.loads(matching_rows.iloc[0].to_json())
return None


cur_sample = sys.argv[1]
cur_sample_data = {}
with Path(cur_sample).open("r") as file:
Expand Down Expand Up @@ -42,8 +68,11 @@

def load_profile_map():
profile_map = {}
paths = [Path("dictionary-support-files/ExplanationOfBenefit.yaml"),
Path("dictionary-support-files/ExplanationOfBenefit-Pharmacy.yaml")]
paths = [
Path("dictionary-support-files/ExplanationOfBenefit.yaml"),
Path("dictionary-support-files/ExplanationOfBenefit-Pharmacy.yaml"),
Path("dictionary-support-files/ExplanationOfBenefit-PriorAuth.yaml"),
]

for p in filter(Path.exists, paths):
with p.open("r") as f:
Expand Down Expand Up @@ -257,6 +286,13 @@ class Provider:
]
provider_list = []

# we only use CLM_SRVC_PRVDR_GNRC_ID_NUM for part D events (we filter for PRVDR_SRVC_NPI_)
if cur_sample_data.get("CLM_TYPE_CD") not in (1, 2, 3, 4):
billing_column = "PRVDR_BLG_PRVDR_NPI_NUM"
else:
billing_column = "CLM_SRVC_PRVDR_GNRC_ID_NUM"


# There may be an opportunity to consolidate even the duplicate NPIs into a
# single careTeam reference, but we should wait to get feedback on this
# The reason being: it's possible to lose context on rendering vs ordering
Expand All @@ -270,16 +306,18 @@ def create_billing_and_service_provider(billing_col_name):
if qualifier in ("01", None):
# Only pull NPI data if it's an NPI
npi_num = cur_sample_data.get(billing_col_name)
prvdr_hstry_for_npi = json.loads(df[df["PRVDR_SK"] == str(npi_num)].iloc[0].to_json())
provider_object.NPI_TYPE = "2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
provider_object.PRVDR_SK = npi_num
provider_object.PRVDR_LAST_OR_LGL_NAME = (
prvdr_hstry_for_npi["PRVDR_LGL_NAME"]
if provider_object.NPI_TYPE == "2"
else prvdr_hstry_for_npi["PRVDR_LAST_NAME"]
)
if prvdr_hstry_for_npi.get("PRVDR_1ST_NAME"):
provider_object.PRVDR_1ST_NAME = prvdr_hstry_for_npi.get("PRVDR_1ST_NAME")
prvdr_hstry_for_npi = lookup_provider_history(npi_num)
if prvdr_hstry_for_npi:
provider_object.NPI_TYPE = "2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
provider_object.PRVDR_SK = npi_num
provider_object.PRVDR_LAST_OR_LGL_NAME = (
prvdr_hstry_for_npi["PRVDR_LGL_NAME"]
if provider_object.NPI_TYPE == "2"
else prvdr_hstry_for_npi["PRVDR_LAST_NAME"]
)
if prvdr_hstry_for_npi.get("PRVDR_1ST_NAME"):
provider_object.PRVDR_1ST_NAME = prvdr_hstry_for_npi.get("PRVDR_1ST_NAME")

if cur_sample_data.get("CLM_BLG_PRVDR_OSCAR_NUM"):
provider_object.PRVDR_OSCAR_NUM = cur_sample_data.get("CLM_BLG_PRVDR_OSCAR_NUM")
if cur_sample_data.get("CLM_BLG_PRVDR_TAX_NUM"):
Expand Down Expand Up @@ -308,17 +346,20 @@ def create_careteam_provider(careteam_column):
if qualifier in ("01", None):
# Only pull NPI data if it's an NPI
npi_num = cur_sample_data.get(careteam_column)
prvdr_hstry_for_npi = json.loads(df[df["PRVDR_SK"] == str(npi_num)].iloc[0].to_json())
provider_object.NPI_TYPE = "2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
provider_object.PRVDR_SK = npi_num
# set a default name using PRVDR_HSTRY if not available.
provider_object.PRVDR_CARETEAM_NAME = (
prvdr_hstry_for_npi["PRVDR_LGL_NAME"]
if provider_object.NPI_TYPE == "2"
else prvdr_hstry_for_npi["PRVDR_LAST_NAME"]
+ ", "
+ prvdr_hstry_for_npi["PRVDR_1ST_NAME"]
)
prvdr_hstry_for_npi = lookup_provider_history(npi_num)
if prvdr_hstry_for_npi:
provider_object.NPI_TYPE = "2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
provider_object.PRVDR_SK = npi_num
# set a default name using PRVDR_HSTRY if not available.
provider_object.PRVDR_CARETEAM_NAME = (
prvdr_hstry_for_npi["PRVDR_LGL_NAME"]
if provider_object.NPI_TYPE == "2"
else f"{prvdr_hstry_for_npi.get('PRVDR_LAST_NAME') or ''}, {prvdr_hstry_for_npi.get('PRVDR_1ST_NAME') or ''}".strip(", ")
)
else:
provider_object.NPI_TYPE = "1"
provider_object.PRVDR_SK = npi_num
provider_object.PRVDR_CARETEAM_NAME = "Practitioner"

provider_object.careTeamType = careteam_header_columns.get(careteam_column)
if qualifier:
Expand Down Expand Up @@ -351,17 +392,18 @@ def create_rendering_line_provider(npi_num):
provider_object = Provider(
PRVDR_SK=npi_num, careTeamType="rendering", PRVDR_CARETEAM_NAME="N/A"
)
prvdr_hstry_for_npi = json.loads(df[df["PRVDR_SK"] == str(npi_num)].iloc[0].to_json())
provider_object.NPI_TYPE = "2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
provider_object.PRVDR_CARETEAM_NAME = (
prvdr_hstry_for_npi["PRVDR_LGL_NAME"]
if provider_object.NPI_TYPE == "2"
else prvdr_hstry_for_npi["PRVDR_LAST_NAME"] + ", " + prvdr_hstry_for_npi["PRVDR_1ST_NAME"]
)
prvdr_hstry_for_npi = lookup_provider_history(npi_num)
if prvdr_hstry_for_npi:
provider_object.NPI_TYPE = "2" if prvdr_hstry_for_npi.get("PRVDR_LGL_NAME") else "1"
provider_object.PRVDR_CARETEAM_NAME = (
prvdr_hstry_for_npi["PRVDR_LGL_NAME"]
if provider_object.NPI_TYPE == "2"
else f"{prvdr_hstry_for_npi.get('PRVDR_LAST_NAME') or ''}, {prvdr_hstry_for_npi.get('PRVDR_1ST_NAME') or ''}".strip(", ")
)
return provider_object

# now we go through the line items!
for line_item in cur_sample_data["lineItemComponents"]:
for line_item in cur_sample_data.get("lineItemComponents", []):
# we only care about PRVDR_RNDRNG_PRVDR_NPI_NUM
cur_rendering_providers = [
x.PRVDR_SK for x in provider_list if getattr(x, "careTeamType", None) == "rendering"
Expand Down
111 changes: 107 additions & 4 deletions apps/bfd-model-idr/claims_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from claims_adj import AdjudicatedGeneratorUtil
from claims_other import OtherGeneratorUtil
from claims_pac import PacGeneratorUtil
from claims_priorauth import PriorAuthGeneratorUtil
from claims_static import INSTITUTIONAL_CLAIM_TYPES, PHARMACY_CLM_TYPE_CDS, PROFESSIONAL_CLAIM_TYPES
from claims_util import four_part_key, match_line_num
from generator_util import (
Expand All @@ -35,6 +36,7 @@
CLM_VAL,
CNTRCT_PBP_NUM,
PRVDR_HSTRY,
PRAUC,
GeneratorUtil,
RowAdapter,
adapters_to_dicts,
Expand Down Expand Up @@ -566,6 +568,89 @@ class _ClaimsFile(StrEnum):
f.META_LST_UPDT_SK,
],
)
PRAUC = (
PRAUC,
[
f.MBI_NUM,
f.CAN,
f.EQUAT_BIC,
f.UTN,
f.SEGMENT_COUNT,
f.CURRENT_SEGMENT,
f.UTN_VALID_ST_DT,
f.UTN_VALID_EN_DT,
f.PA_IND,
f.CLM_TYPE,
f.HCPCS_OR_CPT_OR_HIPPS,
f.ICD_PROC_IND,
f.ICD_PROC_CODE,
f.MAC_ID,
f.PA_FILLER,
f.ICN_DCN,
f.PA_DT_ADDED,
f.PA_DT_UPDATED,
f.SERVICE_CNTS,
f.PA_DECISION,
f.PA_REQ_SUB_DT,
f.PA_REQ_REC_DT,
f.PA_DECISION_DT,
f.PA_DECISION_EXP_DT,
f.ORDER_REFER_NPI,
f.RENDER_NPI,
f.OPERATE_NPI,
f.SVC_RENDER_ST,
f.PRICE_MOD1,
f.PRICE_MOD2,
f.PLACE_OF_SERV,
f.ICD_DIAG_IND_1,
f.ICD_DIAG_CODE_1,
f.ICD_DIAG_IND_2,
f.ICD_DIAG_CODE_2,
f.ICD_DIAG_IND_3,
f.ICD_DIAG_CODE_3,
f.ICD_DIAG_IND_4,
f.ICD_DIAG_CODE_4,
f.ICD_DIAG_IND_5,
f.ICD_DIAG_CODE_5,
f.NPI,
f.NAME,
f.CMS_CERT,
f.REV_CODE_1,
f.REV_CODE_2,
f.REV_CODE_3,
f.REV_CODE_4,
f.REV_CODE_5,
f.REV_CODE_6,
f.REV_CODE_7,
f.REV_CODE_8,
f.REV_CODE_9,
f.REV_CODE_10,
f.REV_CODE_11,
f.REV_CODE_12,
f.REV_CODE_13,
f.REV_CODE_14,
f.REV_CODE_15,
f.REV_CODE_16,
f.REV_CODE_17,
f.REV_CODE_18,
f.REV_CODE_19,
f.REV_CODE_20,
f.COND_CODE_1,
f.COND_CODE_2,
f.COND_CODE_3,
f.COND_CODE_4,
f.OCCUR_CODE_1,
f.OCCUR_CODE_2,
f.OCCUR_CODE_3,
f.OCCUR_CODE_4,
f.TOB,
f.MR_COUNT_IND,
f.MR_COUNT_ST_DT,
f.MR_COUNT_END_DT,
f.ATT_PHY_NPI,
f.RRB_EXCL_IND,
],
)

def __init__(
self,
Expand Down Expand Up @@ -725,6 +810,7 @@ def generate(
CLM_RLT_COND_SGNTR_MBR: [],
PRVDR_HSTRY: [],
CNTRCT_PBP_NUM: [],
PRAUC: [],
}
load_file_dict(files=files, paths=list(paths))
gen_utils.cntrct_pbp_num = [row.kv for row in files[CNTRCT_PBP_NUM]]
Expand Down Expand Up @@ -1084,10 +1170,14 @@ def generate(
clm_lines=clm_line_dcmtns,
clm_line_num=clm_line_num,
)
tracking_num = clm_line.get(f.CLM_LINE_PA_UNIQ_TRKNG_NUM) or (
init_clm_line_dcmtn[f.CLM_LINE_PA_UNIQ_TRKNG_NUM]
if init_clm_line_dcmtn
else None
tracking_num = (
clm_line.get(f.CLM_LINE_PA_UNIQ_TRKNG_NUM)
or clm_line.get(f.CLM_LINE_PMD_UNIQ_TRKNG_NUM)
or (
init_clm_line_dcmtn[f.CLM_LINE_PA_UNIQ_TRKNG_NUM]
if init_clm_line_dcmtn
else None
)
)
if tracking_num:
out_tables[CLM_LINE_DCMTN].append(
Expand Down Expand Up @@ -1167,6 +1257,19 @@ def generate(

print("Done generating synthetic claims data for provided BENE_SKs")

# Generate synthetic prior authorization data (SYNTHETIC_PRAUC)
print("Generating synthetic prior authorization data...")
pa_util = PriorAuthGeneratorUtil()
prauc_rows = pa_util.gen_prior_auths(
gen_utils=gen_utils,
files=files,
out_tables=out_tables,
generated_type_1_npis=generated_type_1_npis,
generated_type_2_npis=generated_type_2_npis,
)
out_tables[PRAUC].extend(prauc_rows)
print(f"Done generating synthetic prior authorization data.")

_save_claims_data({_ClaimsFile(k): v for k, v in out_tables.items() if k in _ClaimsFile})


Expand Down
3 changes: 2 additions & 1 deletion apps/bfd-model-idr/claims_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
CLM_ANSI_SGNTR,
RowAdapter,
gen_basic_id,
gen_npi_id,
)

_faker = Faker()
Expand Down Expand Up @@ -82,7 +83,7 @@ def gen_provider_history(
generated_type_1_npis = set()
generated_type_2_npis = set()
for idx, provider_history in enumerate(all_provider_historys):
prvdr_sk = gen_basic_id(field="PRVDR_SK", length=9)
prvdr_sk = gen_npi_id(field="PRVDR_SK")
# make half of providers type 1 npi and half type 2
# type 1 npis never have a legal name
# need to return both the subsets of type 1/2 npis that were used so that
Expand Down
Loading