From collection to inference
Source:vignettes/collection-to-inference.Rmd
collection-to-inference.Rmdstudyframe 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.09806Checking 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.