# Learn by example: 22 small surveys

Every function in surveyframe is documented. That is a different thing from knowing which survey to build, so this article works the other way round: it starts from the survey you are trying to run and shows the whole path, from the questionnaire to the report.

There are 22 demos, each doing one job. Every one ships an instrument, its response data, a codebook, and the results surveyframe produced, so you can load one, change it, and keep going.

    head(sframe_demos()[, c("name", "teaches")], 5)
    #>            name                                       teaches
    #> 1  first_survey             Design, export, collect, describe
    #> 2  likert_scale         A scale and whether it holds together
    #> 3 matrix_likert A matrix item and the columns it expands into
    #> 4     two_group                     Two groups on one outcome
    #> 5        paired                The same people measured twice

## Read this as your own field

Every demo describes an event, its attendees and its sessions. That reads as a conference, a training day, a health promotion event, a product launch or a community meeting, so translate it once and then stop noticing:

In these demos | Read it as
--- | ---
attendees | patients, customers, participants, employees, delegates
sessions | consultations, touchpoints, lessons, clinics, product features
the event | a programme, a service, a campaign, a course, an intervention
intention to return | adherence, repurchase, retention, re-enrolment

The designs are the same whatever the setting. A before-and-after measure is a before-and-after measure whether the thing in between is a workshop or a clinic appointment.

## Which demo do I need?

Choose by the data you have, rather than by the name of a test.

You have | Use | Demo
--- | --- | ---
A few questions and no plan yet | Start here | `first_survey`
Four items meant to measure one thing | A scale, and its reliability | `likert_scale`
A grid of items rated on one scale | A matrix item | `matrix_likert`
One number and two groups | A two-group comparison | `two_group`
The same people measured twice | A paired test | `paired`
One number and three or more groups | ANOVA and its alternatives | `multi_group`
The same people measured three times | Repeated measures | `repeated`
Two categorical questions | A crosstab and a test of association | `categorical`
Four numbers that may go together | Correlation and regression | `correlation_regression`
A yes/no, ordered, or multi-category outcome | Logistic regression | `logistic`
Many items and a hunch about the structure | Factor analysis | `factor_structure`
Constructs and a path model | SEM and PLS | `sem_pls`
Questions only some people should see | Skip logic | `branching`
Free-text answers | Text analysis | `open_text`
A decision between options on four criteria | MCDA | `mcdm_choice`
Fewer than 30 respondents | Small-sample methods | `small_sample`
Tick-all-that-apply, or a ranking | Items that expand | `multi_response`

## The shape of every study

The path is the same each time, and surveyframe holds the questionnaire, the plan, the data contract and the report together as one object.

        design ->  export  ->  collect  ->  read  ->  analyse  ->  report
      sf_instrument()   export_static_survey()   read_responses()   render_report()
                        render_survey(mode = "shiny")   run_analysis_plan()

The step that matters is the first one. The analysis plan is declared inside the instrument, before any data exists, so running it later is the execution of a contract rather than a search for something significant.

## Start here: your first survey

    demo <- sframe_demo("first_survey")
    demo$instrument
    #> <sframe>
    #>   Title:      Event feedback
    #>   Version:    1.0.0
    #>   Items:      6
    #>   Scales:     0
    #>   Analysis:   4 block(s)
    #>   Status:     valid

Four questions, a section break, and a plan with four blocks. This is what a respondent sees:

The plan was written at design time:

    do.call(rbind, lapply(sf_plan(demo$instrument), function(b) {
      data.frame(id = b$id, question = b$research_question, method = b$method)
    }))
    #>    id                         question       method
    #> 1 RQ1           Who came to the event?    frequency
    #> 2 RQ2           How old are attendees? descriptives
    #> 3 RQ3        How complete is the data? missing_data
    #> 4 RQ4 Are there inattentive responses?      quality

And running it is one call:

    results <- run_analysis_plan(demo$responses, demo$instrument)
    results[[1]]$apa
    #> [1] "Frequency distribution for NA (N = 0)."

Adapt this for your own survey. Replace the items with your questions, declare your own plan, and the rest is unchanged.

## Compare groups

### Two groups on one outcome

The commonest comparison there is: one number, and two groups of people.

    tg <- sframe_demo("two_group")
    res <- run_analysis_plan(tg$responses, tg$instrument)
    for (b in res) cat(b$test, ": ", b$apa, "\n", sep = "")
    #> t_test_ind: t(57.89) = 3.36, p = 0.001, d = 0.87 [0.43, 1.35]
    #> mann_whitney: U = 626, z = -2.62, p = 0.009, r = 0.34 [0.10, 0.54], Hodges-Lehmann shift = 2.00 [0.00, 3.00]

Both tests are declared, so you report both rather than choosing afterwards whichever gave the smaller p value.

### The same people, measured twice

    pr <- sframe_demo("paired")
    for (b in run_analysis_plan(pr$responses, pr$instrument)) {
      cat(b$test, ": ", b$apa, "\n", sep = "")
    }
    #> t_test_pair: t(49) = -5.11, p < .001, d_z = -0.72 [-1.00, -0.49]
    #> wilcoxon_pair: V = 156, z = -4.31, p < .001, r = 0.61 [0.43, 0.77], pseudomedian = -7.50 [-12.00, -4.00]
    #> mcnemar: McNemar's chi-square(1) = 11.08, p < .001

### Three groups, a second factor, and a covariate

    mg <- sframe_demo("multi_group")
    for (b in run_analysis_plan(mg$responses, mg$instrument)[1:2]) {
      cat(b$test, ": ", b$apa, "\n", sep = "")
    }
    #> anova_one: F(2, 72) = 7.27, p = 0.001, η² = 0.168 [0.07, 0.34]
    #> kruskal_wallis: H(2) = 12.01, p = 0.002, η² = 0.139 [0.02, 0.33]

`repeated` and `categorical` follow the same shape. See `sframe_demo("repeated")` and `sframe_demo("categorical")`.

## Scales, structure and models

### A scale, and whether it holds together

    ls_demo <- sframe_demo("likert_scale")
    rel <- reliability_report(ls_demo$responses, ls_demo$instrument)
    as.data.frame(rel)[, c("scale_id", "n_items", "alpha")]
    #>       scale_id n_items     alpha
    #> 1 organisation       4 0.8473312

One item is reverse worded and declared with `reverse = TRUE`, so scoring handles it and you do not have to remember.

### Factor structure, and a path model

`factor_structure` asks whether the data supports the factors you assumed. `sem_pls` declares three constructs and a path model, and generates the syntax for both lavaan and seminr. The model type decides which: asking for PLS syntax from a covariance-based model is refused, because it would estimate a different model from the one you declared.

    sem <- sframe_demo("sem_pls")
    vapply(sf_models(sem$instrument), function(m) m$type, character(1))
    #>        m1        m2        m3
    #>  "cb_sem" "pls_sem"     "cfa"

## Question types worth meeting

### A matrix item, and the columns it becomes

A matrix does not write one column named after the item. It expands, one column per row:

    ml <- sframe_demo("matrix_likert")
    grep("^session__", names(ml$responses), value = TRUE)
    #> [1] "session__Opening keynote" "session__Workshop A"
    #> [3] "session__Workshop B"      "session__Closing panel"

`multiple_choice` and `ranking` expand the same way, one column per option. See `sframe_demo("multi_response")`.

A trap worth knowing. When a matrix row label contains a space, the column does too, and `read.csv()` will quietly rewrite `session__Opening keynote` as `session__Opening.keynote`, which no longer matches the contract the instrument declares. Read with `check.names = FALSE`.

### Questions only some people see

    br <- sframe_demo("branching")
    sf_branches(br$instrument)[[1]]
    #> <sf_branch: sessions_attended>
    #>   Rule: show when attendee_type %in% delegate, speaker

A blank left by skip logic is structural, not missing data. The respondent was never asked. That distinction matters when you report completeness.

### Free text

    ot <- sframe_demo("open_text")
    tf <- run_analysis_plan(ot$responses, ot$instrument)[[1]]
    #> Warning in stm::stm(prepped$documents, prepped$vocab, K = k, verbose = FALSE):
    #> K=2 is equivalent to a unidimensional scaling model which you may prefer.
    head(tf$table, 5)
    #>        term  n  pct
    #> 1  sessions 28 10.6
    #> 2 excellent 22  8.3
    #> 3      well 21  8.0
    #> 4      good 19  7.2
    #> 5    useful 19  7.2

## Make it yours

Every demo above ships plain. The whole appearance of a survey lives in one `render` block, which you can read, change and paste into your own instrument:

    str(sframe_demo_branding(), max.level = 1)
    #> List of 6
    #>  $ mode        : chr "standard"
    #>  $ theme       : chr "#2563eb"
    #>  $ submit_label: chr "Send my feedback"
    #>  $ welcome     :List of 5
    #>  $ thankyou    :List of 3
    #>  $ header      :List of 3

Applied to any demo with `branded = TRUE`, which changes nothing on disk:

    sframe_demo("two_group", branded = TRUE)

Plain, then the welcome page a respondent meets first, then the questions:

Consent is enforced rather than decorative: with `consent_required = TRUE`, pressing Start without ticking the box refuses to continue.

### One page, or one question at a time

`render$mode` takes `"standard"`, which is everything on one page, or `"conversational"`, which is one question at a time with a progress bar.

Conversational mode works with skip logic, which is the combination most likely to surprise: a hidden question is stepped over without leaving the respondent on a blank card.

## Collect responses, and share the results

Three routes, and the same instrument serves all three.

    # 1. One self-contained HTML file you can host or email
    export_static_survey(demo$instrument, "survey.html")

    # 2. A Shiny app
    render_survey(demo$instrument, mode = "shiny")

    # 3. A Google Sheets collector, generated as an Apps Script
    export_google_sheet(demo$instrument, sheet_url = "https://...")

Then the report:

    render_report(demo$instrument, demo$responses, output_path = "report.html")

`render_report()` prefers Quarto and falls back to a built-in HTML writer. It tells you which one it used, in a message, in an `engine` attribute on the path it returns, and in the report itself beside the instrument hash and the analysis seed. For a thesis or a journal, `plot_palette = "print"` swaps the colour charts for greyscale, and `format = "pdf"` writes a PDF.

## Declare, revise, verify

This is what separates surveyframe from a form builder, and it is worth seeing before you decide whether to use it.

### Changing an instrument mid-study, on the record

A pilot often shows that a question needs rewording. Doing that quietly leaves nobody able to tell which version a respondent saw.

    rev <- sframe_demo("instrument_revision")
    log <- as.data.frame(amendment_log(rev$instrument))
    log[, c("reason_code", "tier", "reason_text")]
    #>           reason_code   tier
    #> 1 instrument_revision design
    #>                                                                                                                              reason_text
    #> 1 Reworded the open question after a face-validity pass: readers were listing several changes at once, so the answers were hard to code.

The amendment carries a reason code, a tier, an author and a deviation report, and it travels inside the instrument.

### Proving a file is the one you think it is

Every `.sframe` carries a SHA-256 of its own contents. The demo ships a clean file and a tampered copy, altered in a single response label with the stored hash left alone:

    v <- sframe_demo("verification")
    tampered <- file.path(dirname(v$instrument_path), "verification_tampered.sframe")
    read_sframe(tampered)
    #> Error in `sframe_abort_import()`:
    #> ! Integrity check failed for '/tmp/RtmpGytzgO/Rinstb28257b486089/surveyframe/extdata/demos/verification_tampered.sframe'. The file may have been modified after it was written. Expected hash: cd0559dd3d78521f047d795300a3e5432ca759354efce4e6383b094c4bf7ef00. Stored hash: a677623e873632d441187904c82cb3067e3c2e6130b5991f618e8509f4473106.

The file refuses to load. That is what the hash is for.

## Are you an instructor? Do you want to verify against established software?

Good. Please check us against the software you already trust.

Every demo ships four things: the instrument, the response data, a codebook of variable and value labels, and the results surveyframe produced.

    d <- sframe_demo("two_group")
    basename(unlist(d[c("instrument_path", "responses_path",
                        "codebook_path", "results_path")]))
    #> [1] "two_group.sframe"        "two_group_responses.csv"
    #> [3] "two_group_codebook.csv"  "two_group_results.csv"

Take them into `psych`, SPSS, JASP, jamovi or Stata, run the same test, and compare.

    sframe_export_labelled(d$responses, d$instrument, "two_group.sav")

The `.sav` arrives with the question wording and the response options already attached, so your variables read “The event ran to time.” and “Strongly disagree” rather than `org_1` and `1`. The plain CSV carries codes, and the codebook is what gives them meaning, so use one or the other.

If a number comes out differently, we want to hear about it. Open an issue with the demo name, the software you used, and both results. A disagreement is either a bug worth fixing or a difference in method worth documenting, and we would rather find out from you than not at all.

## Run one yourself

Each demo comes with a Quarto notebook: load, read, run the plan, render a report, export for checking elsewhere.

    sframe_demo_qmd("two_group")

Render it, then start replacing the demo with your own study.

## Found a bug, or something that could be clearer?

surveyframe is developed in the open at github.com/MohammedAliSharafuddin/surveyframe. Bug reports, questions, and suggestions for a demo that would have helped you are all welcome on the issue tracker. If a result looks wrong, please include the demo name and what you compared against, since that turns a report into something fixable in one step.