Active Questionnaires in #FHIR
Jul 17, 2018FHIR defines a Questionnaire resource that specifies a set of questions for a user, along with a QuestionnaireResponse resource to capture their response. Forms/Questionnaires like this are ubiquitious in healthcare, so this has had a lot of attention. A questionnaire contains a list of questions, with a nested structure, and for each question, a way to specify another question that the visibility of the question depends on. But there’s several common ways people use questionnaires that this doesn’t deal with, that we could call ‘Active Questionnaire’ support:
- Prepopulating a questionnaire from known data
- Calculating a score for a questionnaire
- Extracting data from a questionnaire after entry and populating other resources
- Adaptive questionnaire where the next question depends on the answer to previous questions
A few of us have been working on proposals for how these problems could be solved in an interoperable way. I’m publishing these ideas here for discussion - then they’ll go to the HL7 committees for further consideration.
Note that our solutions to these problems are based on the availability of resources, and the RESTful API. This enables interoperability between the record server and the form filler, but it is not necessary that they are separate, or even that they use FHIR to communicate between them – the form filler can process the FHIR based questionnaire content for whatever form is desired.
Prepopulating a questionnaire
This is a common idea; that a series of questions will be asked about a patient, for a clinical user to answer. At least some of the questions will already have known answers from data stored in the system. E.g. Patient demographics, blood type, existing medications etc. Once the answers are populated, they are presented to the user who can change them if necessary.
We understand pre-populating a questionnaire in 3 phases:
- Specifying what the ‘context’ of a questionnaire is: the information that the form-filler needs when the user answers a questionnaire
- Defining Variables
- Filling the answers
Specifying the context
Extension: http://hl7.org/fhir/StructureDefinition/form-filler-parameter
This parameter defines a parameter that the form filler needs to pre-populate the questionnaire:
“Extension”: [{
"url" : "http://hl7.org/fhir/StructureDefinition/form-filler-parameter",
"extension: [{
"url" : "name",
"valueString" : "patient"
},{
"url" : "type",
"valueCode" : "Patient"
}]
}]
This extension says that the form filler takes a parameter which is a FHIR Patient resource. The application that hosts the form filler (which may be a separate app or a module in the application) is provided this as external input. The parameter is named “patient” through out the questionnaire.
E.g. the form filler would be provided with a map of named resource that identify the application context of the questionnaire.
It’s up the form filler and/or the application to decide what to do if the parameter is not available; obviously pre-population won’t be possible, but it may not be right to even answer the questionnaire.
Defining Variables
“Extension”: [{
"url" : "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression" : {
"name" : "sleepObs",
"language" : "application/x-fhir-query",
"expression" : "Observation?code=http://loinc.org|65972-2&date=gt{{today()-7 days}}&patient={{patient.id}}&_sort=-date&_count=1"
}
}]
This extension defines a ‘variable’. A variable has a name, and a value which is a list of resources or FHIR types. Variables are defined on the questionnaire itself, or on any item, and available in the scope of the element where it is defined, and any contained items (unless overridden by a variable with same name on a contained item, which overrides if in the scope of that nested item).
Each variable is defined by a expression, which the form-filler evaluates when needed. Form-fillers should support FHIRPath and CQL for these expressions, but most important is the application/x-fhir-query type, which specifies a templated FHIR query. This is a query string that has no [base] (that’s known to the form filler, not the questionnaire). The query string allowed expressions using liquid format e.g. {{expression}}, where each expression is itself a FHIRPath expression.
The form filler evaluates the expression, and then executes the FHIR query against the record store it is operating in the context of. The set of resources in the response becomes the value of the variable.
In the case of the example above, that means one resource, the most recent observation for the patient if there is one in the last 7 days. Or ‘null’ if there isn’t one.
Filling the answers
When processing an item, the form-filler has a set of variables in context. The “populate-value” extension defines how a value is derived from the variables:
“Extension”: [{
"url" : "http://hl7.org/fhir/StructureDefinition/populate-value",
"valueExpression" : {
"language" : "text/fhirPath",
"expression " : "sleepObs.value"
}
}]
The extension has a value which an expression, which is a FHIRPath expression, that extracts a value from one of the variables. The form-filler evaluates the expression, and if there is a value returned, and it is a valid answer for the item (e.g based on item type and allowed answers), then it goes in the answer.
The combination of
- Specifying what information is provided from the context
- Building up a set if variables by querying the record store
- Populating the answers by selecting information from the variables
meets most of the requirements for pre-populating the answers. Requirements beyond what can be done here fall back to adaptive questionnaires (see below).
Note that the data extraction section below defines additional ways by which questionnaires can be pre-populated.
Calculating a score for a questionnaire
A very common use case for questionnaires is to use them to calculate some kind of clinical score – such questionnaires are very common across healthcare. Mostly, it’s a fairly simple form – answer a series of linear questions, and get a single score, but there’s much more complex examples
Calculating a score is understood as a 3 step process:
- Specifying a numeric value for some/all answers
- Generating the score
- Putting the score in an answer
Note that the third part is not necessary – the score might be generated and displayed to the user without being made part of the formal answers (on the basis that it can be recalculated as required).
Calculating the score
“Extension”: [{
"url" : "http://hl7.org/fhir/StructureDefinition/questionnaire-calculated-value",
"valueExpression" : {
“description” : “Score (0-4: healthy; 5-8: Moderate; 9-12: Serious)”,
"language" : "text/fhirpath",
"name" : "score",
"expression" : "answers().sum(value.ordinal())"
}
}]
The questionnaire-calculated-value extension defines a variable (just like the /variable extension above) but it is special for two reasons:
- The value is re-generated each time the set of answers change (whereas other variables are only calculated when prepopulating
- The form filler may show the outcomes of calculated scores in a special part of the UI that is not part of the questionnaire
Most calculated scores depend on assigning calculated-value values to coded answers using the http://hl7.org/fhir/StructureDefinition/valueset-ordinalValue extension. This extension can appear in one of the following places:
- On an answerOption
- On a value set referred to from answerValueSet (contained or external)
- On the underlying code system (contained or external)
Resolving the ordinal value is a chore that can make the expressions unnecessarily complex. We define 3 FHIRPath short cuts to simplify the expressions:
- answers() - a flattened collection of all the answers in a response
- ordinal() – given the focus of an answer (QuestionnaireResponse.item.answer.valueCoding), look up the ordinal value where ever it is defined
- sum(expression) – a shortcut for aggregate($this + expression, 0)
These are defined FHIR Path extensions for form-fillers to implement.
For most scores, there is only one score on the questionnaire. However more complex questionnaires may have multiple scores for different sections, or even multiple interlacing scores. The questionnaire-calculated-value extension may be found at the root of the questionnaire, or on any item (just like other variable definitions)
Extracting Data from a questionnaire
This is the converse of the Pre-population case – given a set of answers to a questionnaire, extract data out of the answers into a set of resources – maybe medication statements, etc, but most of all, observations.
Note that it is at the discretion of the system when to extract data from a questionnaire. The most obvious time to do so is when the user indicates the questionnaire is ‘complete’ but there may be intermediate steps (e.g. if the user suspends answering the questionnaire).
Extracting data well can be a hard problem. There is one common special case that justifies a specific simple approach: when the data is extracted into an observation. For other cases, we define a context based approach.
Observation / Questionnaires
A very common situation is that questionnaire items are actually observations, and the answer provided by the user will become stored observation in the system – and also should be used to pre-populate the answer if an appropriate observation exists.
Questionnaires already have a code to tie them to an observation:
"code": [{
"system": "http://loinc.org",
"code": "65972-2",
"display": "Appetite or sleep change notes"
}],
We define an extension to define the link between the item and an observation this:
“Extension”: [{
"url" : "http://hl7.org/fhir/StructureDefinition/questionnaire-observation-link",
"valueDuration" : {
"value" : "14",
"system" : "http://unitsofmeasure.org",
"code" : "d"
}
}]
This instructs the form-filler to look for any observations that have occurred within the last 14 days (or, more generally, any duration, based on how quickly the particular observation goes stale). If such an observation is found, then the most recent is used. When the data is extracted from the questionnaire, a new observation is created if:
- there is no existing observation
- the user changed the answer
- the system decides to create a new observation from user approval of the existing value anyway
If an element has both a link to an observation, and a pre-population details, the pre-population is treated as a fallback approach for if no existing observation is found.
Context based Data Extraction
“Extension”: [{
"url" : "http://hl7.org/fhir/StructureDefinition/context",
"valueExpression" : {
"language" : "application/x-fhir-query",
"name" : "meds2",
"expression" : "MedicationStatement?date=ge{{today()-30 days}}&patient={{patient.id}}&status=active,completed,stopped,unknown"
}
}]
This not only defines a variable (per the variable discussion above) but establishes that the variable is the context for the item, which means that for each value in the variable, the item will repeat. This implies that a context should generally only be established on a repeating item that is a group, though specific applications might not follow this pattern.
There can only be one context for an item.
Once a context is established, items are tied directly to the context by using Observation.item.definition:
“definition” : “MedicationStatement#MedicationStatement.medicationCodeableConcept”,
The form-filler extracts the indicated element and populates the answer value accordingly. Note that there’s considerable subtlety here – the are potential data type conversion and cardinality issues to handle, and the answer value may only be a part of the data type (e.g. in this case, a coding from a CodeableConcept). Failures may be handled
The element definition can leverage a custom profile to be more specific:
“definition” : “http://acme.com/profiles/MedStmtBase#MedicationStatement.medicationCodeableConcept.coding:rxnorm”,
The form-filler retains the link from populating the resource, so that existing resources can be updated. Newly created item groups create new resources when they are associated with a repeating context, and the questionnaire defines hidden form fields with calculated or fixed values to fill out the rest of the content of new resources.
Note: this approach only handles moderately complex questionnaires. To go beyond this, applications would have to use StructureMap or write custom transforms in some implementation language.
Implementation Notes
Note that the reference libraries can easily do all these things – FHIRPath, queries, etc, but many product implementations do have the underlying language support for this. It may be useful to define a lower level conformance/questionnaire/reasoning API to make it easy for these facilities to be added to existing products.
Adaptive Questionnaire
This is a growing area of interest, where behind the questionnaire, there’s an expert system (or maybe a full AI system) guiding the user through a set of questions – enabling or disabling, or even creating a custom questionnaire as the user works through the questions.
The basic idea here is that the form-filler begins with a questionnaire, and a link to an advisor system. Each time the user answers a question, the advisor is consulted and returns one of the following responses:
- No more questions
- Existing answer (by id) is invalid
- Next question is [id]
- A new question to ask (after [id], or at the end)
In some cases, this means that there’s a custom questionnaire; this is built as the user answers the questions, and then should be stored alongside the answers to provide context (or inside the QuestionnaireResponse, as a contained resource).
For this, one option is to use cds-hooks. The hook definition is:
Questionnaire-advise
Metadata | Value |
specificationVersion | 1.0 |
hookVersion | 0.0 |
Workflow
Call this hook each time a user provides an answer in a questionnaire. The client (the form-filler) provides both the questionnaire that the user is answering, and the QuestionnaireResponse that captures the answers to this point, along with the id of the question answered, and an id that represents the instance of the form, which the cds hooks server may use to request access to the form filler parameters (as defined above).
Context
Field | Optionality | Prefetch Token | Type | Description |
questions | REQUIRED | No | Questionnaire | The questionnaire that the user is answering (provide the whole questionnaire, since it may change during the answering process) |
answers | REQUIRED | No | QuestionnaireResponse | The users’ answers to this point |
answered | OPTIONAL | No | string | The dotted linkId of the last question answered (e.g. linkId based path to the answer given that groups can repeat. No value – the user hasn’t answered any questions yet (this session) |
parameters | OPTIONAL | Yes | string | A list of name=value pairs in URL syntax that defines the parameters available to the form filler e.g. patient=Patient/123&context=Encounter/123. The questionnaire can be expected to refer to these parameters. |
The cds hooks server can request that these be prefetched (how exactly?)
There’s plenty of questions about this approach; would it be better to just define a direct operation to do this? (Pro: it would be simpler, but con: cds hooks provides an infrastructure to get more information about the patient when the responder to this is not the same as the patient record holder). So discussion about this is ongoing
Example
Here’s a worked example to help understand these ideas:
{
"resourceType": "Questionnaire",
"id": "questionnaire-example-phq9",
"url": "http://fhir.org/guides/argonaut-questionnaire/Questionnaire/questionnaire-example-phq9",
"meta": {
"profile": [
"http://fhir.org/guides/argonaut-questionnaire/StructureDefinition/q"
]
},
"contained": [
{
"resourceType": "ValueSet",
"id": "PHQ-9",
"url": "http://fhir.org/guides/argonaut-questionnaire/ValueSet/PHQ-9",
"version": "0.0.0",
"name": "PHQ-9",
"title": "PHQ / Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day",
"status": "active",
"date": "2018-04-05T12:40:10-07:00",
"description": "PHQ / Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day http://loinc.org/vs/LL358-3",
"jurisdiction": [
{
"coding": [
{
"system": "urn:iso:std:iso:3166",
"code": "US",
"display": "United States of America"
}
]
}
],
"copyright": "This content LOINC® is copyright © 1995 Regenstrief Institute, Inc. and the LOINC Committee, and available at no cost under the license at http://loinc.org/terms-of-use",
"compose": {
"include": [
{
"system": "http://loinc.org",
"concept": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/valueset-ordinalValue",
"valueDecimal": 0
}
],
"code": "LA6568-5",
"display": "Not at all"
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/valueset-ordinalValue",
"valueDecimal": 1
}
],
"code": "LA6569-3",
"display": "Several days"
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/valueset-ordinalValue",
"valueDecimal": 2
}
],
"code": "LA6570-1",
"display": "More than half the days"
},
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/valueset-ordinalValue",
"valueDecimal": 3
}
],
"code": "LA6571-9",
"display": "Nearly every day"
}
]
}
]
}
}
],
"identifier": [
{
"system": "http://acme.org/q-identifiers",
"value": "business-identifier"
}
],
"title": "Patient Health Questionnaire (PHQ-9)",
"status": "draft",
"code": [
{
"system": "http://loinc.org",
"code": "44249-1",
"display": "PHQ-9 quick depression assessment panel:-:Pt:^Patient:-:Report.PHQ-9"
}
],
"subjectType": [
"Patient"
],
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/questionnaire-calculated-value",
"valueExpression" : {
"language" : "text/fhirpath",
"name" : "score",
"expression" : "answers().sum(value.ordinal())"
}
}, {
"url" : "http://hl7.org/fhir/StructureDefinition/form-filler-parameter",
"extension: [{
"url" : "name",
"valueString" : "patient"
},{
"url" : "type",
"valueCode" : "Patient"
}]
}]
"item": [
{
"linkId": "panel",
"code": [
{
"system": "http://loinc.org",
"code": "44249-1",
"display": "PHQ-9 quick depression assessment panel:-:Pt:^Patient:-:Report.PHQ-9"
}
],
"text": "Over the last 2 weeks, how often have you been bothered by any of the following problems?",
"type": "display"
},
{ // observation lookup
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression" : {
"language" : "application/x-fhir-query",
"name" : "sleepObs",
"expression" : "Observation?code=http://loinc.org|65972-2&date=gt{{today()-7 days}}&patient={{patient.id}}&_sort=-date&_count=1"
}
}, {
"url" : "http://hl7.org/fhir/StructureDefinition/populate-value",
"valueExpression" : {
"language" : "text/fhirPath",
"expression " : "sleepObs.value"
}
}, {
// if there's an observation on this patient (and in other applicable context) with the
// same code as the item, in the specified duration before [now],
// use the most recent observation value for the item. If there's no observation, it would
// appropriate to create one for later re-use if the system is capable of this.
//
// note: if you have this extension, the other two extensions (variable & populate) are
// treated as fall backs if there is no observation
//
// if there's no answer when form is completed, systems can (but are not required to) create an observation with
// dataAbsentReason = asked-declined
"url" : "http://hl7.org/fhir/StructureDefinition/questionnaire-observation-link",
"valueDuration" : {
"value" : "14",
"system" : "http://unitsofmeasure.org",
"code" : "days"
}
}],
"linkId": "sleeping",
"code": [
{
"system": "http://loinc.org",
"code": "65972-2",
"display": "Appetite or sleep change notes"
}
],
"text": "How well have you been sleeping",
"type": "string"
},
{ // medications look up
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression" : {
"language" : "application/x-fhir-query",
"name" : "meds",
"expression" : "?type=MedicationAdministration,MedicationRequest,MedicationStatement
&code:in=http://valuesets/anti-depressant&date=gt{{today()-7 days}}&patient={{patient.id}}"
}
}, {
"url" : "http://hl7.org/fhir/StructureDefinition/variable",
"valueExpression" : {
"language" : "application/x-fhir-query",
"name" : "medsObs",
"expression" : "Observation?code=http://loinc.org|XXX-X&date=gt{{today()-7 days}}&patient={{patient.id}}&_sort=-date&_count=1"
}
}, {
"url" : "http://hl7.org/fhir/StructureDefinition/populate-value",
"valueExpression" : {
"language" : "text/fhirPath",
"expression " : "meds.count() > 0 or medsObs.value"
}
}],
"linkId": "anti-depressant",
"text": "Are you taking anti-depressant therapy?",
"type": "boolean"
}, {
// tie this to a list of medication statement resources
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/context",
"valueExpression" : {
"language" : "application/x-fhir-query",
"name" : "meds2",
"expression" : "MedicationStatement?date=ge{{today()-30 days}}&patient={{patient.id}}&status=active,completed,stopped,unknown"
}]
"linkId": "medlist",
"text": "List of current or recent meds (last 30 days)",
"type": "group",
"required": false,
"repeats": true,
"item" : [{
"linkId": "med",
// one of these two.... (second ties to a more limited context)
"definition" : "MedicationStatement#MedicationStatement.medicationCodeableConcept", // if system doesn't know which coding, pick first or go bang
"definition" : "http://chris.special.guy/profiles/MedStmtBase#MedicationStatement.medicationCodeableConcept.coding:rxnorm",
"text": "Name of medication",
"type": "open-choice",
"required": true,
"options" : "http://hl7.org/fhir/rxnorm-and-unii-and-everthing-else-too"
}, {
// tie this to MedicationStatement.status
"extension" : {[
"url" : "http://hl7.org/fhir/StructureDefinition/populate-value",
"valueExpression" : {
"language" : "text/fhirPath",
"expression " : "status = 'active'" // cause we've specified the context
}
}, {
"url" : "http://hl7.org/fhir/StructureDefinition/extract-value",
"valueExpression" : {
"language" : "text/fhirPath",
"expression " : "iff($this, 'active', 'stopped')"
}
}]
"linkId": "status",
"definition" : "MedicationStatement#MedicationStatement.status",
"text": "Current?",
"type": "boolean",
"required": true
}, {
"linkId": "how-long",
"text": "How many days ago did you stop taking this?",
"type": "decimal",
"required": false
}, {
// tie this to MedicationStatement.note.text
"linkId": "comment",
"definition" : "MedicationStatement#MedicationStatement.note.text", // this is implicitly defined not explicit
"text": "Comments about medication (dose, frequency etc)",
"type": "string",
"required": false
}]
},
{
"linkId": "LittleInterest",
"code": [
{
"system": "http://loinc.org",
"code": "44250-9"
}
],
"text": "Little interest or pleasure in doing things",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day"
}
},
{
"linkId": "FeelingDown",
"code": [
{
"system": "http://loinc.org",
"code": "44255-8"
}
],
"text": "Feeling down, depressed, or hopeless",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day"
}
},
{
"linkId": "TroubleSleeping",
"code": [
{
"system": "http://loinc.org",
"code": "44259-0"
}
],
"text": "Trouble falling or staying asleep",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day"
}
},
{
"linkId": "FeelingTired",
"code": [
{
"system": "http://loinc.org",
"code": "44254-1"
}
],
"text": "Feeling tired or having little energy",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day"
}
},
{
"linkId": "BadAppetite",
"code": [
{
"system": "http://loinc.org",
"code": "44251-7"
}
],
"text": "Poor appetite or overeating",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day"
}
},
{
"linkId": "FeelingBadAboutSelf",
"code": [
{
"system": "http://loinc.org",
"code": "44258-2"
}
],
"text": "Feeling bad about yourself - or that you are a failure or have let yourself or your family down",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day"
}
},
{
"linkId": "TroubleConcentrating",
"code": [
{
"system": "http://loinc.org",
"code": "44252-5"
}
],
"text": "Trouble concentrating on things, such as reading the newspaper or watching television",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day"
}
},
{
"linkId": "MovingSpeaking",
"code": [
{
"system": "http://loinc.org",
"code": "44253-3"
}
],
"text": "Moving or speaking so slowly that other people could have noticed. Or the opposite - being so fidgety or restless that you have been moving around a lot more than usual",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Patient Health Questionnaire (PHQ-9) Not at all/Several days/More than half the days/Nearly every day"
}
},
{
"linkId": "TotalScore",
"code": [
{
"system": "http://loinc.org",
"code": "44261-6"
}
],
"text": "Total score",
"type": "integer",
"required": true,
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/populate-value",
"valueExpression" : {
"language" :"text/fhirPath",
"expression " : "score"
}
},{
"url" : "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden",
"valueBoolean" : false
}]
},
{
"linkId": "Difficulty",
"code": [
{
"system": "http://loinc.org",
"code": "44256-6"
}
],
"text": "If you checked off any problems, how difficult have these problems made it for you to do your work, take care of things at home, or get along with other people",
"type": "choice",
"required": true,
"options": {
"reference": "#PHQ-9",
"display": "Not difficult at all/Somewhat difficult/Very difficult/Extremely difficult-Perceived difficulty (PHQ-9)"
}
},
{
// including a questionnaire module
// any link ids in the questionnire are prefixed with this id.
"linkId": "prefix",
"type": "group",
"required": true,
"extension" : [{
"url" : "http://hl7.org/fhir/StructureDefinition/questionnaire-include",
"valueUri" : "[canonical URL]"
}]
}
]
}