Reporting module: Calculated field and Converters - few questions

Hi community!

I am adding a field to my visit report which is the patient’s Weight divided by Height.

Currently, I have created an ArithmeticOperationConverter that does that (could be PRed if it appears to be a satisfying choice). The way it works is:

  1. the data definition associated with my “Weight/Height” column returns a list of obs (answers of a given question) for each patient
  2. the converter takes this list of obs as an input and performs an arithmetic operation, in this case it divides every member of the list

(Q) Is that a correct approach? (Q) Is there something existing already that I should use?

I have chosen to use a converter in this case because I have also created a ObsForVisitDataDefinition (PR to add this to the Reporting module coming soon) useful for other fields of this report. This way, we can just keep using this data definition, and simply apply the converter.

Another possible option I guess would be to simply create a new WeightOnHeightPatientDataDefinition and assign this to my “Weight/Height” report column.

Then I also need to calculate the Weight divided by Age. So in this case I won’t be able to use the ObsForVisitDataDefinition (Age is not an obs). I will have to create a new data definition that returns one obs and the patient’s age, something like ObsAndAgeForVisitDataDefintion, and then apply the ArithmeticOperationConverter. So this starts to be a bit stretched.

Am I better off creating two new data defs?

  • WeightOnHeightPatientDataDefinition.
  • WeightOnAgePatientDataDefinition. rather than leveraging on converters for these cases?

Thanks

Romain

@mksrom, good question. In general, my thinking behind creating the DataConverters was that they would be most useful when you already had a DataDefinition that produced the value you wanted, but you wanted to display or slice-and-dice it in different ways.

For example, if there is a DataDefinition that produces an Obs for each patient (eg. latest obs for a patient for a given Concept, with optional filters for location, date range, and encounter type) I didn’t want us to have to replicate this same definition and all of these configuration properties just to get the latest Obs location or latest Obs dateTime or latest Obs valueXYZ. In this case, it makes a lot more sense to lighter-weight data converters that can be applied to the Obs and retrieve that value of interest off of it. Other common case might be to add a translation (eg. convert a GenderDataDefinition from M/F to “Homme”, “Femme”), convert a List of values to a single comma-delimited String, format a Date, or convert an Age to an Age Range category.

Now, you could achieve what you wanted with a DataConverter by having a DataDefinition that produced a complex object with lots of properties, if this is something you feel like you will want to manipulate in lots of different ways on your reports. (eg. a bean containing Height Obs, Weight Obs, Age of Patient). Then, you could have one or more DataConverters that could be configured to return some converted value off of this (height.valueNumeric/weight.valueNumeric) or (weight.valueNumeric / age.fullYears).

That being said, the nature of caching in the reporting module means that there is not much (if any) penalty for doing these all as independent DataDefinitions, as long as they are evaluated in such a way that subsequent evaluations of the same definition can be pulled from the cache rather than being computed fresh. In this case, you could have:

DataDefinition: Produce Weight Obs DataDefinition: Produce Height Obs DataDefinition: Produce Age

(all of the above exist already I believe)

Then, you’d have an ArithmeticDataDefinition with configuration properties of various data definitions and the operation you want to perform on them. This could be as sophisticated as what we have in the CompositionCohortDefinition, where you can specify 1-N cohort queries and an expression like “(1 OR (2 AND 3)) AND NOT 4”. Or it could support simple operations of 2 operands:

DataDefinition definition1; DataDefinition definition2; Operator operator;

Mike

Thanks @mseaton. I think I will create the ArithmeticDataDefinition. Trying now…

One thing I have forgotten to ask is: Is it possible to chain converters?

I see that the fonction DataSetDefinition#addColumn() can accept multiple converters, so does the output of the first converter can simply be used as the input of the second one?

So to keep things simple, I will create for now a ObsForAgeDataDefinition (of type PersonDataDefiniton).

This ObsForAgeDataDefinition will take 2 properties:

  • ObsForPersonDataDefinition
  • AgeDataDefinition

and return the resulting calculated value for each patient.

So in my report, I first instanciate a new ObsForPersonDataDefinition:

ObsForPersonDataDefinition obsPersonDD = new ObsForPersonDataDefinition();
obsPersonDD.addParameter(new Parameter("question", "Concept Question", Concept.class));

and then an AgeDataDefinition

AgeDataDefinition agePersonDD = new AgeDataDefinition();
agePersonDD.addParameter(new Parameter("effectiveDate", "Effective Date", Date.class));

Finally the ObsForAgeDataDefintion

ObsOnAgeDataDefinition obsOnAge = new ObsOnAgeDataDefinition();
obsOnAge.setObsDataDefinition(obsPersonDD);
obsOnAge.setAgeDataDefinition(agePersonDD);

and then attaching this to the visit dataset definition…

Now the evaluation of this happens in the ObsForAgeDataEvaluator#evaluate() method.

ObsOnAgeDataDefinition obsOnAgeDD = (ObsOnAgeDataDefinition) definition;

EvaluatedPersonData obsData = personDataService.evaluate(obsOnAgeDD.getObsDataDefinition(), context);
EvaluatedPersonData ageData = personDataService.evaluate(obsOnAgeDD.getAgeDataDefintion(), context);

for (Integer pid : obsData.getData().keySet()) {
// etc etc
}
		

(Q) But how to evaluate the obsPersonDD and agePersonDD within the ObsForAgeDataEvaluator so their parameters are mapped with my report Parameters?

@mksrom, yes this is correct - if you pass in multiple converters it will chain them together. There is also an explicit ChainedConverter class you can use directly.

The data type for your “obsDataDefinition” and “ageDataDefinition” properties on your ObsForAgeDataDefinition should be of type Mapped<PersonDataDefinition>, not simply of type PersonDataDefinition.

See for example a RelativeDateCohortDefinition class I wrote at one point for our Malawi project - this also takes in 2 underlying Definitions and performs an operation on them (this is a CohortDefinition, but he same principle applies).

Mike

Thanks for pointing me to this example :thumbsup: I was able to make this work (Weigh on Age).

(For anyone who might want to do the same, here is the commit)

@mksrom …and you might also want to investigate the ScriptedCompositionPatientDataDefinition (…some test cases here ) if you are going to do a lot of custom calculations based on multiple person/patient data definitions.

@rubailly that’s very interesting. I probably won’t have to use this for this first set of requirements I have. But that’s something I will keep in mind. Thanks.