Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOMED Hydrating, Filtering, Special MR Generation #1648

Draft
wants to merge 19 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
feat(snomed): procedures, medications, conditions good
Refs: #1442
  • Loading branch information
jonahkaye committed Mar 28, 2024
commit 4df103b293db9ed80a57c8cd9585dd0cdaa10dd0
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Location,
Medication,
MedicationStatement,
MedicationAdministration,
Observation,
Organization,
Patient,
Expand Down Expand Up @@ -38,6 +39,7 @@ export const bundleToHtml = (fhirBundle: Bundle): string => {
patient,
medications,
medicationStatements,

conditions,
allergies,
procedures,
Expand Down Expand Up @@ -219,7 +221,6 @@ export const bundleToHtml = (fhirBundle: Bundle): string => {
<div class="divider"></div>
<div id="mr-sections">
${createMedicationSection(medications, medicationStatements)}

${createConditionSection(conditions, encounters)}
${createAllergySection(allergies)}
${createProcedureSection(procedures)}
Expand All @@ -241,6 +242,7 @@ function extractFhirTypesFromBundle(bundle: Bundle): {
practitioners: Practitioner[];
medications: Medication[];
medicationStatements: MedicationStatement[];
medicationAdministrations: MedicationAdministration[];
conditions: Condition[];
allergies: AllergyIntolerance[];
locations: Location[];
Expand Down Expand Up @@ -277,6 +279,7 @@ function extractFhirTypesFromBundle(bundle: Bundle): {
const tasks: Task[] = [];
const coverages: Coverage[] = [];
const organizations: Organization[] = [];
const medicationAdministrations: MedicationAdministration[] = [];

if (bundle.entry) {
for (const entry of bundle.entry) {
Expand All @@ -285,6 +288,8 @@ function extractFhirTypesFromBundle(bundle: Bundle): {
patient = resource as Patient;
} else if (resource?.resourceType === "MedicationStatement") {
medicationStatements.push(resource as MedicationStatement);
} else if (resource?.resourceType === "MedicationAdministration") {
medicationAdministrations.push(resource as MedicationAdministration);
} else if (resource?.resourceType === "Medication") {
medications.push(resource as Medication);
} else if (resource?.resourceType === "Condition") {
Expand Down Expand Up @@ -345,6 +350,7 @@ function extractFhirTypesFromBundle(bundle: Bundle): {
diagnosticReports,
medications,
medicationStatements,
medicationAdministrations,
conditions,
allergies,
locations,
Expand Down
161 changes: 105 additions & 56 deletions packages/utils/src/terminology-server/snomed-dedup-and-filter.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add documentation explaining what it does and how to use it, please.

Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import fs from "fs";
import path from "path";
import { getCodeDisplay, getCodeDetailsFull } from "./term-server-api";
import { populateHashTableFromCodeDetails, SnomedHierarchyTableEntry } from "./snomed-heirarchies";
import {
prettyPrintHashTable,
RemovalStats,
createInitialRemovalStats,
prettyPrintRemovalStats,
} from "./stats";
import { RemovalStats, createInitialRemovalStats, prettyPrintRemovalStats } from "./stats";

async function processDirectoryOrFile(
directoryOrFile: string,
Expand All @@ -34,8 +29,7 @@ async function processDirectoryOrFile(

async function computeHashTable(
filePath: string,
hashTable: Record<string, SnomedHierarchyTableEntry>,
conditionIdsDictionary: Record<string, Set<string>>
hashTable: Record<string, SnomedHierarchyTableEntry>
) {
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
const entries = data.bundle ? data.bundle.entry : data.entry;
Expand All @@ -50,7 +44,6 @@ async function computeHashTable(
if (codeDetails) {
await populateHashTableFromCodeDetails(
hashTable,
conditionIdsDictionary,
codeDetails,
coding.code,
resource.id
Expand All @@ -62,9 +55,46 @@ async function computeHashTable(
}
}

async function processFileEntries(
async function removeNonRootSnomedCodes(
filePath: string,
hashTable: Record<string, SnomedHierarchyTableEntry>,
conditionIdsDictionary: Set<string>,
allRemainingEnries: Set<string>,
removalStats: RemovalStats
) {
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
const entries = data.bundle ? data.bundle.entry : data.entry;

for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
const resource = entry.resource;
if (resource && resource.resourceType === "Condition") {
conditionIdsDictionary.add(resource.id);
const codings = resource.code?.coding || [];
for (const coding of codings) {
if (coding.system === "http:https://snomed.info/sct") {
if (hashTable[coding.code] && !hashTable[coding.code].root) {
removalStats.nonSnomedRootCodes.count += 1;
removalStats.nonSnomedRootCodes.codes.add(coding.code);
console.log("Removing non-root SNOMED code:", coding.code, "Resource.id:", resource.id);
entries.splice(i, 1);
break;
} else {
console.log("Keeping SNOMED code:", coding.code, "Resource.id:", resource.id);
hashTable[coding.code].inserted = true;
allRemainingEnries.add(resource.id);
break;
}
}
}
}
}
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
}

async function removeConditionsProceduresMedAdmins(
filePath: string,
conditionSet: Set<string>,
cptSet: Set<string>,
medicationConditionDict: Record<string, string>,
allRemainingEnries: Set<string>,
Expand All @@ -82,29 +112,22 @@ async function processFileEntries(

for (const coding of codings) {
if (coding.system === "http:https://snomed.info/sct") {
hasSnomedCode = true;
if (hashTable[coding.code] && hashTable[coding.code].inserted) {
if (conditionSet.has(coding.code)) {
removalStats.duplicateCodes.count += 1;
removalStats.duplicateCodes.codes.add(coding.code);
entries.splice(i, 1);
break;
} else if (hashTable[coding.code] && !hashTable[coding.code].root) {
removalStats.nonSnomedRootCodes.count += 1;
removalStats.nonSnomedRootCodes.codes.add(coding.code);
entries.splice(i, 1);
break;
} else {
conditionSet.add(coding.code);
const codeDetails = await getCodeDisplay(coding.code, "SNOMEDCT_US");
if (codeDetails && codeDetails.category === "disorder") {
hashTable[coding.code].inserted = true;
hasSnomedCode = true;
const updatedText = `${codeDetails.display} (${codeDetails.category})`;
resource.code.text = updatedText;
coding.text = updatedText;
allRemainingEnries.add(resource.id);
break;
} else {
removalStats.nonDisorderCodes.count += 1;
removalStats.nonDisorderCodes.codes.add(coding.code);
entries.splice(i, 1);
break;
}
}
Expand All @@ -122,20 +145,27 @@ async function processFileEntries(
if (coding.system === "http:https://www.ama-assn.org/go/cpt") {
const cptCode = parseInt(coding.code, 10);
if (cptCode >= 10004 && cptCode <= 69990) {
hasValidCptCode = true;
if (cptSet.has(coding.code)) {
removalStats.duplicateCptCodes.count += 1;
removalStats.duplicateCptCodes.codes.add(coding.code);
entries.splice(i, 1);
break;
} else {
hasValidCptCode = true;
console.log(
"Keeping CPT code:",
coding.code,
"Resource.id:",
resource.id,
"hasValidCptCode:",
hasValidCptCode
);
cptSet.add(coding.code);
allRemainingEnries.add(resource.id);
break;
}
break;
} else {
removalStats.invalidCptCodes.count += 1;
removalStats.invalidCptCodes.codes.add(coding.code);
entries.splice(i, 1);
break;
}
}
Expand All @@ -144,17 +174,15 @@ async function processFileEntries(
removalStats.entriesWithoutCptCodes.count += 1;
entries.splice(i, 1);
}
} else if (
resource &&
resource.resourceType === "MedicationAdministration" &&
resource.reasonReference
) {
const medicationRef = resource.medicationReference?.reference.split("/")[1];
const conditionRef = resource.reasonReference[0]?.reference.split("/")[1];
} else if (resource && resource.resourceType === "MedicationAdministration") {
if (resource.reasonReference) {
const medicationRef = resource.medicationReference?.reference.split("/")[1];
const conditionRef = resource.reasonReference[0]?.reference.split("/")[1];

if (medicationRef && conditionRef) {
medicationConditionDict[medicationRef] = conditionRef;
allRemainingEnries.add(resource.id);
if (medicationRef && conditionRef) {
medicationConditionDict[medicationRef] = conditionRef;
allRemainingEnries.add(resource.id);
}
} else {
console.log("Removing MedicationAdministration entry without condition reference");
entries.splice(i, 1);
Expand All @@ -166,7 +194,7 @@ async function processFileEntries(

async function filterMedicationsEntries(
filePath: string,
allRemainingEnries: Set<string>,
conditionIdsDictionary: Set<string>,
medicationConditionDict: Record<string, string>,
rxNormSet: Set<string>,
removalStats: RemovalStats
Expand All @@ -179,7 +207,9 @@ async function filterMedicationsEntries(
const resource = entry.resource;
if (resource && resource.resourceType === "Medication") {
const conditionRef = medicationConditionDict[resource.id];
if (allRemainingEnries.has(conditionRef)) {

// if the medication is linked to a conditon that is a non-duplicate disorder
if (conditionIdsDictionary.has(conditionRef)) {
const codings = resource.code?.coding || [];
for (const coding of codings) {
if (coding.system === "http:https://www.nlm.nih.gov/research/umls/rxnorm") {
Expand All @@ -189,7 +219,14 @@ async function filterMedicationsEntries(
entries.splice(i, 1);
} else {
rxNormSet.add(coding.code);
allRemainingEnries.add(resource.id);
console.log(
"Keeping Medication. RXNORM code:",
coding.code,
"linked to condition:",
conditionRef,
"Resource.id:",
resource.id
);
}
}
}
Expand All @@ -208,42 +245,54 @@ async function main() {
console.error("Please provide a directory path as an argument.");
process.exit(1);
}
const hashTable: Record<string, SnomedHierarchyTableEntry> = {};
const conditionIdsDictionary: Record<string, Set<string>> = {};

await processDirectoryOrFile(directoryPath, async filePath => {
await computeHashTable(filePath, hashTable, conditionIdsDictionary);
});

const filteredConditionIdsDictionary = Object.fromEntries(
Object.entries(conditionIdsDictionary).filter(([, value]) => value.size > 0)
);
console.log("conditionIdsDictionary", filteredConditionIdsDictionary);

prettyPrintHashTable(hashTable);
const removalStats = createInitialRemovalStats();

// Removal of non-disorder SNOMED codes, invalid CPT codes, and duplicate CPT codes
const cptSet = new Set<string>();
const medicationConditionDict: Record<string, string> = {};

const conditionsSet = new Set<string>();
const allRemainingEnries = new Set<string>();
const medicationConditionDict: Record<string, string> = {}; // a mapping of all medications to conditions
await processDirectoryOrFile(directoryPath, async filePath => {
await processFileEntries(
await removeConditionsProceduresMedAdmins(
filePath,
hashTable,
conditionsSet,
cptSet,
medicationConditionDict,
allRemainingEnries,
removalStats
);
});

const rxNormSet = new Set<string>();
prettyPrintRemovalStats(removalStats);

// Create hierarchy of SNOMED codes
const hashTable: Record<string, SnomedHierarchyTableEntry> = {};
await processDirectoryOrFile(directoryPath, async filePath => {
await filterMedicationsEntries(
await computeHashTable(filePath, hashTable);
});

// Remove non-root SNOMED codes
const conditionIdsDictionary = new Set<string>();
await processDirectoryOrFile(directoryPath, async filePath => {
await removeNonRootSnomedCodes(
filePath,
hashTable,
allRemainingEnries,
conditionIdsDictionary,
removalStats
);
});

console.log("conditionIdsDictionary", conditionIdsDictionary);
console.log("medicationConditionDict", medicationConditionDict);

// Removal of duplicate RXNORM codes and medications without condition reference
const rxNormSet = new Set<string>();
await processDirectoryOrFile(directoryPath, async filePath => {
await filterMedicationsEntries(
filePath,
conditionIdsDictionary,
medicationConditionDict,
rxNormSet,
removalStats
Expand Down
Loading
Loading