Skip to content

Commit 2cc0a62

Browse files
authored
Merge pull request #1521 from synthetichealth/icd_code_mapping
ICD-10-CM Code Mapping
2 parents e2ca05d + b9dec27 commit 2cc0a62

File tree

8 files changed

+175
-14
lines changed

8 files changed

+175
-14
lines changed

src/main/java/App.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,14 +280,15 @@ public static void main(String[] args) throws Exception {
280280
* Reset the fields of the provided options to the current values in the Config.
281281
*/
282282
private static void resetOptionsFromConfig(Generator.GeneratorOptions options,
283-
Exporter.ExporterRuntimeOptions exportOptions) {
284-
// Any options that are automatically set by reading the configuration
285-
// file during options initialization need to be reset here.
286-
options.population = Config.getAsInteger("generate.default_population", 1);
287-
options.threadPoolSize = Config.getAsInteger("generate.thread_pool_size", -1);
283+
Exporter.ExporterRuntimeOptions exportOptions) {
284+
// Any options that are automatically set by reading the configuration
285+
// file during options initialization need to be reset here.
286+
options.population = Config.getAsInteger("generate.default_population", 1);
287+
options.threadPoolSize = Config.getAsInteger("generate.thread_pool_size", -1);
288288

289-
exportOptions.yearsOfHistory = Config.getAsInteger("exporter.years_of_history", 10);
290-
exportOptions.terminologyService = !Config.get("generate.terminology_service_url", "").isEmpty();
289+
exportOptions.yearsOfHistory = Config.getAsInteger("exporter.years_of_history", 10);
290+
exportOptions.terminologyService = !Config.get("generate.terminology_service_url", "")
291+
.isEmpty();
291292
}
292293

293294
private static boolean validateConfig(Generator.GeneratorOptions options,

src/main/java/org/mitre/synthea/engine/Generator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ private void init() {
218218
CDWExporter.getInstance().setKeyStart((stateIndex * 1_000_000) + 1);
219219
}
220220
Exporter.loadCustomExporters();
221+
Exporter.loadCodeMappers();
221222

222223
this.populationRandom = new DefaultRandomNumberGenerator(options.seed);
223224
this.clinicianRandom = new DefaultRandomNumberGenerator(options.clinicianSeed);

src/main/java/org/mitre/synthea/export/Exporter.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@
1414
import java.nio.file.StandardOpenOption;
1515
import java.util.ArrayList;
1616
import java.util.Collections;
17+
import java.util.HashMap;
1718
import java.util.Iterator;
1819
import java.util.LinkedList;
1920
import java.util.List;
21+
import java.util.Map;
2022
import java.util.ServiceLoader;
2123
import java.util.concurrent.BlockingQueue;
2224
import java.util.concurrent.ConcurrentHashMap;
2325
import java.util.concurrent.LinkedBlockingQueue;
2426
import java.util.function.Predicate;
27+
import java.util.stream.Collectors;
2528

2629
import org.apache.commons.lang3.tuple.ImmutablePair;
2730
import org.apache.commons.lang3.tuple.Pair;
@@ -34,6 +37,7 @@
3437
import org.mitre.synthea.export.flexporter.FlexporterJavascriptContext;
3538
import org.mitre.synthea.export.flexporter.Mapping;
3639
import org.mitre.synthea.export.rif.BB2RIFExporter;
40+
import org.mitre.synthea.export.rif.CodeMapper;
3741
import org.mitre.synthea.helpers.Config;
3842
import org.mitre.synthea.helpers.TransitionMetrics;
3943
import org.mitre.synthea.helpers.Utilities;
@@ -69,6 +73,7 @@ public enum SupportedFhirVersion {
6973

7074
private static List<PatientExporter> patientExporters;
7175
private static List<PostCompletionExporter> postCompletionExporters;
76+
private static Map<String, CodeMapper> codeMappers;
7277

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

103+
/**
104+
* Load any configured code mappers. Code mappers are configured via the
105+
* synthea.properties file and a sample configuration is shown below:
106+
* <pre>
107+
* exporter.code_map.icd_10=export/anti_amyloid_code_map.json
108+
* exporter.code_map.cpt=export/phlebotomy_code_map.json,export/neurology_code_map.json
109+
* </pre>
110+
* The above define a single code map for ICD-10 codes and two code maps for CPT codes.
111+
*/
112+
public static void loadCodeMappers() {
113+
codeMappers = new HashMap<String, CodeMapper>();
114+
List<String> codeSystemProperties = Config.allPropertyNames()
115+
.stream()
116+
.filter((key) -> key.startsWith("exporter.code_map"))
117+
.collect(Collectors.toList());
118+
codeSystemProperties.forEach(codeSystemProperty -> {
119+
String codeSystem = codeSystemProperty.strip().replace(
120+
"exporter.code_map.", "").toUpperCase();
121+
String[] resources = Config.get(codeSystemProperty).split(",");
122+
for (String resource: resources) {
123+
CodeMapper mapper = new CodeMapper(resource);
124+
if (codeMappers.containsKey(codeSystem)) {
125+
codeMappers.get(codeSystem).merge(mapper);
126+
} else {
127+
codeMappers.put(codeSystem, mapper);
128+
}
129+
}
130+
});
131+
}
132+
133+
/**
134+
* Get the code mapper for the supplied code system.
135+
* @param codeSystem the code system
136+
* @return the corresponding code mapper or null if none configured
137+
*/
138+
public static CodeMapper getCodeMapper(String codeSystem) {
139+
return codeMappers.get(codeSystem);
140+
}
141+
98142
/**
99143
* Runtime configuration of the record exporter.
100144
*/

src/main/java/org/mitre/synthea/export/FhirR4.java

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import java.awt.geom.Point2D;
1212
import java.io.IOException;
13+
import java.util.AbstractMap;
1314
import java.util.ArrayList;
1415
import java.util.Arrays;
1516
import java.util.Calendar;
@@ -139,7 +140,10 @@
139140

140141
import org.mitre.synthea.engine.Components;
141142
import org.mitre.synthea.engine.Components.Attachment;
143+
import org.mitre.synthea.export.rif.CodeMapper;
142144
import org.mitre.synthea.helpers.Config;
145+
import org.mitre.synthea.helpers.RandomNumberGenerator;
146+
import org.mitre.synthea.helpers.RandomValueGenerator;
143147
import org.mitre.synthea.helpers.SimpleCSV;
144148
import org.mitre.synthea.helpers.Utilities;
145149
import org.mitre.synthea.identity.Entity;
@@ -404,7 +408,7 @@ public static Bundle convertToFHIR(Person person, long stopTime) {
404408

405409
if (shouldExport(Condition.class)) {
406410
for (HealthRecord.Entry condition : encounter.conditions) {
407-
condition(personEntry, bundle, encounterEntry, condition);
411+
condition(person, personEntry, bundle, encounterEntry, condition);
408412
}
409413
}
410414

@@ -431,7 +435,7 @@ public static Bundle convertToFHIR(Person person, long stopTime) {
431435

432436
if (shouldExport(org.hl7.fhir.r4.model.Procedure.class)) {
433437
for (Procedure procedure : encounter.procedures) {
434-
procedure(personEntry, bundle, encounterEntry, procedure);
438+
procedure(person, personEntry, bundle, encounterEntry, procedure);
435439
}
436440
}
437441

@@ -849,6 +853,27 @@ private static BundleEntryComponent basicInfo(Person person, Bundle bundle, long
849853
return newEntry(bundle, patientResource, (String) person.attributes.get(Person.ID));
850854
}
851855

856+
/**
857+
* Add a code translation (if available) of the supplied source code to the
858+
* supplied CodeableConcept.
859+
* @param codeSystem the code system of the translated code
860+
* @param from the source code
861+
* @param to the CodeableConcept to add the translation to
862+
* @param rand a source of randomness
863+
*/
864+
private static void addTranslation(String codeSystem, Code from,
865+
CodeableConcept to, RandomNumberGenerator rand) {
866+
CodeMapper mapper = Exporter.getCodeMapper(codeSystem);
867+
if (mapper != null && mapper.canMap(from)) {
868+
Coding coding = new Coding();
869+
Map.Entry<String, String> mappedCode = mapper.mapToCodeAndDescription(from, rand);
870+
coding.setCode(mappedCode.getKey());
871+
coding.setDisplay(mappedCode.getValue());
872+
coding.setSystem(ExportHelper.getSystemURI("ICD10-CM"));
873+
to.addCoding(coding);
874+
}
875+
}
876+
852877
/**
853878
* Map the given Encounter into a FHIR Encounter resource, and add it to the given Bundle.
854879
*
@@ -894,6 +919,8 @@ private static BundleEntryComponent encounter(Person person, BundleEntryComponen
894919
if (encounter.reason != null) {
895920
encounterResource.addReasonCode().addCoding().setCode(encounter.reason.code)
896921
.setDisplay(encounter.reason.display).setSystem(SNOMED_URI);
922+
addTranslation("ICD10-CM", encounter.reason,
923+
encounterResource.getReasonCodeFirstRep(), person);
897924
}
898925

899926
Provider provider = encounter.provider;
@@ -1607,6 +1634,7 @@ private static BundleEntryComponent explanationOfBenefit(BundleEntryComponent pe
16071634
* @return The added Entry
16081635
*/
16091636
private static BundleEntryComponent condition(
1637+
RandomNumberGenerator rand,
16101638
BundleEntryComponent personEntry, Bundle bundle, BundleEntryComponent encounterEntry,
16111639
HealthRecord.Entry condition) {
16121640
Condition conditionResource = new Condition();
@@ -1630,7 +1658,9 @@ private static BundleEntryComponent condition(
16301658
conditionResource.setEncounter(new Reference(encounterEntry.getFullUrl()));
16311659

16321660
Code code = condition.codes.get(0);
1633-
conditionResource.setCode(mapCodeToCodeableConcept(code, SNOMED_URI));
1661+
CodeableConcept concept = mapCodeToCodeableConcept(code, SNOMED_URI);
1662+
addTranslation("ICD10-CM", code, concept, rand);
1663+
conditionResource.setCode(concept);
16341664

16351665
CodeableConcept verification = new CodeableConcept();
16361666
verification.getCodingFirstRep()
@@ -1964,13 +1994,14 @@ static org.hl7.fhir.r4.model.SampledData mapValueToSampledData(
19641994
/**
19651995
* Map the given Procedure into a FHIR Procedure resource, and add it to the given Bundle.
19661996
*
1997+
* @param person The Person
19671998
* @param personEntry The Person entry
19681999
* @param bundle Bundle to add to
19692000
* @param encounterEntry The current Encounter entry
19702001
* @param procedure The Procedure
19712002
* @return The added Entry
19722003
*/
1973-
private static BundleEntryComponent procedure(
2004+
private static BundleEntryComponent procedure(Person person,
19742005
BundleEntryComponent personEntry, Bundle bundle, BundleEntryComponent encounterEntry,
19752006
Procedure procedure) {
19762007
org.hl7.fhir.r4.model.Procedure procedureResource = new org.hl7.fhir.r4.model.Procedure();
@@ -2013,6 +2044,7 @@ private static BundleEntryComponent procedure(
20132044
// we didn't find a matching Condition,
20142045
// fallback to just reason code
20152046
procedureResource.addReasonCode(mapCodeToCodeableConcept(reason, SNOMED_URI));
2047+
addTranslation("ICD10-CM", reason, procedureResource.getReasonCodeFirstRep(), person);
20162048
}
20172049
}
20182050

@@ -2322,6 +2354,8 @@ && shouldExport(org.hl7.fhir.r4.model.Medication.class)) {
23222354
// we didn't find a matching Condition,
23232355
// fallback to just reason code
23242356
medicationResource.addReasonCode(mapCodeToCodeableConcept(reason, SNOMED_URI));
2357+
addTranslation("ICD10-CM", reason, medicationResource.getReasonCodeFirstRep(),
2358+
person);
23252359
}
23262360
}
23272361

@@ -2474,6 +2508,8 @@ private static BundleEntryComponent medicationAdministration(
24742508
// we didn't find a matching Condition,
24752509
// fallback to just reason code
24762510
medicationResource.addReasonCode(mapCodeToCodeableConcept(reason, SNOMED_URI));
2511+
addTranslation("ICD10-CM", reason, medicationResource.getReasonCodeFirstRep(),
2512+
person);
24772513
}
24782514
}
24792515

@@ -2717,6 +2753,8 @@ private static BundleEntryComponent carePlan(Person person,
27172753
activityDetailComponent.addReasonReference().setReference(reasonCondition.getFullUrl());
27182754
} else if (reason != null) {
27192755
activityDetailComponent.addReasonCode(mapCodeToCodeableConcept(reason, SNOMED_URI));
2756+
addTranslation("ICD10-CM", reason, activityDetailComponent.getReasonCodeFirstRep(),
2757+
person);
27202758
}
27212759

27222760
activityComponent.setDetail(activityDetailComponent);
@@ -2863,7 +2901,9 @@ private static BundleEntryComponent careTeam(Person person,
28632901

28642902
if (carePlan.reasons != null && !carePlan.reasons.isEmpty()) {
28652903
for (Code code : carePlan.reasons) {
2866-
careTeam.addReasonCode(mapCodeToCodeableConcept(code, SNOMED_URI));
2904+
CodeableConcept concept = mapCodeToCodeableConcept(code, SNOMED_URI);
2905+
addTranslation("ICD10-CM", code, concept, person);
2906+
careTeam.addReasonCode(concept);
28672907
}
28682908
}
28692909

src/main/java/org/mitre/synthea/export/rif/CodeMapper.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.google.gson.reflect.TypeToken;
66
import java.io.IOException;
77
import java.lang.reflect.Type;
8+
import java.util.AbstractMap.SimpleEntry;
89
import java.util.ArrayList;
910
import java.util.Collections;
1011
import java.util.HashMap;
@@ -86,6 +87,26 @@ public CodeMapper(String jsonMapResource) {
8687
}
8788
}
8889

90+
/**
91+
* Merge the mappings from the supplied code mapper. This method performs a deep
92+
* merge such that destination codes will be merged if this and other contain
93+
* mappings for the same source code. Care is required with the use of weights
94+
* such that this and other use the same weighting scale (e.g. 0-1) otherwise one
95+
* set of mappings could overwhelm the other.
96+
* @param other the other code mapper from which mappings will be copied.
97+
*/
98+
public void merge(CodeMapper other) {
99+
if (other.hasMap()) {
100+
other.map.forEach((source, mapped) -> {
101+
if (map.containsKey(source)) {
102+
map.get(source).addAll(mapped);
103+
} else {
104+
map.put(source, mapped);
105+
}
106+
});
107+
}
108+
}
109+
89110
/**
90111
* Determines whether this mapper has an entry for the supplied code.
91112
* @param codeToMap the Synthea code to look for
@@ -249,6 +270,24 @@ public String map(Code codeToMap, String bfdCodeType, RandomNumberGenerator rand
249270
return map(codeToMap.code, bfdCodeType, rand, stripDots);
250271
}
251272

273+
/**
274+
* Get one of the BFD codes for the supplied Synthea code. Equivalent to
275+
* {@code map(codeToMap, "code", rand)}.
276+
* @param codeToMap the Synthea code to look for
277+
* @param rand a source of random numbers used to pick one of the list of BFD codes
278+
* @return the BFD code and display string or null if the code can't be mapped
279+
*/
280+
public SimpleEntry<String, String> mapToCodeAndDescription(Code codeToMap,
281+
RandomNumberGenerator rand) {
282+
if (!canMap(codeToMap)) {
283+
return null;
284+
}
285+
RandomCollection<Map<String, String>> options = map.get(codeToMap.code);
286+
Map<String, String> mappedCode = options.next(rand);
287+
return new SimpleEntry<>(mappedCode.get("code"),
288+
mappedCode.get("description"));
289+
}
290+
252291
/**
253292
* Get the missing code as a list of maps, where each map includes the mapper name, a missing
254293
* code, a description, and the count of times the code was requested.

src/main/java/org/mitre/synthea/helpers/RandomCollection.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22

33
import java.io.Serializable;
44
import java.util.Map.Entry;
5-
import java.util.NavigableMap;
65
import java.util.TreeMap;
76

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

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

30+
/**
31+
* Add all of the entries from the supplied RandomCollection.
32+
* @param other the collection from which to copy entries.
33+
*/
34+
public void addAll(RandomCollection<E> other) {
35+
Double weightAdj = 0.0;
36+
for (Entry<Double, E> e: other.map.entrySet()) {
37+
add(e.getKey() - weightAdj, e.getValue());
38+
weightAdj = e.getKey();
39+
}
40+
}
41+
3142
/**
3243
* Select an item from the collection at random by the weight of the items.
3344
* Selecting an item from one draw, does not remove the item from the collection

src/main/resources/synthea.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ exporter.symptoms.text.export = false
9898
# enable searching for custom exporter implementations
9999
exporter.enable_custom_exporters = true
100100

101+
# enable mapping of Synthea-native code systems to others
102+
# each key is for a specific code system and the values are comma separated paths to
103+
# JSON mapping files
104+
# see the CodeMapper class for code mapping file format and functionality
105+
#exporter.code_map.icd10-cm=export/anti_amyloid_code_map.json
106+
101107
# the number of patients to generate, by default
102108
# this can be overridden by passing a different value to the Generator constructor
103109
generate.default_population = 1

0 commit comments

Comments
 (0)