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 twiceEvery 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.
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 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.
demo <- sframe_demo("first_survey")
demo$instrument
#> <sframe>
#> Title: Event feedback
#> Version: 1.0.0
#> Items: 6
#> Scales: 0
#> Analysis: 4 block(s)
#> Status: validFour 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? qualityAnd 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.
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.
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 < .001mg <- 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").
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.8473312One item is reverse worded and declared with
reverse = TRUE, so scoring handles it and you do not have
to remember.
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.
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.
br <- sframe_demo("branching")
sf_branches(br$instrument)[[1]]
#> <sf_branch: sessions_attended>
#> Rule: show when attendee_type %in% delegate, speakerA blank left by skip logic is structural, not missing data. The respondent was never asked. That distinction matters when you report completeness.
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.2Every 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 3Applied to any demo with branded = TRUE, which changes
nothing on disk:
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.
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.
This is what separates surveyframe from a form builder, and it is worth seeing before you decide whether to use it.
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.
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.
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.
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.
Each demo comes with a Quarto notebook: load, read, run the plan, render a report, export for checking elsewhere.
Render it, then start replacing the demo with your own study.
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.