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

ICD-10-CM Code Mapping #1521

Merged
merged 2 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 8 additions & 7 deletions src/main/java/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,15 @@ public static void main(String[] args) throws Exception {
* Reset the fields of the provided options to the current values in the Config.
*/
private static void resetOptionsFromConfig(Generator.GeneratorOptions options,
Exporter.ExporterRuntimeOptions exportOptions) {
// Any options that are automatically set by reading the configuration
// file during options initialization need to be reset here.
options.population = Config.getAsInteger("generate.default_population", 1);
options.threadPoolSize = Config.getAsInteger("generate.thread_pool_size", -1);
Exporter.ExporterRuntimeOptions exportOptions) {
// Any options that are automatically set by reading the configuration
// file during options initialization need to be reset here.
options.population = Config.getAsInteger("generate.default_population", 1);
options.threadPoolSize = Config.getAsInteger("generate.thread_pool_size", -1);

exportOptions.yearsOfHistory = Config.getAsInteger("exporter.years_of_history", 10);
exportOptions.terminologyService = !Config.get("generate.terminology_service_url", "").isEmpty();
exportOptions.yearsOfHistory = Config.getAsInteger("exporter.years_of_history", 10);
exportOptions.terminologyService = !Config.get("generate.terminology_service_url", "")
.isEmpty();
}

private static boolean validateConfig(Generator.GeneratorOptions options,
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/mitre/synthea/engine/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ private void init() {
CDWExporter.getInstance().setKeyStart((stateIndex * 1_000_000) + 1);
}
Exporter.loadCustomExporters();
Exporter.loadCodeMappers();

this.populationRandom = new DefaultRandomNumberGenerator(options.seed);
this.clinicianRandom = new DefaultRandomNumberGenerator(options.clinicianSeed);
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/org/mitre/synthea/export/Exporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
Expand All @@ -34,6 +37,7 @@
import org.mitre.synthea.export.flexporter.FlexporterJavascriptContext;
import org.mitre.synthea.export.flexporter.Mapping;
import org.mitre.synthea.export.rif.BB2RIFExporter;
import org.mitre.synthea.export.rif.CodeMapper;
import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.helpers.TransitionMetrics;
import org.mitre.synthea.helpers.Utilities;
Expand Down Expand Up @@ -69,6 +73,7 @@ public enum SupportedFhirVersion {

private static List<PatientExporter> patientExporters;
private static List<PostCompletionExporter> postCompletionExporters;
private static Map<String, CodeMapper> codeMappers;

/**
* If the config setting "exporter.enable_custom_exporters" is enabled,
Expand All @@ -95,6 +100,45 @@ public static void loadCustomExporters() {
}
}

/**
* Load any configured code mappers. Code mappers are configured via the
* synthea.properties file and a sample configuration is shown below:
* <pre>
* exporter.code_map.icd_10=export/anti_amyloid_code_map.json
* exporter.code_map.cpt=export/phlebotomy_code_map.json,export/neurology_code_map.json
* </pre>
* The above define a single code map for ICD-10 codes and two code maps for CPT codes.
*/
public static void loadCodeMappers() {
codeMappers = new HashMap<String, CodeMapper>();
List<String> codeSystemProperties = Config.allPropertyNames()
.stream()
.filter((key) -> key.startsWith("exporter.code_map"))
.collect(Collectors.toList());
codeSystemProperties.forEach(codeSystemProperty -> {
String codeSystem = codeSystemProperty.strip().replace(
"exporter.code_map.", "").toUpperCase();
String[] resources = Config.get(codeSystemProperty).split(",");
for (String resource: resources) {
CodeMapper mapper = new CodeMapper(resource);
if (codeMappers.containsKey(codeSystem)) {
codeMappers.get(codeSystem).merge(mapper);
} else {
codeMappers.put(codeSystem, mapper);
}
}
});
}

/**
* Get the code mapper for the supplied code system.
* @param codeSystem the code system
* @return the corresponding code mapper or null if none configured
*/
public static CodeMapper getCodeMapper(String codeSystem) {
return codeMappers.get(codeSystem);
}

/**
* Runtime configuration of the record exporter.
*/
Expand Down
29 changes: 27 additions & 2 deletions src/main/java/org/mitre/synthea/export/FhirR4.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
Expand Down Expand Up @@ -139,7 +140,10 @@

import org.mitre.synthea.engine.Components;
import org.mitre.synthea.engine.Components.Attachment;
import org.mitre.synthea.export.rif.CodeMapper;
import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.helpers.RandomNumberGenerator;
import org.mitre.synthea.helpers.RandomValueGenerator;
import org.mitre.synthea.helpers.SimpleCSV;
import org.mitre.synthea.helpers.Utilities;
import org.mitre.synthea.identity.Entity;
Expand Down Expand Up @@ -404,7 +408,7 @@ public static Bundle convertToFHIR(Person person, long stopTime) {

if (shouldExport(Condition.class)) {
for (HealthRecord.Entry condition : encounter.conditions) {
condition(personEntry, bundle, encounterEntry, condition);
condition(person, personEntry, bundle, encounterEntry, condition);
}
}

Expand Down Expand Up @@ -894,6 +898,16 @@ private static BundleEntryComponent encounter(Person person, BundleEntryComponen
if (encounter.reason != null) {
encounterResource.addReasonCode().addCoding().setCode(encounter.reason.code)
.setDisplay(encounter.reason.display).setSystem(SNOMED_URI);
CodeMapper mapper = Exporter.getCodeMapper("ICD10-CM");
if (mapper != null && mapper.canMap(encounter.reason.code)) {
Coding coding = new Coding();
Map.Entry<String, String> mappedCode = mapper.mapToCodeAndDescription(
encounter.reason, person);
coding.setCode(mappedCode.getKey());
coding.setDisplay(mappedCode.getValue());
coding.setSystem(ExportHelper.getSystemURI("ICD10-CM"));
encounterResource.getReasonCodeFirstRep().addCoding(coding);
}
}

Provider provider = encounter.provider;
Expand Down Expand Up @@ -1607,6 +1621,7 @@ private static BundleEntryComponent explanationOfBenefit(BundleEntryComponent pe
* @return The added Entry
*/
private static BundleEntryComponent condition(
RandomNumberGenerator rand,
BundleEntryComponent personEntry, Bundle bundle, BundleEntryComponent encounterEntry,
HealthRecord.Entry condition) {
Condition conditionResource = new Condition();
Expand All @@ -1630,7 +1645,17 @@ private static BundleEntryComponent condition(
conditionResource.setEncounter(new Reference(encounterEntry.getFullUrl()));

Code code = condition.codes.get(0);
conditionResource.setCode(mapCodeToCodeableConcept(code, SNOMED_URI));
CodeableConcept concept = mapCodeToCodeableConcept(code, SNOMED_URI);
CodeMapper mapper = Exporter.getCodeMapper("ICD10-CM");
if (mapper != null && mapper.canMap(code)) {
Coding coding = new Coding();
Map.Entry<String, String> mappedCode = mapper.mapToCodeAndDescription(code, rand);
coding.setCode(mappedCode.getKey());
coding.setDisplay(mappedCode.getValue());
coding.setSystem(ExportHelper.getSystemURI("ICD10-CM"));
concept.addCoding(coding);
}
conditionResource.setCode(concept);

CodeableConcept verification = new CodeableConcept();
verification.getCodingFirstRep()
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/org/mitre/synthea/export/rif/CodeMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -86,6 +87,26 @@ public CodeMapper(String jsonMapResource) {
}
}

/**
* Merge the mappings from the supplied code mapper. This method performs a deep
* merge such that destination codes will be merged if this and other contain
* mappings for the same source code. Care is required with the use of weights
* such that this and other use the same weighting scale (e.g. 0-1) otherwise one
* set of mappings could overwhelm the other.
* @param other the other code mapper from which mappings will be copied.
*/
public void merge(CodeMapper other) {
if (other.hasMap()) {
other.map.forEach((source, mapped) -> {
if (map.containsKey(source)) {
map.get(source).addAll(mapped);
} else {
map.put(source, mapped);
}
});
}
}

/**
* Determines whether this mapper has an entry for the supplied code.
* @param codeToMap the Synthea code to look for
Expand Down Expand Up @@ -249,6 +270,24 @@ public String map(Code codeToMap, String bfdCodeType, RandomNumberGenerator rand
return map(codeToMap.code, bfdCodeType, rand, stripDots);
}

/**
* Get one of the BFD codes for the supplied Synthea code. Equivalent to
* {@code map(codeToMap, "code", rand)}.
* @param codeToMap the Synthea code to look for
* @param rand a source of random numbers used to pick one of the list of BFD codes
* @return the BFD code and display string or null if the code can't be mapped
*/
public SimpleEntry<String, String> mapToCodeAndDescription(Code codeToMap,
RandomNumberGenerator rand) {
if (!canMap(codeToMap)) {
return null;
}
RandomCollection<Map<String, String>> options = map.get(codeToMap.code);
Map<String, String> mappedCode = options.next(rand);
return new SimpleEntry<>(mappedCode.get("code"),
mappedCode.get("description"));
}

/**
* Get the missing code as a list of maps, where each map includes the mapper name, a missing
* code, a description, and the count of times the code was requested.
Expand Down
15 changes: 13 additions & 2 deletions src/main/java/org/mitre/synthea/helpers/RandomCollection.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@

import java.io.Serializable;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.TreeMap;

/**
* Random collection of objects, with weightings. Intended to be an equivalent to the ruby Pickup
* gem. Adapted from https://stackoverflow.com/a/6409791/630384
*/
public class RandomCollection<E> implements Serializable {
private final NavigableMap<Double, E> map = new TreeMap<Double, E>();
private final TreeMap<Double, E> map = new TreeMap<Double, E>();
private double total = 0;

/**
Expand All @@ -28,6 +27,18 @@ public void add(double weight, E result) {
map.put(total, result);
}

/**
* Add all of the entries from the supplied RandomCollection.
* @param other the collection from which to copy entries.
*/
public void addAll(RandomCollection<E> other) {
Double weightAdj = 0.0;
for (Entry<Double, E> e: other.map.entrySet()) {
add(e.getKey() - weightAdj, e.getValue());
weightAdj = e.getKey();
}
}

/**
* Select an item from the collection at random by the weight of the items.
* Selecting an item from one draw, does not remove the item from the collection
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/synthea.properties
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ exporter.symptoms.text.export = false
# enable searching for custom exporter implementations
exporter.enable_custom_exporters = true

# enable mapping of Synthea-native code systems to others
# each key is for a specific code system and the values are comma separated paths to
# JSON mapping files
# see the CodeMapper class for code mapping file format and functionality
#exporter.code_map.icd10-cm=export/anti_amyloid_code_map.json

# the number of patients to generate, by default
# this can be overridden by passing a different value to the Generator constructor
generate.default_population = 1
Expand Down
19 changes: 19 additions & 0 deletions src/test/java/org/mitre/synthea/export/rif/CodeMapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ public void testWeightedCodeMapper() {
assertTrue(defCount > abcCount);
}

@Test
public void testCodeMapperMerge() {
Config.set("exporter.bfd.require_code_maps", "true");
CodeMapper mapper = new CodeMapper("export/unweighted_code_map.json");
mapper.merge(new CodeMapper("export/weighted_code_map.json"));
assertTrue(mapper.canMap("10509002"));
int abcCount = 0;
int defCount = 0;
for (int i = 0; i < 10; i++) {
String code = mapper.map("10509002", random);
if (code.equals("ABC20.9")) {
abcCount++;
} else if (code.equals("DEF20.9")) {
defCount++;
}
}
assertTrue(defCount > abcCount);
}

@Test(expected = MissingResourceException.class)
public void testThrowsExceptionWhenMapFileMissing() {
Config.set("exporter.bfd.require_code_maps", "true");
Expand Down
Loading