A defect-fix release. An external review of 0.4.1 found defects that silently alter or lose data, and this release corrects them. Collection defects come first, because an answer recorded wrongly is lost for good. This section grows as each group of fixes lands.
A Shiny-collected response now carries a
respondent_id, as the first column, holding a
generated identifier such as RK3P8QX2A. The static survey
has written one since the first release, so the two collection routes
produced different column sets for the same instrument, leaving the
duplicate check and a retry to work from whatever they could find.
If you are collecting to a CSV written by 0.4.1 or
earlier, that file predates the column:
render_survey() keeps appending to it and says once that
the response went in unidentified. Collect into a new file to record an
identifier for every response. Anything reading these files by column
position needs updating to read by name.
survey_module_server() returns a different
response shape. The response is now the collection row, as a
list: response_id, started_at,
submitted_at, then one element per response column, named
as read_responses() expects. A multiple-choice question
becomes one element per option holding "1" or
"0", and a matrix, ranking or decision question becomes one
element per row, option, pair or criterion. Values are character. An
item hidden by branching, or left unanswered, is NA. The
module previously returned one raw input value per item, and lacked
controls for matrix, ranking, rating and decision questions.
Untouched sliders, rankings and dates are now
unanswered in render_survey() and the survey
module. A slider counts once the respondent moves it, and a ranking once
it is reordered or confirmed with “Keep this order”. Previously a slider
stored its starting position, a ranking stored the declared order, and a
date question in the module stored today’s date, for anyone who moved
past them.
render_survey(save_responses = "csv")
refuses a response file written for different questions, when
the app starts and on each submission. Collect a changed instrument into
a new file. A file with the same columns in another order is still
accepted, and rows are aligned by column name.
Google Sheets collection now needs the Sheets advanced
service. The generated collector stores an answer through the
Sheets API’s RAW option, which is the documented way to
store a value without the spreadsheet parsing it. Regenerate and
redeploy the collector with export_google_sheet(), and in
the Apps Script editor add Services > Google Sheets API with the
identifier Sheets. A collector lacking it refuses
each response and replies with an error, in place of storing an
answer it would have to alter. Code that reads the sheet directly,
bypassing read_sheet_responses(), should expect text cells
where numbers appeared before.
Scale scores and reliability can change. Re-run
analyses of scales that reverse-code items, share items with other
scales, or had an absent item column. Reverse coding now applies within
the scale that declares it, a scale with an absent item column counts
that item as unanswered, and report figures now show the scores
score_scales() computes.
Some instruments that validated before are now
rejected. An item and a scale sharing an ID, an ID equal to a
response column or to respondent_id,
response_id, started_at or
submitted_at, reverse coding for an item outside the scale
declaring it, repeated scale items, a min_valid outside 1
to the number of items, and weights that are zero, negative or infinite
are all reported by validate_sframe(), and
sf_scale() refuses the scale parameters directly. Rename or
correct the declaration.
A reverse-coded item needs declared response
bounds: a numeric choice set, slider_min and
slider_max, or a rating maximum. Scoring reports an error
for a reversed item that has none.
A factor column is scored through its labels. A factor whose labels are text is an error. Convert it to numeric codes first.
Some statistics change. Re-run analyses using Cochran’s Q with missing answers, ANCOVA, partial correlation, a Firth likelihood ratio, PLS-SEM constructs with non-consecutive indicators, or a Mann-Whitney or Wilcoxon signed-rank test. The rank tests now use the normal approximation without continuity correction for z, p, r and its interval alike, so their p values move slightly, and z is now signed by the direction of the difference.
Repeated-measures ANOVA now uses complete
respondents. A respondent missing any repeated measure is
excluded as a unit before fitting the classical balanced model. Results
report the retained n and
n_excluded_incomplete. This replaces a singular fit on
incomplete rows.
ANCOVA tables hold adjusted tests, each term
tested after all others, with columns effect,
df, sum_sq, mean_sq,
F and p.
sample_size_plan() now calculates
power for t tests, ANOVA and, with the new f2,
regression. The new d and f arguments give the
expected effect, and a medium effect is assumed, with a warning, when
they are left out. Estimates now depend on alpha and
power.
run_analysis_plan() refuses repeated block
IDs and gains strict. Its results carry a
status attribute counting failed blocks, so check it, or
set strict = TRUE, before treating a returned object as
success.
Bootstrap intervals can be withheld.
bootstrap_ci(), cohens_d_ci(),
cramers_v_ci() and eta_sq_ci() return
NA bounds with a reason attribute when fewer
than 90% of resamples give a value or all give the same value, and every
result records its resample counts.
Decision rankings can change. Re-run TOPSIS, VIKOR, MOORA, SMART, WASPAS, PROMETHEE and ELECTRE analyses whose weights or criterion types were named in a different order from the performance matrix, AHP and ANP analyses given a data frame, and aggregations of judgement matrices named in different orders. Weights, criterion types and judgement matrices are now matched by criterion name.
Some decision inputs that ran before are now
refused: weights naming different criteria from the matrix,
supplied AHP matrices that are not positive, unit-diagonal and
reciprocal, WASPAS values of zero or below, infinite values, a VIKOR
v or WASPAS lambda outside 0 to 1, ELECTRE
cutoffs outside 0 to 1, and PROMETHEE thresholds that are negative or
out of order. ANP refuses a reducible network, and DEMATEL a matrix
whose total relation does not exist.
ELECTRE’s kernel follows Roy’s definition, so it can hold more alternatives than before, and is reported as undefined when the outranking relation has a cycle.
sensitivity_analysis() results gain
n_perturbations, n_effective and
n_failed, and stable is
FALSE when no perturbation moved the weights. A requested
sensitivity run that could not be made is recorded in the result’s
sensitivity_error.
write_sframe() refuses an undisclosed
revision. An instrument read with read_sframe()
and then changed must record the change with amend_sframe()
before it is written, and its amendment log must stay complete and in
order. To publish changed content as a separate instrument, clear its
amendment log and pass new_instrument = TRUE. SurveyStudio
offers the same choice when exporting an edited file.
amend_sframe() sets the tier from what
changed. An amendment that changes the analysis plan, a model
or a conjoint design is design tier and needs a
deviation_report, whatever reason_code or
tier says.
.sframe files write one-member collections
as arrays, as the published instrument profile and the builder
already did. Reading an older file and writing it again gives it a new
hash where it held such a collection. Older files still read and verify
as they are.
read_responses() reads a CSV file as
text and converts only columns of items with numeric responses.
Identifiers such as 001 and metadata columns now arrive as
text, and numeric answers as doubles. A response file with 2 columns of
the same name is refused.
link_git_commit() gains path
and returns verified, which is TRUE
only when the instrument matches that file as committed.
linked alone confirms a repository and a commit.
Some shipped demo results changed to match the
corrected statistics: likert_scale, two_group,
paired, multi_group, sem_pls,
mcdm_choice and small_sample.
The Shiny survey erased every answer. In
render_survey(), each answer re-drew the survey page, and
the re-drawn questions reported themselves empty, so every answer was
wiped about a second after it was given. A submitted response came back
blank. This affected every standard-mode survey run with
render_survey() in 0.4.1 and earlier. Answers now stay put,
and the page stays as it is when a question is answered.
The Google Sheets collector could turn an answer into a
formula. An answer beginning with = was evaluated
by the spreadsheet, so =1+1 was stored as 2,
and a code such as 007 lost its leading zeros. A stored
answer is now written through the Sheets API’s RAW option,
which stores it as submitted. A deployment lacking that option refuses
the response and says so, in place of altering it. The same fix applies
to collectors generated from the survey builder. See the setup note
above.
The exported survey said a response had been recorded
when it could not know. The page posts with
no-cors, which leaves the collector’s reply unreadable, so
the browser can establish that a request left and nothing further. The
thank-you screen claimed the response had been recorded, hid the
download, offered a restart that discarded the page’s only copy, and
could auto-redirect away from it. It now reports that the answers were
sent and that receipt is unconfirmed, keeps the download available,
withholds the restart that would clear the response, and leaves any
redirect for the participant to choose.
A custom thank-you message no longer shows on a survey
with a collector. A message set through
render$thankyou$message is fixed text, written before
anyone knew how delivery would go, so a wording such as “your response
has been recorded” put a claim on screen beside the page’s own statement
that receipt was unconfirmed. The package’s own status is shown instead.
A custom message still shows on a download-only survey, where nothing is
being claimed about a collector. Put debrief text, contact details or
payment instructions on a final display item or in the redirect target,
where they always reach the participant.
The exported survey changed typed numbers.
Clearing a number field with a minimum wrote the minimum in, a number
outside the range was replaced with the nearest limit, and in a points
allocation 2.5 became 25. What the respondent
types is now kept, and the survey asks for a correction.
An option coded 0 was recorded as a blank in the exported survey, so a respondent choosing it on a required question was blocked from continuing, and 0 looked identical to a blank. Affected 0/1 codings and 0 to 10 scales.
A ranking could show one order and submit another in the exported survey after dragging, and an untouched ranking was submitted as if the respondent had chosen the order shown.
Shiny matrix and ranking questions showed codes in place
of labels. A five-point agreement scale appeared as
1 2 3 4 5. Shiny rankings also stored labels, so every rank
came out empty wherever labels and codes differed.
Shiny rankings needed a mouse. Each option now has move up and move down buttons, and the new position is announced.
Shiny decision questions started with an answer selected, “Equally important” or “No influence”, and points allocations started at 0, so an untouched question submitted an invented judgement. They now start empty. Every Shiny date question was also pre-filled with today’s date, and now starts empty.
Appending to a Shiny response file ignored its header, so a changed instrument wrote answers under another question’s heading. An existing empty file also received headerless rows.
The survey module lacked 5 item types. Matrix,
rating, ranking, pairwise comparison and criteria weight showed only a
placeholder, and a required one made the survey impossible to finish.
The module now uses the same questions as
render_survey().
The survey module submitted answers the respondent had removed. A cleared answer, and an answer to a question branching later hid, were both submitted. A multiple-choice answer lost all but its first selection when the page was revisited.
A failed save in the survey module showed the thank-you
screen. The survey was marked complete before
on_submit ran, and an error there ended the session. The
respondent now sees a message, stays on the page and can try
again.
Changing a reactive instrument now resets the survey module, as its help said it did, and starts every answer blank. Moving between pages scrolls the module into view, where it scrolled the whole host page.
The static survey said a response had been recorded
before it knew. A failed send was discarded and the thank-you
screen appeared anyway, and a configured redirect then carried the
participant away with the answers in nobody’s hands. A failure now says
so, keeps the CSV download reachable, offers a retry, and withholds the
redirect. The submission is a no-cors POST, so the
collector’s reply is unreadable and acceptance cannot be confirmed from
the page. The screen claims only that the request was sent.
A completed comparison or points allocation counted as unanswered in the progress display, because progress read the question’s own answer where these types store one answer per pair or per criterion. Progress and the required-question check now share one rule.
A rating left the question that depends on it hidden until another control was touched, while the required check still demanded an answer to it.
A comparison answered on a phone could show the wrong selection on a wider screen, and the reverse. Each question renders a button strip and a dropdown, shown by screen width, and each recorded the answer while leaving the other as it was. The stored judgement was always the one given.
Long rating scales now stack on a phone. An 11-point scale needed 550 pixels, so a participant on a 390-pixel screen saw part of it with both ends off screen. Below 600 pixels each point becomes a full-width row.
Choice groups, rating scales and validation errors now read correctly to a screen reader. A group carries its question as its name, each rating star reports whether it is the one chosen, and an error is announced with the control that has it. Validation and a page change move focus to the task instead of only scrolling.
A multi-select answer failed a branching rule that allowed any of its options. Selecting two options stored them together, and the static survey compared the pair as one value, so a rule showing a follow-up for either option stayed closed. Any selected option the rule allows now satisfies it, which is what the Shiny survey already did.
A second branching rule on the same question replaced the first in the Shiny survey, so a question gated on two conditions ran on one. Every rule is kept and they combine, and a question controlled by one that is itself hidden now counts as unanswered, so a stale answer behind a closed branch keeps the questions below it closed too. The static survey already worked this way.
One-question-at-a-time mode ignored branching, so a participant could be required to answer a question the rules exclude, which was then blanked on submission. Navigation follows the same visible sequence the rest of the survey uses.
A failed callback after a saved response invited a
duplicate. The save and the on_submit callback
shared one error handler, so a callback failure reported that nothing
was saved and submitting again wrote the answers a second time. The two
steps are tracked separately, a retry repeats only what failed, and the
two failures now read differently.
preview = TRUE,
a new export_static_survey() argument that removes the
collector and the completion redirect. If you previewed a
configured instrument in Studio on 0.4.1 or earlier, check the
collecting sheet for test rows.surveyframe: prefix: Design an instrument, Open
analysis workspace, Analyse an existing instrument, and Insert starter
instrument. SurveyBuilder was described as a Shiny app, and it is a
client-side HTML page..sframe fails
to load. It used to handle a missing rstudioapi alone. The exported
launchers keep raising their errors.validation, and ends with a commented
write_sframe() line for saving it.library(surveyframe) now says where to
start. In an interactive session it prints a short note: 3
commands that load a branded demo and show it as a respondent sees it
and inside SurveyStudio, the functions to start your own study with,
citation("surveyframe"), and the website. Scripts, Quarto
renders and R CMD check stay silent, and
suppressPackageStartupMessages() removes it.score_scales() replaced
the item’s answers with the score, and an analysis of that item read the
score. Shared names are now rejected at validation, and
score_scales() refuses to overwrite data.reverse_items reversed it
everywhere, which changed the scores and the alpha of any other scale
using that item.min_valid, so a declared sum of 2 and 4 was
shown as 3.min_valid = NULL, meaning every
item, a 3-item scale with 1 absent column was scored on 2 items. The
absent item now counts as unanswered, with a warning.item_report() diagnostics now match the scale’s
scoring. Item-rest correlations use the reversed orientation,
where a reverse-keyed item used to come out strongly negative beside a
high alpha. They use respondents who answered every item, where a
missing item counted as 0. Floor and ceiling use the item’s declared
lowest and highest response, where they used the sample’s extremes, and
are NA for an item that declares no bounds.sf_scale() now checks its parameters, and refuses a
constructed item passed inside a scale’s items, which was
silently dropped from the instrument.Q1 and Q3 was
written as the range Q1 to Q3, adding
Q2.sample_size_plan() returned 64 or 50 per group
for t tests and ANOVA whatever alpha and power were requested,
while printing both.render_results() table put a whole row in
a single cell, headers included. Each value now has its own
cell.var_equal, and t
tests and one-way ANOVA judged significance at .05 whatever
alpha the block declared. Options a method never reads are
now listed in options_ignored.term_context(window = 0) copied the match into
its own context. A zero window now gives empty context, and a
negative one is refused, as is max_matches below 1 and an
n-gram size below 2.cor.test(x, y, method = "pearson") where the wrapper call
was all that showed before. Both report engines show it, and
render_report(show_code = FALSE) leaves it out.analysis_syntax() returns that code,
for one result or a whole set. header = TRUE prepends the
lines that load the instrument, read the responses and score the scales,
giving a script that runs on its own.cfa_syntax() and its neighbours.p loses its leading zero, as p = .032, and an
interval says its level, as 95% CI [0.12, 0.48]. The
sentence is plain text, so italicising the symbols stays the author’s
step, and ?sf_apa now says so.render_report(include_analysis = FALSE) still
printed a seed beside the instrument hash, which reads as provenance for
an analysis that never ran.hash, meta and items, and the
reader refuses a file from a newer major format version.read_responses() changed values on the way
in. A respondent ID of 001 became 1,
a text answer of NA became missing, a matrix missing a row
column raised no warning, and a duplicated column was silently
dropped.sframe_export_labelled() left matrix cells,
option columns and text-held codes unlabelled. Every response
column now carries its row, option or criterion wording, and every
column of choice codes carries value labels.reverse flag, keeping only the reversal
declared on a scale. An item marked reverse = TRUE was
scored as though answered in the same direction as the rest, moving
every composite and alpha built on it.reverse_items stay on the scale, where a rebuild used to
copy them onto each item, which changed a loaded instrument’s content
simply by opening it.export_google_sheet() told researchers to let
anyone with the link edit the response sheet, which exposes
participant data to anyone holding the URL. The collector writes through
the sheet it is attached to, so link sharing was always unnecessary. The
help now says to keep the sheet private. If you followed the old advice,
review the sheet’s sharing settings.vignette("mcdm-analysis").survey_module_ui() and
survey_module_server() help is rewritten, covering
supported item types, what is submitted, a failed save, and changing the
instrument, with a complete example that stores responses.item_report() help describes item-rest
correlations, where it said item-total, and the nested result it
returns, with both ways to extract it. The sf_scale() help
states the rules for min_valid, weights and
reverse_items.sample_size_plan() help separates power
calculations from precision targets and rules of thumb, and states which
arguments each one uses.sensitivity_analysis(),
sframe_dematel_compute() and
sframe_rated_matrix() help describe the new counts, the
undefined total relation, and how rated items pair with a weight
item.amend_sframe(),
link_git_commit(), read_sframe() and
write_sframe() states what the hashes establish: a
canonical content check, local and unsigned. They identify content, and
who wrote an instrument or when needs other evidence. The bundled schema
says the same.read_responses() help describes one contract for
undeclared columns and documents expansion columns and value
conversion.vignette("scale-reliability-validity") said
that supplying construct scores returns the HTMT matrix. It
returns the absolute inter-construct correlations, and records
htmt_method = "correlation_fallback" to say which it
computed. The Henseler heterotrait-monotrait ratio needs
items_by_construct, which records
htmt_method = "henseler". validity_report()’s
own help was already accurate, and the vignette overstated it in an
unevaluated chunk. The section now names what each argument gives, and
its examples run, so a build would contradict the claim if it drifted
again.citation("surveyframe") abbreviated the author
as “Sharafuddin M”. The given names Mohammed Ali were passed to
person() as one string, which R shortens to a single
initial. They are now 2 given names, so the text citation reads
“Sharafuddin MA”. The BibTeX entry keeps its correct form. The citation
year now comes from the release date, where it followed the date the
citation was run.?sframe_plots and ?sframe_as_data_frame, and
the Usage of sf_item() and sample_size_plan()
ran past the right margin in 83 places. The tables are now lists, the
Usage lines wrap, and the manual lets a line stretch its spaces before
letting a name overflow.callr, chromote, httpuv and
pkgload join Suggests. They are used only by tests that
drive a survey in a real browser, which are skipped on CRAN.
sframe_demos(). Load one with
sframe_demo("two_group"), or get a Quarto notebook to edit
with sframe_demo_qmd("two_group"). Together they cover
every analysis method and every question type.vignette("learn-by-example"). Pick the
demo that matches the data you have, and follow it from questionnaire to
report.sframe_export_labelled() writes SPSS
.sav or Stata .dta with the question wording
and response options attached, so variables arrive labelled rather than
as codes.run_analysis_plan() gains a seed argument,
set by default. Bootstrap confidence intervals and the EFA parallel
analysis previously drew from the random stream unseeded, so the same
data gave a slightly different interval on every run.
Confidence intervals will move once when you re-run
an older analysis. Test statistics and p values are unaffected. Use
seed = NULL for the previous behaviour.
render_report() now says which engine produced the file,
Quarto or the built-in writer, in a message, in an engine
attribute, and in the report itself beside the instrument hash and the
seed.
%in% with more than one
value never worked in an exported survey. The question stayed
hidden whatever the respondent answered. Present since 0.3.0.sem_lavaan_syntax() produced a mediation model
lavaan could not fit. Indirect effects referred to path labels
that were never written.quality_report() gains straightline_min_items.
When no scale is long enough to check, the report now says so instead of
omitting the chart silently.read.csv() no longer
mangles matrix columns whose labels contain spaces. Use
check.names = FALSE.haven and V8 join Suggests, both
optional.
A major release. It adds multi-criteria decision analysis (10
methods), small-sample statistics, text and open-ended response analysis
(9 methods), and a disclosed-amendment and Git-linked provenance trail
for .sframe files, alongside 4 corrected results and 2
breaking changes. See below for full detail on each.
surveyframe’s decision-family extension links survey collection directly to 10 MCDA methods, closing the gap between MCDA computation packages, which assume a clean matrix already exists, and survey software, which has no concept of a decision method at all.
pairwise_comparison (Saaty’s 1-to-9 ratio scale
for AHP and ANP, or a 0-to-4 directed influence scale for DEMATEL) and
criteria_weight (a constant-sum allocation across
criteria).R/decision_data.R)
turns per-respondent answers into the matrices the methods consume:
sframe_assemble_pairwise() builds one matrix per respondent
and validates every pair was answered,
sframe_aggregate_judgements() combines them (geometric mean
for AHP/ANP, which preserves reciprocity, or arithmetic mean for
DEMATEL), and sframe_rated_matrix() builds a performance
matrix from ordinary matrix items. AHP judgements are additionally
screened for consistency against Saaty’s random-index table, with the CR
distribution reported whether or not a study has pre-declared a
filtering threshold.sensitivity_analysis() reports how far a ranking moves
under a declared perturbation of the weights, and carries a
degenerate flag so a ranking that never separated its
alternatives cannot report false stability (see “Decision analysis:
non-results now say so” below).A track of corrections for comparisons run on small samples, where the ordinary versions of these tests can flip significance on repeated draws from data whose true difference never changed.
logistf in
Suggests) for regression prone to separation at small n.assumption_report()
and sample_size_plan(), flagging when a study’s sample size
falls in the range where these corrections are worth considering.vignettes/small-sample.Rmd walks through when to prefer
each correction over its conventional counterpart.A 9-method text-analysis family for open-ended survey items, from term frequency through topic modelling, sharing the same analysis-plan, role-resolution, and reporting pipeline every other method family uses.
term_freq: top terms by frequency, optionally split by
a group variable, rendered as a bar chart or a word cloud.ngram_freq: top bigrams or trigrams by frequency.term_context: a keyword-in-context concordance table
(before/match/ after) for a chosen keyword.co_occurrence: pairwise within-response co-occurrence
counts on the top terms, rendered as a heatmap.co_occurrence_network: a Louvain-clustered (Blondel et
al. 2008), force-directed (Fruchterman & Reingold 1991) term
co-occurrence network; requires the optional igraph package.tidy_sentiment: positive/negative sentiment counts and
proportion positive using the bing lexicon, optionally split by a group
variable, rendered as a diverging bar chart or a positive/negative
comparison word cloud; requires the optional tidytext package.quanteda_dfm: a document-feature matrix summary
(feature count, sparsity, top features); requires the optional quanteda
package.topic_model_lda: Latent Dirichlet Allocation topic
modelling, top terms per topic as a ranked table and a faceted bar
chart; requires the optional tidytext and topicmodels packages.stm_topics: structural topic modelling, the same
top-terms-per-topic output; requires the optional stm and tidytext
packages.clean_text_responses()) and a
174-word Snowball-based English stopword list, both exported so a study
can reuse or override them outside a runner.k)
options that steer their plots and models.vignettes/text-analysis.Rmd walks through cleaning,
each method, and what the family deliberately does not attempt
(stemming/lemmatisation, tf-idf, and keyness comparison are not yet
implemented).write_sframe()’s SHA-256 hash proves a
.sframe file is unchanged since it was written, but gives
no way to distinguish a legitimate revision (a data-entry correction,
bot-response removal, a documented model respecification) from an
undisclosed edit – both break the hash identically. This release adds a
disclosed-revision path alongside the existing hash check, without
weakening it.
amend_sframe() compares an instrument before and after
a change and appends a structured, timestamped entry to an ordered
amendment log – never overwrites – recording the reason (a controlled
vocabulary: data_correction, bot_removal,
model_respecification, instrument_revision,
other), a free-text explanation, and which top-level fields
changed."pipeline" amendments (data corrections, bot removal) need
only a reason. "design" amendments (anything touching the
analysis plan or a model) require a deviation_report
describing what changed in the research question, method, or model and
why, matching how a formal preregistration deviation is normally
handled. signoff is never left blank – it records a
reviewer’s name or the literal "none", so an unreviewed
design change stays visible to an auditor.amendment_log() returns the full history as a data
frame, one row per disclosed amendment, exportable with
write.csv()..sframe file, bypassing
amend_sframe(), still fails read_sframe()’s
integrity check exactly as before. The amendment log adds a disclosed
path alongside the existing hash check.link_git_commit() records the current Git commit SHA
and subject line alongside an instrument. This ties the SHA-256 hash to
a specific, already-explained commit. It returns an informative message
when Git isn’t installed or the path isn’t a repository. Git is
optional.inst/schema/sframe_schema.json documents the
.sframe format (every top-level field, including the new
amendments log) as a standalone JSON Schema, so a reviewer
or a second tool can read and validate a .sframe file
without installing the package. .sframe was already plain,
git-diffable JSON before this release; the schema makes that format
explicit and independently checkable.vignettes/surveyframe.Rmd gains a “What the SHA-256
hash proves, and what it does not” section, stating plainly that the
hash proves file identity, not methodological validity, and pointing to
the design-time analysis_plan binding and
run_analysis_plan()’s single-pass execution as the
package’s separate, complementary defence against HARKing and
p-hacking.Four defects found by independent cross-validation are fixed. Each produced normal-looking numbers with no error or warning, so re-run any results computed with an earlier version.
item_report() returned the wrong item-rest correlation.
It subtracted each item from a rowMeans() total, which
leaves roughly noise carrying the item negatively, so a highly reliable
scale reported strong negative values. On simulated data with alpha
0.947 every item came back at about -0.46. The statistic is now the item
against the sum of the other items in its scale, and matches
psych::alpha()’s item.stats$r.drop to
1e-10.aov() treated it as a continuous covariate. On a
fixture where jmv::anovaRM() gives F(2, 78) = 86.93,
surveyframe reported F = 1.45, p = 0.24. Correcting the identifier alone
was not sufficient: the corrected design produces no
Error: Within stratum, so the effect is now located by
searching the strata directly.validate_sframe() rejected valid instruments. Its
known-variable list held only base item and scale ids, so an analysis
plan naming an expansion column (item__sub,
item__option, item__a__vs__b,
item__crit) failed validation for variables that do exist,
including real exports from the visual builder.
read_responses() already accepted those columns. Both now
derive the list from one shared helper.seminr_syntax(), sem_lavaan_syntax(), and
cfa_lavaan_syntax() never checked model$type,
and the builder offered every saved model to all 3 generators, so a
covariance-based model produced PLS-SEM syntax with no complaint. That
is a runnable script estimating a model the researcher never declared.
All 3 now refuse a mismatched estimation family, and the builder filters
each model role to the types its generator can produce.validate_sframe() and validate_model() return
a diagnosticBoth validators previously returned two different things depending on
strict: the object itself, invisibly, when
strict = TRUE, and a bare unclassed list when
strict = FALSE. Neither was a diagnostic, the success path
printed nothing at all, and the strict = FALSE return had
no methods. Both now return an sframe_validation object,
and they return it visibly, so validate_sframe(instrument)
typed at the console shows the user what it found.
valid, every problems
message, and a checks table listing all 18 instrument
checks (10 for a model) whether or not each found anything. A diagnostic
that lists only failures cannot tell a user that a check passed from one
that was never reached.print(), summary() for the
check roster, as.data.frame() for one row per problem,
sf_is_valid(), and sf_problems().strict = TRUE still aborts with
sframe_validation_error when anything is wrong. That has
not changed.$valid and $problems keep
working, so the common reading pattern needs no migration.strict = TRUE return as an instrument, as in
instrument <- validate_sframe(instrument). Wrap it in
as_sframe(). Passing a validation result where an
instrument is expected now raises a directed error naming
as_sframe() immediately.Raised by a Journal of Statistical Software editor reviewing the code: “we would at least expect that the object is not silently returned and that the print method is adapted to allow the user to read directly the diagnostic”.
The same review found that the classes carried print,
summary and format only, so user code had no
route to their contents except $ on the underlying list,
which makes the internal layout part of the public contract. Two facts
made that concrete: as.data.frame() failed on all 14 result
classes with “cannot coerce class … to a data.frame”, and [
dropped the class on the list-backed reports, so
results[1:2] silently degraded to a bare list and lost its
print method.
as.data.frame() now works on the instrument and on
every report class, returning that object’s primary table.[ keeps the class on
sframe_analysis_results,
sframe_reliability_report and
sframe_item_report.sf_meta(),
sf_items(), sf_scales(),
sf_choice_sets(), sf_branches(),
sf_checks(), sf_models() and
sf_plan(), with sf_plan<- for declaring the
plan. The component accessors return an sf_component_list
named by ID, so sf_items(instrument)[["sat_1"]] reaches one
item.sf_id() and
sf_label().sf_apa() and
sf_flagged().as_sframe().The vignettes and the examples are rewritten to use these accessors. The registered S3 method count goes from 41 to 103.
render_survey() pipe-joined a matrix item’s cells into
a single column, so a matrix question answered in the Shiny survey
arrived as mx = "4|5" where read_responses()
and the whole analysis layer expect mx__r1 and
mx__r2. Data collected that way could not be read back by
the package at all, and nothing said so at collection time. Ranking and
multiple-choice items had the same shape problem.render_survey() will see its matrix, ranking, and
multi-select columns change name and layout between versions. Responses
already gathered under the old shape need re-shaping before they can be
read, and the decision item types are unaffected because they emitted
the correct columns from the start."matrix" level, and neither surface gave matrix items one:
the studio classified them as "identifier" and the builder
grouped them under "expanded", which no role accepts. The
effect was that the rated-matrix path, where respondents rate every
alternative on every criterion, could only be built by writing R
directly, even though it is one of the 3 declared ways to supply a
decision matrix. Matrix items now carry their own "matrix"
level in both surfaces.sensitivity_analysis() gains a degenerate
flag for the same reason. A ranking that never separated the
alternatives cannot be changed by perturbing a weight, so every check
passed and stable came back TRUE: the
strongest robustness signal the function can give, produced by the
weakest result it can be handed. print() now leads with “No
result to test” instead of “Stable” in that case.quality_report() counted only columns matching a bare
item id, and multi-column items never post under those, so every
expansion column was invisible to the missingness check. A respondent
who skipped an entire pairwise battery was reported at 0 percent
missing. Expansion columns now count as item data, which brings matrix,
ranking, multi-select, and the 2 decision item types into the
missingness figures for the first time. Reported missingness
rates will change for any instrument using those item types,
because columns that were silently excluded are now counted.
Straight-lining and timing are unaffected: straight-lining runs over
declared scales, and timing is measured on the clock.cb_sem model, so sframe_demo_data() generated
PLS-SEM syntax from a covariance-based model, and every vignette and
example loading it inherited the same mismatch. Each demo now carries a
real pls_sem model with composite constructs. The
instrument hashes changed with it.sframe_decision_options() documents PROMETHEE’s
preference functions and records why the default is
"usual", Brans and Vincke’s type I step function, chosen
over the linear function several other implementations default to. Net
flows differ between the 2 functions, and the ranking changed in 226 of
400 randomly drawn 4-alternative by 3-criterion matrices.This release completes the plotting, interface, statistics, and reporting work started in 0.3.3. Every analysis family now has a chart, every effect size ships with a confidence interval, reports accept written interpretations and print to PDF, both dashboards gain quality and correlation panels, date questions gain bounds, and the builder and vignettes pass a WCAG 2.2 AA accessibility audit. Hard dependencies are unchanged. naniar and pagedown join Suggests.
bootstrap_ci()
(percentile bootstrap for any statistic), cohens_d_ci(),
cramers_v_ci(), and eta_sq_ci().d_ci on the t-tests,
r_ci on Mann-Whitney and Wilcoxon, eta_ci on
ANOVA and Kruskal-Wallis, ci on the correlations (analytic
Fisher z for Pearson, bootstrap for the rank methods), and
v_ci on chi-square and cross-tabulation.d = 0.62 [0.18, 1.05]. Data too small for an interval keeps
the previous string.validity_report() computes the Henseler
heterotrait-monotrait ratio when item-level data is supplied through the
new items_by_construct argument. Without it, the previous
correlation-based fallback applies and the htmt_method
element records which was used.missing_data_report() runs Little’s MCAR test when
naniar is installed. Without naniar the result is unchanged.reliability_report() records why omega is unavailable
for a scale in an omega_note, and the reliability chart
names those scales in its subtitle.efa_solution() adds three tidy data frames ready for
plotting and reporting: loadings_long,
communalities_table, and variance_table.render_report(format = "pdf") prints the HTML report to
PDF through pagedown, which requires a local Chrome or Chromium. HTML
output is unchanged and remains the default.interpretations argument on
render_report() and render_results(). Pass a
named list keyed by analysis-plan block id to add a written
interpretation to each research question after the results are known.
The report shows it beside the pre-declared decision rule, so the
prospective plan stays visible next to the post-hoc narrative.
Interpretations are report content only and are never written into the
instrument file.run_analysis_plan(plots = TRUE) now attaches a chart to
every supported family: regression diagnostics (4 panels), EFA scree and
loadings heatmap, reliability bars, mosaic and crosstab, correlation
heatmap, quality flag rates, group-comparison boxplots, paired slope
charts, raw-variable distributions, repeated-measures profiles, a
partial-correlation residual scatter, logistic-regression odds-ratio
forest plots, a moderation interaction plot, and a mediation effect
chart. Every analysis-plan block now returns a table, a chart, or
generated syntax.plot() methods for descriptives, EFA, quality,
reliability, validity, missing-data, and analysis-results objects.
plot(results) draws every attached chart, and
plot(results, which = "rq_id") returns one.plot_palette argument on
run_analysis_plan() and render_report():
"web" for brand colour on screen, "print" for
black and white suitable for print and journal submission. SurveyStudio
exposes the choice as a Chart theme option on the Export screen.date_min and
date_max bounds in sf_item(), the
SurveyBuilder, and the exported survey. The date picker enforces the
bounds and typed dates outside them show a clear message.read_responses() produces.sf_item()’s date_min and
date_max accept only "YYYY-MM-DD" or a
Date object now (an ambiguous string such as
"01/02/2024" used to parse silently into a specific date
depending on locale). Anything else raises a validation error.bootstrap_ci(), cohens_d_ci(),
cramers_v_ci(), and eta_sq_ci() no longer
alter the random-number seed for code that runs after a reproducible,
seeded call.This release adds an opt-in plotting layer, fixes bugs surfaced by the package’s first field deployment, and redesigns the survey-taking experience. ggplot2 joins Suggests; hard dependencies are unchanged.
plots argument on run_analysis_plan()
(default FALSE). When TRUE, supported analysis
blocks return a ggplot object in $plot: bar charts for
frequency and chi-square blocks, and scatter plots with a regression
overlay for correlation and regression blocks.theme_surveyframe(), a
publication-oriented ggplot2 theme with an accessible fixed-order series
palette. All plots use it.$table data frame ready
for knitr::kable(); the HTML report shows these tables
automatically.item__option = 1 for the top choice), so ranks are
directly analysable. Multiple-choice items likewise export one 0/1
column per option instead of a single comma-joined column.
read_responses() accepts the expanded columns for ranking,
matrix, and multiple-choice items without warnings.render_report() now attaches each analysis block’s
chart directly under its result table, in both the Quarto and internal
HTML report paths, instead of tables and plots appearing in separate
places.read_sheet_responses() gains a meta_cols
argument for extra sheet columns a host application appends, and
SurveyStudio’s dashboard now computes completion times from imported
sheet responses.sem_lavaan_syntax() turns free-text path labels into
valid lavaan parameter names (a label starting “H1:” becomes the
parameter H1).seminr_syntax() output loads seminr and uses
summary() accessors, so the generated code runs as
pasted.run_analysis_plan() accepts pls_sem as an
alias for seminr_syntax..sframe verifies and reports its SHA-256
integrity status.This release corrects the package citation, completes the S3 method surface for the component classes, and improves the graphical tools and the HTML report. It adds no new exported functions, no new statistical methods, and no new bundled datasets.
inst/CITATION now reports the correct package title and
reads the version dynamically from the package metadata, so the citation
no longer pins an old version or an outdated title.print(), format(), and
summary() methods for the component classes
sf_choices, sf_item, sf_scale,
sf_branch, sf_check, and
sf_model, so each class now has a visible, documented S3
surface.lavaan is declared in Suggests. It is used
only to fit the syntax produced by cfa_syntax(). The
package itself generates syntax and never requires lavaan
to be installed.export_static_survey() and
export_google_sheet().render_report() renders reliably through Quarto when it
is installed. A path defect that made the Quarto render fail and fall
back to the plain internal output is fixed.This is a patch release. It fixes the static-survey to Google Sheets to R collection loop, repairs a serialisation defect, and improves the first-time user experience. There are no new exported functions, no new statistical methods, and no new bundled datasets.
export_static_survey() now renders the header logo and
institution name from render$header, so exported surveys
match the Shiny renderer and the builder preview.export_static_survey() now falls back to the
instrument’s render$google_sheets_endpoint when
endpoint_url is not supplied, so a Google Sheets endpoint
set in the builder is honoured on export.respondent_id, matching the Google Apps Script
collector and read_responses(). The collection round-trip
now preserves the identifier.export_google_sheet() now includes matrix sub-item
columns (item_id__sub) in the Apps Script header row, so
matrix answers are stored in the Sheet.read_sheet_responses() now declares
started_at as a meta column and no longer raises a warning
on every read.image/png, image/jpeg,
image/gif), so JPEG and GIF logos display correctly in the
builder, the Shiny renderer, and the static export.write_sframe() now strips list-level names from the
item, choice, scale, branching, check, and model collections before
serialisation. Instruments built with Map() or other
helpers that attach element names (for example, using item IDs as names)
previously serialised those collections as keyed JSON objects, producing
a hash mismatch and an integrity error on read_sframe().
Saved instruments now round-trip correctly regardless of how the
component lists were constructed.sframe object. The message points the user to
sf_instrument() and read_sframe() instead of
showing a raw inherits() assertion failure.reliability_report() no longer prints
psych internal warnings to the console. McDonald’s omega is
skipped silently for scales with fewer than three items, where the
statistic is not meaningful.run_analysis_plan() when no
analysis plan is present now describes both the programmatic route
(instrument$analysis_plan) and the visual SurveyBuilder
route.surveyframe.Rmd) as an
end-to-end worked example: design the questionnaire, export it as a
hosted survey with a Google Sheets backend, collect responses, score
them, run the analysis plan, and render a report. The results section
uses simulated responses so the vignette builds offline; a single
read_sheet_responses() call connects the same workflow to
live responses. The questionnaire and concept are adopted from
Sharafuddin, Madhavan, and Wangtueai (2024, Administrative
Sciences, 14(11), 273, doi:10.3390/admsci14110273), with generic destination
wording so the example transfers to any tourism services context.sf_instrument() examples now include a complete
analysis_plan block.install.packages("surveyframe"), adds a short path for
users who already have a response CSV, and points to
browseVignettes("surveyframe").The first CRAN release of the full workflow: a typed instrument object carrying the questions, the analysis plan, and the measurement model, with deployment, collection, analysis, and reporting built around it.
variables/test analysis blocks. New plans can
store family, method, roles,
options, hypotheses,
decision_rule, reporting_references,
status, and requires_data.descriptives_report(), missing_data_report(),
assumption_report(), posthoc_report(),
validity_report(), and
sample_size_plan().run_analysis_plan() to dispatch the v0.3
method registry, including descriptives, missing data, sparse-table
tests, related-sample tests, Kendall and partial correlations, two-way
ANOVA, ANCOVA, repeated ANOVA, ordinal and multinomial logistic
regression, mediation, moderation, and model-syntax output.sf_construct(),
sf_path(), sf_covariance(),
sf_indirect(), sf_model(),
validate_model(), model_json(),
add_model(), efa_solution(),
efa_syntax(), cfa_lavaan_syntax(),
sem_lavaan_syntax(), seminr_syntax(), and
model_report_template(). Syntax generation does not require
lavaan or seminr.cfa_syntax() remains available as a backward-compatible
wrapper around cfa_lavaan_syntax().Added export_static_survey(). This produces a
single, self-contained HTML file that runs the survey in any modern
browser without a Shiny server or an internet connection. All thirteen
item types are fully rendered (Likert, single choice, multiple choice,
matrix, numeric, text, long text, date, slider, rating, ranking, section
break, text block). Branching logic, required-field validation, a
progress bar, welcome and thank-you pages are all handled in client-side
JavaScript. On submission the browser downloads a per-respondent CSV
file. An optional endpoint_url argument adds a parallel
JSON POST to any serverless endpoint (Google Apps Script, Netlify
function, etc.).
The exported file is suitable for hosting on GitHub Pages, Netlify, or any static file server, and can also be shared directly as an e-mail attachment and opened from disk.
The SHA-256 hash written into .sframe files by
write_sframe() and by the SurveyBuilder HTML is computed
using the same canonicalisation algorithm, so instruments round-trip
correctly between the browser and R.
Added launch_dashboard(). Opens a five-panel Shiny
dashboard for exploring collected response data alongside the instrument
definition, without modifying either. The panels are: Overview (response
count, date range, instrument metadata), Items (per-item bar charts,
histograms, and frequency tables), Scales (scale score distributions
with mean overlay), Quality (attention-check pass rates), and Raw data
(a scrollable response table with CSV download).
When called without arguments the dashboard loads the bundled tourism
services demo. When called with a user-supplied instrument and no
responses argument, it opens in metadata-only mode showing
instrument structure.
Added sframe_demo_data(),
sframe_input_types_demo_data(),
launch_builder_demo(), launch_studio_demo(),
and launch_dashboard_demo() for CRAN-safe examples,
training, and local GUI testing.
Added a bundled input-types demo instrument and simulated response dataset for testing SurveyBuilder, SurveyStudio, the dashboard, and all supported item controls.
launch_studio() now accepts preloaded instruments,
response data frames, CSV response paths, initial screen selection,
host, port, and browser control. SurveyStudio reads these preloaded
values during startup.
Added survey_module_ui() and
survey_module_server(). These allow a survey to be embedded
inside a larger Shiny application as a first-class module.
survey_module_server() returns a reactive that
holds NULL until the form is submitted, then returns the
response as a named list keyed by item ID.
ui <- fluidPage(survey_module_ui("s1"))
server <- function(input, output, session) {
resp <- survey_module_server("s1", instrument = instr)
observeEvent(resp(), { saveRDS(resp(), "response.rds") })
}An optional on_submit callback fires immediately on
submission, before any observeEvent() elsewhere in the
app.
run_analysis_plan() now implements four additional tests
used by the SurveyBuilder’s test dropdown:
anova_one: One-way ANOVA with eta-squared effect size.
When the result is significant and there are more than two groups, Tukey
HSD post-hoc output is included in the result object.t_test_pair: Paired-samples t-test with Cohen’s
d_z.wilcoxon_pair: Wilcoxon signed-rank test with r effect
size.regression_logistic_binary: Binary logistic regression
with McFadden R-squared and an overall model chi-square test. The full
coefficient table is returned for interpretation.All four runners produce an APA-formatted summary string and an
interpretation prompt field to guide write-up.
write_sframe() validates the instrument and writes
the validated object, preserving meta$validated = TRUE in
the saved .sframe file.
.sframe serialisation now includes a
models field and continues to read older
.sframe files where models is absent.
read_responses() no longer requires display-only
items such as section_break and text_block to
appear as response columns.
validate_sframe() now checks model references,
analysis-plan roles, invalid model IDs, duplicate model IDs, and model
indicator/path integrity.
launch_builder(open = TRUE) opens the SurveyBuilder
HTML in the system’s default browser via
utils::browseURL().
R/studio_builder.R contains three fully implemented
internal functions (sframe_builder_empty_state,
sframe_builder_state_from_instrument,
sframe_builder_validate_draft) used by SurveyStudio startup
and draft validation.
SHA-256 hashing in the SurveyBuilder HTML includes a
pure-JavaScript fallback for environments where
crypto.subtle is unavailable on file://
origins, including common Firefox file:// configurations.
Saving a .sframe file from the builder now always
succeeds.
The SurveyBuilder’s rqSuggest box now appears with
an icon and a plain-language recommendation when two or more variables
are selected in the RQ modal.
The undo and redo buttons in the SurveyBuilder topbar are now correctly disabled when their respective history stacks are empty.
export_google_sheet() now writes Google Apps Script
using JSON-encoded JavaScript literals instead of interpolating
instrument metadata directly into executable code. The generated
endpoint also rejects missing, over-large, and non-object JSON POST
bodies..sframe and .csv files by extension, size, and
text-content checks before passing them to import functions.read_sframe() now validates the top-level
.sframe payload structure before hash verification and
object reconstruction.on.exit() even when rendering fails.validate_sframe(), score_scales(),
codebook_report(), cfa_syntax(), and
launch_builder(open = FALSE) have fully runnable
examples.launch_builder_demo(),
launch_studio_demo(), and
launch_dashboard_demo()) open in the browser with the demo
instrument, scales, and analysis plan preloaded, so no manual file
loading is needed.demo("survey")) walks
through the whole workflow with step-by-step prompts.sf_instrument(),
sf_item(), sf_choices(),
sf_scale().write_sframe(),
read_sframe() with SHA-256 integrity checking.render_survey().launch_builder().read_responses().