Answer Options Tutorial#

This Foundations tutorial focuses on option engineering only. It intentionally does not run inference.

You will learn two complementary paths:

  • Manual control with AnswerTexts + AnswerOptions when formatting must be exact.

  • Convenience setup with generate_likert_options(...) when you want fast setup and optional perturbations.

This tutorial is split into progressive blocks.

Each block explains:

  • what the code does

  • why you might use it in practice

import random

import pandas as pd

from qstn.prompt_builder import LLMPrompt, generate_likert_options
from qstn.utilities import placeholder
from qstn.utilities.constants import QuestionnairePresentation
from qstn.utilities.survey_objects import AnswerOptions, AnswerTexts

1. Minimal Setup#

We start with a very small questionnaire and one reusable prompt template built with placeholders.

This gives us a clean baseline, so each option change is easy to see and compare.

questionnaire_df = pd.DataFrame(
    [
        {
            "questionnaire_item_id": 1,
            "question_content": (
                "How satisfied are you with the public healthcare system "
                "in your area?"
            ),
        },
        {
            "questionnaire_item_id": 2,
            "question_content": (
                "How satisfied are you with the quality of public education "
                "in your area?"
            ),
        },
        {
            "questionnaire_item_id": 3,
            "question_content": (
                "How satisfied are you with local public transportation "
                "where you live?"
            ),
        },
    ]
)

base_prompt = LLMPrompt(
    questionnaire_name="OptionsDemo",
    questionnaire_source=questionnaire_df,
    system_prompt="You are a careful survey respondent.",
    prompt=(
        f"Question: {placeholder.PROMPT_QUESTIONS}\n"
        f"{placeholder.PROMPT_OPTIONS}\n"
        "Answer with exactly one option."
    ),
)
   questionnaire_item_id                                   question_content
0                      1  How satisfied are you with the public healthca...
1                      2  How satisfied are you with the quality of publ...
2                      3  How satisfied are you with local public transp...

2. Block A: Manual Full Control (AnswerTexts + AnswerOptions)#

In this block, we define indices, separators, and wording ourselves.

Use this route when you care about exact survey coding, strict formatting, or domain-specific label text.

manual_texts = AnswerTexts(
    answer_texts=["Very dissatisfied", "Dissatisfied", "Neutral", "Satisfied", "Very satisfied"],
    indices=["01", "02", "03", "04", "05"],
    index_answer_seperator=" => ",
    option_seperators=" || ",
)

manual_options = AnswerOptions(
    answer_texts=manual_texts,
    list_prompt_template="Choose one satisfaction option: {options}",
)

manual_prompt = base_prompt.duplicate()
manual_prompt.prepare_prompt(
    question_stem=f"Question: {placeholder.QUESTION_CONTENT}",
    answer_options=manual_options,
)

_, manual_user_text = manual_prompt.get_prompt_for_questionnaire_type(
    questionnaire_type=QuestionnairePresentation.SINGLE_ITEM,
    item_position=0,
)

print("OPTIONS STRING:")
print(manual_options.create_options_str())
print("\nRENDERED PROMPT:")
print(manual_user_text)
OPTIONS STRING:
Choose one satisfaction option: 01 => Very dissatisfied || 02 => Dissatisfied || 03 => Neutral || 04 => Satisfied || 05 => Very satisfied

RENDERED PROMPT:
Question: Question: How satisfied are you with the public healthcare system in your area?
Choose one satisfaction option: 01 => Very dissatisfied || 02 => Dissatisfied || 03 => Neutral || 04 => Satisfied || 05 => Very satisfied
Answer with exactly one option.

3. Block B: Manual Scale Mode (from_to_scale=True)#

Here we keep manual control but present the answers as a compact start-to-end scale.

This is helpful when you want shorter prompts while still preserving the intended range semantics.

manual_scale_texts = AnswerTexts(
    answer_texts=["Very dissatisfied", "Dissatisfied", "Neutral", "Satisfied", "Very satisfied"],
    indices=["1", "2", "3", "4", "5"],
    only_scale=True,
)

manual_scale_options = AnswerOptions(
    answer_texts=manual_scale_texts,
    from_to_scale=True,
    scale_prompt_template="Use the scale from {start} to {end}.",
)

scale_prompt = base_prompt.duplicate()
scale_prompt.prepare_prompt(answer_options=manual_scale_options)
_, scale_user_text = scale_prompt.get_prompt_for_questionnaire_type(
    questionnaire_type=QuestionnairePresentation.SINGLE_ITEM,
    item_position=1,
)

print("OPTIONS STRING:")
print(manual_scale_options.create_options_str())
print("\nRENDERED PROMPT:")
print(scale_user_text)
OPTIONS STRING:
Use the scale from 1: Very dissatisfied to 5: Very satisfied.

RENDERED PROMPT:
Question: How satisfied are you with the quality of public education in your area?
Use the scale from 1: Very dissatisfied to 5: Very satisfied.
Answer with exactly one option.

4. Block C: generate_likert_options Baseline#

Now we build a similar setup with generate_likert_options(...) to reduce boilerplate.

This is usually the fastest starting point when defaults are close to what you need.

generated_baseline = generate_likert_options(
    n=5,
    answer_texts=["Very dissatisfied", "Dissatisfied", "Neutral", "Satisfied", "Very satisfied"],
    list_prompt_template="Options are: {options}",
    options_separator=" | ",
)

generated_prompt = base_prompt.duplicate()
generated_prompt.prepare_prompt(answer_options=generated_baseline)
_, generated_user_text = generated_prompt.get_prompt_for_questionnaire_type(
    questionnaire_type=QuestionnairePresentation.SINGLE_ITEM,
    item_position=0,
)

print("GENERATED OPTIONS STRING:")
print(generated_baseline.create_options_str())
print("\nRENDERED PROMPT:")
print(generated_user_text)
GENERATED OPTIONS STRING:
Options are: 1: Very dissatisfied | 2: Dissatisfied | 3: Neutral | 4: Satisfied | 5: Very satisfied

RENDERED PROMPT:
Question: How satisfied are you with the public healthcare system in your area?
Options are: 1: Very dissatisfied | 2: Dissatisfied | 3: Neutral | 4: Satisfied | 5: Very satisfied
Answer with exactly one option.

5. Block D: Full Parameter Tour (generate_likert_options)#

Next we walk through the most important parameter groups.

Each example prints the option string and a short prompt snippet so you can quickly see how the prompt surface changes.

variant_prompt = base_prompt.duplicate()
variant_prompt.prepare_prompt(question_stem=f"Question: {placeholder.QUESTION_CONTENT}")

def render_variant(name, options_obj):
    demo = variant_prompt.duplicate()
    demo.prepare_prompt(answer_options=options_obj)
    _, user_text = demo.get_prompt_for_questionnaire_type(
        questionnaire_type=QuestionnairePresentation.SINGLE_ITEM,
        item_position=0,
    )
    snippet = user_text.splitlines()[:3]
    print(f"=== {name} ===")
    print(options_obj.create_options_str())
    print("Prompt snippet:")
    for line in snippet:
        print(line)
    print()

labels5 = ["Very dissatisfied", "Dissatisfied", "Neutral", "Satisfied", "Very satisfied"]
labels4_even = ["Very dissatisfied", "Dissatisfied", "Satisfied", "Very satisfied"]

5.1 Ordering Perturbations#

Start with order changes to see how the same semantic labels can be presented differently.

random.seed(42)

render_variant(
    "random_order=True",
    generate_likert_options(n=5, answer_texts=labels5.copy(), random_order=True),
)

render_variant(
    "reversed_order=True",
    generate_likert_options(n=5, answer_texts=labels5.copy(), reversed_order=True),
)
=== random_order=True ===
Options are: 1: Satisfied, 2: Dissatisfied, 3: Neutral, 4: Very satisfied, 5: Very dissatisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: 1: Satisfied, 2: Dissatisfied, 3: Neutral, 4: Very satisfied, 5: Very dissatisfied
Answer with exactly one option.

=== reversed_order=True ===
Options are: 1: Very satisfied, 2: Satisfied, 3: Neutral, 4: Dissatisfied, 5: Very dissatisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: 1: Very satisfied, 2: Satisfied, 3: Neutral, 4: Dissatisfied, 5: Very dissatisfied
Answer with exactly one option.

5.2 Scale Shape and Refusal Handling#

These options change the structure of the scale itself, including midpoint and refusal behavior.

render_variant(
    "even_order=True (n starts odd)",
    generate_likert_options(n=5, answer_texts=labels5.copy(), even_order=True),
)

render_variant(
    "add_middle_category=True (n starts even)",
    generate_likert_options(
        n=4,
        answer_texts=labels4_even.copy(),
        add_middle_category=True,
        str_middle_cat="Neutral",
    ),
)

render_variant(
    "add_refusal=True + refusal_code='98'",
    generate_likert_options(
        n=5,
        answer_texts=labels5.copy(),
        add_refusal=True,
        refusal_code="98",
    ),
)

render_variant(
    "only_from_to_scale=True",
    generate_likert_options(
        n=5,
        answer_texts=labels5.copy(),
        only_from_to_scale=True,
        idx_type="integer",
    ),
)
=== even_order=True (n starts odd) ===
Options are: 1: Very dissatisfied, 2: Dissatisfied, 3: Satisfied, 4: Very satisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: 1: Very dissatisfied, 2: Dissatisfied, 3: Satisfied, 4: Very satisfied
Answer with exactly one option.

=== add_middle_category=True (n starts even) ===
Options are: 1: Very dissatisfied, 2: Dissatisfied, 3: Neutral, 4: Satisfied, 5: Very satisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: 1: Very dissatisfied, 2: Dissatisfied, 3: Neutral, 4: Satisfied, 5: Very satisfied
Answer with exactly one option.

=== add_refusal=True + refusal_code='98' ===
Options are: 1: Very dissatisfied, 2: Dissatisfied, 3: Neutral, 4: Satisfied, 5: Very satisfied, 98: Don't know / Refuse to answer
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: 1: Very dissatisfied, 2: Dissatisfied, 3: Neutral, 4: Satisfied, 5: Very satisfied, 98: Don't know / Refuse to answer
Answer with exactly one option.

=== only_from_to_scale=True ===
Options range from 1: Very dissatisfied to 5: Very satisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options range from 1: Very dissatisfied to 5: Very satisfied
Answer with exactly one option.

5.3 Indexing Strategies#

Use this to choose whether options are numbered, lettered, or shown without indices.

render_variant(
    "idx_type='integer' + start_idx=0",
    generate_likert_options(n=5, answer_texts=labels5.copy(), idx_type="integer", start_idx=0),
)

render_variant(
    "idx_type='char_lower'",
    generate_likert_options(n=5, answer_texts=labels5.copy(), idx_type="char_lower", start_idx=0),
)

render_variant(
    "idx_type='char_upper'",
    generate_likert_options(n=5, answer_texts=labels5.copy(), idx_type="char_upper", start_idx=0),
)

render_variant(
    "idx_type='no_index'",
    generate_likert_options(n=5, answer_texts=labels5.copy(), idx_type="no_index"),
)
=== idx_type='integer' + start_idx=0 ===
Options are: 0: Very dissatisfied, 1: Dissatisfied, 2: Neutral, 3: Satisfied, 4: Very satisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: 0: Very dissatisfied, 1: Dissatisfied, 2: Neutral, 3: Satisfied, 4: Very satisfied
Answer with exactly one option.

=== idx_type='char_lower' ===
Options are: a: Very dissatisfied, b: Dissatisfied, c: Neutral, d: Satisfied, e: Very satisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: a: Very dissatisfied, b: Dissatisfied, c: Neutral, d: Satisfied, e: Very satisfied
Answer with exactly one option.

=== idx_type='char_upper' ===
Options are: A: Very dissatisfied, B: Dissatisfied, C: Neutral, D: Satisfied, E: Very satisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: A: Very dissatisfied, B: Dissatisfied, C: Neutral, D: Satisfied, E: Very satisfied
Answer with exactly one option.

=== idx_type='no_index' ===
Options are: Very dissatisfied, Dissatisfied, Neutral, Satisfied, Very satisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Options are: Very dissatisfied, Dissatisfied, Neutral, Satisfied, Very satisfied
Answer with exactly one option.

5.4 Formatting Templates and Separators#

Finally, tune how the options are rendered in text so the prompt matches your questionnaire style.

render_variant(
    "custom templates + separators",
    generate_likert_options(
        n=5,
        answer_texts=labels5.copy(),
        list_prompt_template="Selectable answers => {options}",
        scale_prompt_template="Scale start={start}; end={end}",
        index_answer_separator=" -> ",
        options_separator=" // ",
    ),
)
=== custom templates + separators ===
Selectable answers => 1 -> Very dissatisfied // 2 -> Dissatisfied // 3 -> Neutral // 4 -> Satisfied // 5 -> Very satisfied
Prompt snippet:
Question: Question: How satisfied are you with the public healthcare system in your area?
Selectable answers => 1 -> Very dissatisfied // 2 -> Dissatisfied // 3 -> Neutral // 4 -> Satisfied // 5 -> Very satisfied
Answer with exactly one option.

6. Block E: Per-Question Option Control with a Dict#

prepare_prompt(answer_options={...}) lets each questionnaire item use its own option style.

This matches real surveys where different items often require different scales or perturbation settings.

manual_item_options = AnswerOptions(
    answer_texts=AnswerTexts(
        answer_texts=[
            "Very dissatisfied",
            "Dissatisfied",
            "Neutral",
            "Satisfied",
            "Very satisfied",
        ],
        indices=["1", "2", "3", "4", "5"],
        index_answer_seperator=": ",
        option_seperators=", ",
    ),
    list_prompt_template="Choose one satisfaction option: {options}",
)

perturbed_item_options = generate_likert_options(
    n=5,
    answer_texts=[
        "Very dissatisfied",
        "Dissatisfied",
        "Neutral",
        "Satisfied",
        "Very satisfied",
    ],
    reversed_order=True,
    options_separator=" | ",
)

mixed_prompt = base_prompt.duplicate()
mixed_prompt.prepare_prompt(
    question_stem=f"Question: {placeholder.QUESTION_CONTENT}",
    answer_options={
        1: manual_item_options,
        2: perturbed_item_options,
    },
)

_, item1_text = mixed_prompt.get_prompt_for_questionnaire_type(
    questionnaire_type=QuestionnairePresentation.SINGLE_ITEM,
    item_position=0,
)
_, item2_text = mixed_prompt.get_prompt_for_questionnaire_type(
    questionnaire_type=QuestionnairePresentation.SINGLE_ITEM,
    item_position=1,
)

print("=== Item 1 (manual options) ===")
print(item1_text)
print("\n=== Item 2 (generated + reversed) ===")
print(item2_text)
=== Item 1 (manual options) ===
Question: Question: How satisfied are you with the public healthcare system in your area?
Choose one satisfaction option: 1: Very dissatisfied, 2: Dissatisfied, 3: Neutral, 4: Satisfied, 5: Very satisfied
Answer with exactly one option.

=== Item 2 (generated + reversed) ===
Question: Question: How satisfied are you with the quality of public education in your area?
Options are: 1: Very satisfied | 2: Satisfied | 3: Neutral | 4: Dissatisfied | 5: Very dissatisfied
Answer with exactly one option.

7. Block F: Practical Guardrails + Quick Recap#

Key constraints to remember:

  • len(answer_texts) must match n.

  • even_order=True requires odd n before removing the middle option.

  • add_middle_category=True requires even n before insertion.

  • only_from_to_scale=True requires idx_type="integer".

When to choose which path:

  • Choose manual AnswerTexts + AnswerOptions for full and stable formatting control.

  • Choose generate_likert_options for speed, readability, and perturbation experiments.