Skip to contents

studyframe keeps observations connected to the collection facts needed for a requested analysis. Sampling, treatment assignment, measurement, and analyst choices are separate declarations. An analysis checks the declarations it needs; it does not infer a collection design from the pattern of observed responses.

Begin with what is known

A received response table supplies observations, but does not establish who was invited, sampled, or randomized:

responses <- studyframe(
  data.frame(id = 1:4, score = c(10, 12, 8, 15)),
  keys = "id", unit = "person", dataset_id = "received_responses"
)
context(responses)$roster
#> $kind
#> [1] "unknown"
#> 
#> $source
#> [1] ""
#> 
#> $completeness
#> [1] "unknown"
#> 
#> $scope
#> NULL
#> 
#> $evidence
#> list()
request <- analysis_request("effect", "score", assignment = "offer_assignment")
requirements <- check_analysis(responses, request)
requirements$ok
#> [1] FALSE
vapply(requirements$issues, function(issue) issue$code, character(1))
#> [1] "assignment_required"        "randomized_roster_required"

Those requirements identify the missing assignment declaration and complete randomized roster. Adding labels or observing balanced treatment groups would not supply either fact. A missing backend package is reported separately as a dependency issue.

Collect outcomes against an external roster

The package includes an executable synthetic collection workflow. It creates 40 stable unit IDs, assigns exactly 20 to treatment by uniform shuffle, saves the assignment roster, and generates outcomes with a true additive treatment effect of 2. It then writes a vendor-shaped instrument and response export, shuffles the response rows, imports them by key, writes a bundle and codebook, and reads the bundle into a fresh object before estimation.

The example is simulated and requires arrow, zip, and estimatr. It uses no account credentials or private respondent records.

source(system.file("examples", "qualtrics_workflow.R", package = "studyframe"))
handoff <- run_qualtrics_workflow(tempfile("collection-"))
study <- handoff$study
c(observed = nrow(study), roster = nrow(collection_data(study)))
#> observed   roster 
#>       40       40
context(study)$roster
#> $kind
#> [1] "randomized_units"
#> 
#> $source
#> [1] "Full externally generated assignment roster"
#> 
#> $completeness
#> [1] "asserted_complete"
#> 
#> $scope
#> [1] "offer_assignment"
#> 
#> $evidence
#> $evidence[[1]]
#> [1] "assignment_file"

For an actual collection, retain the external roster and stable unit IDs before fielding. Capture the collection-time QSF instrument and its hash; a later export can describe a different draft. read_qualtrics() can reconcile supported response fields to that roster. It preserves nonrespondents with missing outcomes and refuses attempted overwrites of authoritative assignment fields. The current effect adapter requires complete outcomes, so nonresponse blocks this analysis.

Check, then estimate

request <- analysis_request("effect", "QID1", assignment = "offer_assignment")
requirements <- check_analysis(study, request)
stopifnot(requirements$ok)
result <- run_analysis(study, request)
c(
  estimate = unname(result$fit$coefficients),
  standard_error = unname(result$fit$std.error),
  lower = unname(result$fit$conf.low),
  upper = unname(result$fit$conf.high)
)
#>       estimate standard_error          lower          upper 
#>      1.8284659      0.2758276      1.2685621      2.3883697
result$analysis$confidence_interval
#> $method
#> [1] "Student t"
#> 
#> $level
#> [1] 0.95
#> 
#> $df
#> [1] 35.09806

Checking prepares the design without fitting the outcome estimator. Running the request repeats validation and delegates estimation to estimatr. The estimate targets the effect on the declared randomized roster, without using sampling weights to extrapolate to an external population. Sampling-weighted means use a separate request and the survey package.

A passing check establishes that the declared design is supported. It cannot certify that randomization occurred or that an external completeness assertion is true. Numerical degeneracy can still prevent estimation; such failures return a structured condition rather than a successful result with undefined uncertainty.

Carry the handoff forward

received <- read_studyframe(handoff$bundle)
stopifnot(identical(as.data.frame(received), as.data.frame(study)))
result$analysis$receipt_version
#> [1] "0.3.0"
result$analysis$assumptions
#> $declarations
#> [1] "Supplied assertions, not independently verified facts."
#> 
#> $roster_completeness
#> [1] "asserted_not_independently_verified"
#> 
#> $missingness
#> [1] "Omission, when requested, does not establish a nonresponse assumption."

The bundle contains the current table, attached collection roster, versioned context, and integrity hashes. Source evidence records point to external artifacts; the source artifacts themselves are not embedded. Hashes check artifact identity, not the truth of scientific claims.

Supported selection, rename, derivation, recoding, and keyed joins preserve the relationship between measurements and declarations. A filtered view still carries and exports its full roster. To create a restricted release, construct a new dataset and reconsider its declarations. as.data.frame() explicitly extracts a plain table without the attached context.

The returned result keeps the native backend fit with an analysis receipt recording the request, input identity, selected design, evidence, assumptions, and interval convention. This is the information another analyst or agent needs to inspect the analysis without reconstructing the collection from a detached codebook.