Module 8: Map a Questionnaire to DDI Flow Logic¶
What you will learn
- Read a paper questionnaire specification and identify its flow logic.
- Map each part of the spec to a DDI control construct:
Sequence,QuestionConstruct,IfThenElse,StatementItem. - Build skip patterns with
IfThenElseandElseIf. - Understand how DDI represents loops for household rosters and repeated sections.
- Connect an
Instrumentto a top-levelSequence.
Prerequisites: Module 7: Code Lists.
Time: 50 min self-paced / 60 min instructor-led.
1. From paper to DDI¶
Most questionnaires start as a written specification. The spec lists the questions in order and describes skip patterns: rules like "If the respondent is under 18, skip to Section C."
Your job is to turn that spec into DDI metadata so that the flow logic is machine-readable. This module teaches you how.
2. A sample questionnaire spec¶
Here is a simple health survey specification. Read it carefully; we will map every part to DDI.
Health Survey Spec
Section A: Demographics
- A1. What is your age? (numeric)
- A2. What is your gender? (code list: Male, Female, Other)
Section B: Employment (skip if age < 16)
- B1. Are you currently employed? (Yes / No)
- B2. What is your occupation? (text, ask only if B1 = Yes)
Section C: Health
- C1. How would you rate your general health? (code list: Excellent, Good, Fair, Poor)
- C2. Do you smoke? (Yes / No)
Household Roster (repeat Section A for each household member)
3. The DDI building blocks¶
Each part of the spec maps to a DDI control construct:
| Spec element | DDI construct | What it does |
|---|---|---|
| A section intro | StatementItem |
Shows text without asking a question |
| A question | QuestionConstruct |
Asks one question |
| An ordered section | Sequence |
Groups steps in order |
| "Skip if..." | IfThenElse |
Branches based on a condition |
| "Ask only if..." | IfThenElse |
Same; the condition decides which path |
| "Repeat for each..." | Loop |
Repeats a section for a list of items |
| The whole questionnaire | Instrument |
Points to the top-level Sequence |
All of these live in the ddi_l.models.datacollection module:
from ddi_l.models.datacollection import (
Instrument,
Sequence,
QuestionConstruct,
IfThenElse,
ElseIf,
StatementItem,
)
Two things to remember
- You create every construct with the same
doc.add_item(<Type>, name="...")call you already use for questions and variables. - You link constructs to one another with
.to_reference(). A reference says "this construct points to that one". It carries the target's identifier and type so the file stays valid.
4. Step 1: Create the study and questions¶
First, create the study and all the questions from the spec. Each question becomes a DDI question item.
import ddi_l as ddi
doc = ddi.new_study(title="National Health Survey", agency="health.gc.ca")
# Section A
q_age = doc.add_question(text="What is your age?")
q_gender = doc.add_question(text="What is your gender?")
# Section B
q_employed = doc.add_question(text="Are you currently employed?")
q_occupation = doc.add_question(text="What is your occupation?")
# Section C
q_health = doc.add_question(text="How would you rate your general health?")
q_smoke = doc.add_question(text="Do you smoke?")
print(f"Questions: {len(doc.questions)}") # -> Questions: 6
5. Step 2: Create QuestionConstructs¶
A QuestionConstruct wraps a question so it can be placed in a Sequence. Think of it as the instruction "now ask this question."
Add one for each question, and point it at the question with .to_reference():
qc_age = doc.add_item(
QuestionConstruct, name="Ask Age", question_reference=q_age.to_reference()
)
qc_gender = doc.add_item(
QuestionConstruct, name="Ask Gender", question_reference=q_gender.to_reference()
)
qc_employed = doc.add_item(
QuestionConstruct, name="Ask Employed", question_reference=q_employed.to_reference()
)
qc_occupation = doc.add_item(
QuestionConstruct,
name="Ask Occupation",
question_reference=q_occupation.to_reference(),
)
qc_health = doc.add_item(
QuestionConstruct,
name="Ask Health Rating",
question_reference=q_health.to_reference(),
)
qc_smoke = doc.add_item(
QuestionConstruct, name="Ask Smoke", question_reference=q_smoke.to_reference()
)
print(f"QuestionConstructs: {len(doc.items(QuestionConstruct))}")
# -> QuestionConstructs: 6
6. Step 3: Build Sequences for each section¶
The spec has three sections (A, B, C).
Each section becomes a Sequence that lists its steps in order. You list the
steps with control_construct_references, one reference per step:
seq_demographics = doc.add_item(
Sequence,
name="Section A - Demographics",
control_construct_references=[qc_age.to_reference(), qc_gender.to_reference()],
)
seq_employment = doc.add_item(
Sequence,
name="Section B - Employment",
control_construct_references=[qc_employed.to_reference()],
)
seq_health = doc.add_item(
Sequence,
name="Section C - Health",
control_construct_references=[qc_health.to_reference(), qc_smoke.to_reference()],
)
You can also add a StatementItem to introduce a section. It shows a message instead of asking a question:
7. Step 4: Model skip patterns with IfThenElse¶
The spec says: "Skip Section B if age < 16."
In DDI, this becomes an IfThenElse construct.
An IfThenElse has three parts:
- If condition: the rule to check (age >= 16)
- Then: which construct to run if true (Section B)
- Else: which construct to run if false (skip to Section C)
Create it with add_item(), set the rule with set_condition(), then connect
the branches with .to_reference():
skip_employment = doc.add_item(IfThenElse, name="Age gate for employment")
skip_employment.set_condition("age >= 16", description="Working age")
skip_employment.then_construct_reference = seq_employment.to_reference()
skip_employment.else_construct_reference = seq_health.to_reference()
For multi-way routing ("if 16-64 ask employment, if 65+ ask retirement"), add
ElseIf branches with add_elseif():
seq_retirement = doc.add_item(
Sequence,
name="Section B2 - Retirement",
control_construct_references=[qc_employed.to_reference()],
)
skip_employment.add_elseif(seq_retirement.to_reference(), command="age >= 65")
8. Step 5: Model "ask only if" with a nested IfThenElse¶
The spec says: "Ask B2 (occupation) only if B1 = Yes."
This is another IfThenElse, this time inside Section B.
The "then" path runs the occupation QuestionConstruct; the "else" path is
left empty, so the question is skipped:
ask_occupation = doc.add_item(IfThenElse, name="Occupation routing")
ask_occupation.then_construct_reference = qc_occupation.to_reference()
9. Step 6: Model a household roster with Loop¶
The spec says: "Repeat Section A for each household member." DDI represents this with a Loop construct.
A Loop repeats a control construct (usually a Sequence) for each item in a
list. For example, it runs the demographics section once per household
member. Add it like any other construct, and point it at the section it
repeats with control_construct_reference:
from ddi_l.models.datacollection import Loop
roster_loop = doc.add_item(Loop, name="Household roster loop")
roster_loop.control_construct_reference = seq_demographics.to_reference()
10. Step 7: Build the main Sequence and Instrument¶
The main Sequence defines the overall flow of the questionnaire. It references the section sequences, the IfThenElse gates, and the loop, in order.
main_seq = doc.add_item(
Sequence,
name="Main Survey Flow",
control_construct_references=[
intro_a.to_reference(),
seq_demographics.to_reference(),
skip_employment.to_reference(),
seq_health.to_reference(),
],
)
The order would be:
StatementItem: WelcomeSequence: Section A (Demographics)IfThenElse: Age gate (→ Section B or skip)Sequence: Section C (Health)Loop: Household roster
Finally, create an Instrument that points to the main Sequence. The Instrument is the top-level entry point: it says "this is the questionnaire."
instrument = doc.add_item(Instrument, name="Health Survey Instrument")
instrument.control_construct_reference = main_seq.to_reference()
print(f"Instruments: {len(doc.items(Instrument))}") # -> Instruments: 1
# The whole flow is valid DDI:
assert doc.validate() == []
The second line is the one that matters. Without it the Instrument has a
name and nothing else (a questionnaire that administers nothing), and the
document is still perfectly valid, because the schema does not require the
link. Only the ControlConstructReference connects the entry point to the
flow you just built.
11. The complete mapping¶
Here is the full spec mapped to DDI, as a diagram:
flowchart TD
I["Instrument: Health Survey"] --> M["Sequence: Main Flow"]
M --> W["StatementItem: Welcome"]
M --> SA["Sequence: Section A"]
SA --> QC1["QuestionConstruct: Ask Age"]
SA --> QC2["QuestionConstruct: Ask Gender"]
M --> ITE1["IfThenElse: Age >= 16?"]
ITE1 -->|Yes| SB["Sequence: Section B"]
ITE1 -->|No| SC["Sequence: Section C"]
SB --> QC3["QuestionConstruct: Ask Employed"]
SB --> ITE2["IfThenElse: Employed = Yes?"]
ITE2 -->|Yes| QC4["QuestionConstruct: Ask Occupation"]
ITE2 -->|No| SC
M --> SC
SC --> QC5["QuestionConstruct: Ask Health"]
SC --> QC6["QuestionConstruct: Ask Smoke"]
M --> L["Loop: Household Roster"]
L --> SA
12. Mapping checklist¶
Use this checklist when you map any questionnaire spec to DDI:
- List all questions →
add_item(QuestionConstruct, ...)for each - Identify sections →
add_item(Sequence, ...)for each - Find "skip if..." rules →
add_item(IfThenElse, ...)for each - Find "ask only if..." rules →
add_item(IfThenElse, ...)for each - Find "repeat for each..." rules →
add_item(Loop, ...)for each - Find section introductions →
add_item(StatementItem, ...)for each - Build the main
Sequencethat ties everything together - Create the
Instrumentthat points to the main Sequence - Add
ElseIfbranches for multi-way routing (e.g., age groups)
Scenario
You received the Health Survey spec above from your research team. Your task: create the DDI document with all questions, build the flow logic with Sequences and IfThenElse branches, and connect everything to an Instrument.
Exercises¶
-
Create a study for the Health Survey. Add all 6 questions from the spec. Print the count.
Expected output:
-
Add a
QuestionConstructfor each question and aSequencefor each section (A, B, C). Print the counts.Expected output:
-
Add two
IfThenElseitems: one for the age gate ("Skip Section B if age < 16") and one for the occupation routing ("Ask B2 only if B1 = Yes"). Print the count.Expected output:
-
Add the main
Sequence, aStatementItemfor the welcome message, and anInstrument. Print the final counts.doc.add_item(Sequence, name="Main Survey Flow") doc.add_item(StatementItem, name="Welcome") doc.add_item(Instrument, name="Health Survey Instrument") print(f"Sequences: {len(doc.items(Sequence))}") print(f"StatementItems: {len(doc.items(StatementItem))}") print(f"Instruments: {len(doc.items(Instrument))}")Expected output:
-
(Bonus) Draw a flowchart of your own survey (real or imaginary) on paper. Label each box with the DDI construct type. Then build it in Python with
add_item()and link the constructs with.to_reference().
Quiz¶
Q1: What DDI construct represents an ordered section of a questionnaire?
A. Instrument
B. Sequence
C. IfThenElse
D. QuestionConstruct
Answer
B. A Sequence groups steps in order, like a section
of a questionnaire.
Q2: How do you model 'skip to Section C if age < 16' in DDI?
A. Delete the questions for Section B.
B. Use a StatementItem.
C. Use an IfThenElse with a condition on age.
D. Use a Loop.
Answer
C. An IfThenElse checks the age condition and routes
the respondent to Section B (then) or Section C (else).
Q3: What is the purpose of a QuestionConstruct?
A. It defines the text of a question.
B. It wraps a question so it can be placed in a Sequence.
C. It creates a code list for a question.
D. It validates a question.
Answer
B. A QuestionConstruct wraps a question reference so
the question can participate in the flow logic (be placed
in a Sequence, be the target of an IfThenElse, etc.).
Q4: What DDI construct repeats a section for each household member?
A. Sequence
B. IfThenElse
C. ElseIf
D. Loop
Answer
D. A Loop repeats a control construct (usually a
Sequence) for each item in a list, such as each household
member.
Q5: What is the first step when mapping a questionnaire spec to DDI?
A. Create the Instrument.
B. Build the main Sequence.
C. List all questions and create QuestionConstructs.
D. Write the XML by hand.
Answer
C. Start by identifying all the questions in the spec and creating a QuestionConstruct for each one. Then build the Sequences and flow logic around them.
Instructor notes
- Start with the paper spec on screen. Ask learners to circle every question, underline every skip rule, and box every "repeat" instruction. This makes the mapping concrete before touching code.
- Draw the flowchart on a whiteboard. Label each box with the DDI construct name. Then translate box-by-box to Python.
- Stress the
.to_reference()habit: every time one construct points to another (a Sequence to its steps, an IfThenElse to its branches), the link is a reference, not the object itself. References keep the file valid because they carry the target's identifier and type. - Common confusion: "Why do I need both a Question and a QuestionConstruct?" Answer: the Question is the content ("What is your age?"). The QuestionConstruct is the instruction ("now ask this question"). The Sequence says "ask these in this order." Separating content from flow lets you reuse the same question in different instruments.
- For the Loop: show a household roster form where the same section repeats per person. Ask: "How many times does this section run?" Answer: "It depends on how many people live in the household." That is a loop.
- Use ElseIf for multi-way branches: "If age < 16 → skip. If 16-64 → ask employment. If 65+ → ask retirement." This maps to one IfThenElse with ElseIf branches.
- Exercise 5 (draw your own spec) is excellent for group work. Pairs can swap specs and map each other's questionnaires.
See also: Module 7: Code lists | User guide: Any item type