Skip to content

Commit

Permalink
Changes associated with IDR analysis and updates.
Browse files Browse the repository at this point in the history
  • Loading branch information
jawalonoski committed Jul 23, 2024
1 parent e2cee7a commit 950416f
Show file tree
Hide file tree
Showing 10 changed files with 2,968 additions and 314 deletions.
25 changes: 22 additions & 3 deletions src/main/java/org/mitre/synthea/engine/Distribution.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/
public class Distribution implements Serializable {
public enum Kind {
EXACT, GAUSSIAN, UNIFORM, EXPONENTIAL
EXACT, GAUSSIAN, UNIFORM, EXPONENTIAL, TRIANGULAR
}

public Kind kind;
Expand Down Expand Up @@ -50,8 +50,23 @@ public double generate(Person person) {
break;
case EXPONENTIAL:
double average = this.parameters.get("mean");
double lambda = (1.0d / average);
value = 1.0d + Math.log(1.0d - person.rand()) / (-1.0d * lambda);
double lambda = (-1.0d / average);
value = 1.0d + Math.log(1.0d - person.rand()) / lambda;
break;
case TRIANGULAR:
/* Pick a single value based on a triangular distribution. See:
* https://en.wikipedia.org/wiki/Triangular_distribution
*/
double min = this.parameters.get("min");
double mode = this.parameters.get("mode");
double max = this.parameters.get("max");
double f = (mode - min) / (max - min);
double rand = person.rand();
if (rand < f) {
value = min + Math.sqrt(rand * (max - min) * (mode - min));
} else {
value = max - Math.sqrt((1 - rand) * (max - min) * (max - mode));
}
break;
default:
value = -1;
Expand Down Expand Up @@ -81,6 +96,10 @@ public boolean validate() {
&& this.parameters.containsKey("standardDeviation");
case EXPONENTIAL:
return this.parameters.containsKey("mean");
case TRIANGULAR:
return this.parameters.containsKey("min")
&& this.parameters.containsKey("mode")
&& this.parameters.containsKey("max");
default:
return false;
}
Expand Down
10 changes: 6 additions & 4 deletions src/main/java/org/mitre/synthea/export/CSVExporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -308,14 +308,14 @@ private void writeCSVHeaders() throws IOException {
"START,STOP,PATIENT,PAYER,ENCOUNTER,CODE,DESCRIPTION,BASE_COST,PAYER_COVERAGE,DISPENSES,"
+ "TOTALCOST,REASONCODE,REASONDESCRIPTION");
medications.write(NEWLINE);
conditions.write("START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION");
conditions.write("START,STOP,PATIENT,ENCOUNTER,SYSTEM,CODE,DESCRIPTION");
conditions.write(NEWLINE);
careplans.write(
"Id,START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,REASONCODE,REASONDESCRIPTION");
careplans.write(NEWLINE);
observations.write("DATE,PATIENT,ENCOUNTER,CATEGORY,CODE,DESCRIPTION,VALUE,UNITS,TYPE");
observations.write(NEWLINE);
procedures.write("START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,BASE_COST,"
procedures.write("START,STOP,PATIENT,ENCOUNTER,SYSTEM,CODE,DESCRIPTION,BASE_COST,"
+ "REASONCODE,REASONDESCRIPTION");
procedures.write(NEWLINE);
immunizations.write("DATE,PATIENT,ENCOUNTER,CODE,DESCRIPTION,BASE_COST");
Expand Down Expand Up @@ -752,7 +752,7 @@ private String encounter(String personID,
* @throws IOException if any IO error occurs
*/
private void condition(String personID, String encounterID, Entry condition) throws IOException {
// START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION
// START,STOP,PATIENT,ENCOUNTER,SYSTEM,CODE,DESCRIPTION
StringBuilder s = new StringBuilder();

s.append(dateFromTimestamp(condition.start)).append(',');
Expand All @@ -765,6 +765,7 @@ private void condition(String personID, String encounterID, Entry condition) thr

Code coding = condition.codes.get(0);

s.append(coding.system).append(',');
s.append(coding.code).append(',');
s.append(clean(coding.display));

Expand Down Expand Up @@ -903,7 +904,7 @@ private void observation(String personID,
*/
private void procedure(String personID, String encounterID,
Procedure procedure) throws IOException {
// START,STOP,PATIENT,ENCOUNTER,CODE,DESCRIPTION,COST,REASONCODE,REASONDESCRIPTION
// START,STOP,PATIENT,ENCOUNTER,SYSTEM,CODE,DESCRIPTION,COST,REASONCODE,REASONDESCRIPTION
StringBuilder s = new StringBuilder();

s.append(iso8601Timestamp(procedure.start)).append(',');
Expand All @@ -915,6 +916,7 @@ private void procedure(String personID, String encounterID,
s.append(encounterID).append(',');
// CODE
Code coding = procedure.codes.get(0);
s.append(coding.system).append(',');
s.append(coding.code).append(',');
// DESCRIPTION
s.append(clean(coding.display)).append(',');
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/mitre/synthea/export/ExportHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ public static long nextFriday(long time) {
private static final String CVX_URI = "http://hl7.org/fhir/sid/cvx";
private static final String DICOM_DCM_URI = "http://dicom.nema.org/medical/dicom/current/output/chtml/part16/sect_CID_29.html";
private static final String CDT_URI = "http://www.ada.org/cdt";
private static final String ICD9_URI = "http://hl7.org/fhir/sid/icd-9-cm";
private static final String ICD10_URI = "http://hl7.org/fhir/sid/icd-10";
private static final String ICD10_CM_URI = "http://hl7.org/fhir/sid/icd-10-cm";

/**
* Translate the system name (e.g. SNOMED-CT) into the official
Expand All @@ -234,6 +237,12 @@ public static String getSystemURI(String system) {
system = DICOM_DCM_URI;
} else if (system.equals("CDT")) {
system = CDT_URI;
} else if (system.equals("ICD9")) {
system = ICD9_URI;
} else if (system.equals("ICD10")) {
system = ICD10_URI;
} else if (system.equals("ICD10-CM")) {
system = ICD10_CM_URI;
}
return system;
}
Expand All @@ -258,6 +267,12 @@ public static String getSystemFromURI(String uri) {
return "DICOM-DCM";
case CDT_URI:
return "CDT";
case ICD9_URI:
return "ICD9";
case ICD10_URI:
return "ICD10";
case ICD10_CM_URI:
return "ICD10-CM";
default:
return "Unknown";
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/mitre/synthea/export/rif/CodeMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public List<? extends Map<String, String>> getMissingCodes() {
row.put("count", count.toString());
missingCodeList.add(row);
});
// sort in decending order by count
// sort in descending order by count
Collections.sort(missingCodeList, (o1, o2) -> {
return (int)(Long.parseLong(o2.get("count")) - Long.parseLong(o1.get("count")));
});
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/org/mitre/synthea/export/rif/RIFExporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ protected List<String> getDiagnosesCodes(Person person, long time) {
if (encounter.start <= time) {
for (HealthRecord.Entry dx : encounter.conditions) {
if (dx.start <= time && (dx.stop == 0L || dx.stop > time)) {
if (exporter.conditionCodeMapper.canMap(dx.codes.get(0))) {
if (dx.codes.get(0).system.startsWith("ICD10")) {
// Temporarily duplicate the code... we'll remove it later.
dx.codes.add(dx.codes.get(0));
diagnoses.add(dx);
} else if (exporter.conditionCodeMapper.canMap(dx.codes.get(0))) {
String mapped = exporter.conditionCodeMapper.map(dx.codes.get(0), person, true);
// Temporarily add the mapped code... we'll remove it later.
HealthRecord.Code mappedCode = new HealthRecord.Code("ICD10", mapped,
Expand Down Expand Up @@ -234,7 +238,9 @@ protected long getEarliestDiagnosis(Person person, String code) {
List<HealthRecord.Entry> diagnoses = new ArrayList<HealthRecord.Entry>();
for (HealthRecord.Encounter encounter : person.record.encounters) {
for (HealthRecord.Entry dx : encounter.conditions) {
if (exporter.conditionCodeMapper.canMap(dx.codes.get(0).code)) {
if (dx.codes.get(0).system.startsWith("ICD10")) {
diagnoses.add(dx);
} else if (exporter.conditionCodeMapper.canMap(dx.codes.get(0).code)) {
String mapped = exporter.conditionCodeMapper.map(dx.codes.get(0).code, person, true);
if (mapped.equals(code)) {
diagnoses.add(dx);
Expand Down
86 changes: 62 additions & 24 deletions src/main/java/org/mitre/synthea/world/concepts/Costs.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import java.util.List;
import java.util.Map;

import org.mitre.synthea.engine.Distribution;
import org.mitre.synthea.helpers.Config;
import org.mitre.synthea.helpers.RandomNumberGenerator;
import org.mitre.synthea.helpers.SimpleCSV;
import org.mitre.synthea.helpers.Utilities;
import org.mitre.synthea.world.agents.Person;
Expand All @@ -16,6 +16,8 @@
import org.mitre.synthea.world.geography.Location;

public class Costs {
private static final Distribution.Kind COST_METHOD = parseCostMethod();

// all of these are CSVs with these columns:
// code, min cost in $, mode cost in $, max cost in $, comments
private static final Map<String, CostData> PROCEDURE_COSTS =
Expand Down Expand Up @@ -144,7 +146,7 @@ public static double determineCostOfEntry(Entry entry, Person person) {
return 0.0;
}

String code = entry.codes.get(0).code;;
String code = entry.codes.get(0).code;
// Retrieve the base cost based on the code.
double baseCost;
if (costs != null && costs.containsKey(code)) {
Expand Down Expand Up @@ -256,6 +258,17 @@ private static Map<String, Double> parseEncounterAdjustmentFactors(String resour
}
}

/**
* Load the cost methodology.
* @return a Distribution.Kind enum value. Defaults to Triangular.
*/
private static Distribution.Kind parseCostMethod() {
String configValue =
Config.get("generate.costs.method", Distribution.Kind.TRIANGULAR.toString());
Distribution.Kind distribution = Distribution.Kind.valueOf(configValue.toUpperCase());
return distribution;
}

/**
* Whether or not this HealthRecord.Entry has an associated cost on a claim.
* Billing cost is not necessarily reimbursed cost or paid cost.
Expand Down Expand Up @@ -298,39 +311,64 @@ protected static class CostData {
private double min;
private double mode;
private double max;
private Distribution distribution;

private CostData(double min, double mode, double max) {
CostData(double min, double mode, double max) {
this.min = min;
this.mode = mode;
this.max = max;
this.distribution = new Distribution();
this.distribution.kind = COST_METHOD;
this.distribution.round = false;
this.distribution.parameters = new HashMap<String, Double>();
switch (COST_METHOD) {
case EXACT:
this.distribution.parameters.put("value", this.mode);
break;
case UNIFORM:
this.distribution.parameters.put("low", this.min);
this.distribution.parameters.put("high", this.max);
break;
case GAUSSIAN:
this.distribution.parameters.put("mean", this.mode);
this.distribution.parameters.put("min", this.min);
this.distribution.parameters.put("max", this.max);
double left = (this.mode - this.min) / 4.0d;
double right = (this.max - this.mode) / 4.0d;
double sd = Math.min(left, right);
this.distribution.parameters.put("standardDeviation", sd);
break;
case EXPONENTIAL:
this.distribution.parameters.put("mean", this.mode);
break;
case TRIANGULAR:
this.distribution.parameters.put("min", this.min);
this.distribution.parameters.put("mode", this.mode);
this.distribution.parameters.put("max", this.max);
break;
default:
break;
}
}

/**
* Select an individual cost based on this cost data. Uses a triangular
* distribution to pick a randomized value.
* @param rand Source of randomness
* Select an individual cost based on this cost data.
* @param person Source of randomness
* @return Single cost within the range this set of cost data represents
*/
private double chooseCost(RandomNumberGenerator rand) {
return triangularDistribution(min, max, mode, rand.rand());
}
protected double chooseCost(Person person) {

This comment has been minimized.

Copy link
@hadleynet

hadleynet Jul 26, 2024

Collaborator

Looks like any RandomNumberGenerator implementation would work here, why make it a Person specifically?

This comment has been minimized.

Copy link
@jawalonoski

jawalonoski Jul 26, 2024

Author Member

Because the signature Distribution.generate(Person person) takes a Person as an argument, and I didn't want to have to change that and the resulting downstream impacts to State and all the other ...engine package classes. I suppose I could have left the signature of chooseCost the same, but then I would have to recast the RandomNumberGenerator into a Person.

Person implements RandomNumberGenerator, but not all RandomNumberGenerators are Persons.

This comment has been minimized.

Copy link
@hadleynet

hadleynet Jul 26, 2024

Collaborator

Yeah, it looks like the ValueGenerator and subclasses are tied to Person vs RandomNumberGenerator but I don't see where any of the subclasses actually use stuff that Person provides over RandomNumberGenerator.

This comment has been minimized.

Copy link
@jawalonoski

jawalonoski Jul 26, 2024

Author Member

I don't think they do use Person specific stuff, I just didn't to initiate the domino effect of making that change.

double value = this.distribution.generate(person);

/**
* Pick a single value based on a triangular distribution. See:
* https://en.wikipedia.org/wiki/Triangular_distribution
* @param min Lower limit of the distribution
* @param max Upper limit of the distribution
* @param mode Most common value
* @param rand A random value between 0-1
* @return a single value from the distribution
*/
public static double triangularDistribution(double min, double max, double mode, double rand) {
double f = (mode - min) / (max - min);
if (rand < f) {
return min + Math.sqrt(rand * (max - min) * (mode - min));
} else {
return max - Math.sqrt((1 - rand) * (max - min) * (max - mode));
if (COST_METHOD.equals(Distribution.Kind.EXPONENTIAL)) {
value = value - 1.0;
}

if (value < this.min) {
value = this.min;
} else if (value > this.max) {
value = this.max;
}
return value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public boolean equals(Object obj) {
*
* @param system the URI identifier of the code system
* @param code the code itself
* @param display human-readable description of the coe
* @param display human-readable description of the code
*/
public Code(String system, String code, String display) {
this.system = system;
Expand Down
Loading

0 comments on commit 950416f

Please sign in to comment.