--- title: "Introduction to weasel: Wave and Respondent Selection" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Introduction to weasel: Wave and Respondent Selection} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ## What weasel does Longitudinal panel studies rarely deliver the data you wish you had. Respondents skip waves, drop out, or return after long absences. Before any substantive analysis you must decide which respondents and which waves to keep, and that decision shapes everything downstream. The weasel package (Wave-based Extraction and Selection for Longitudinal Data) turns this selection step into an explicit, inspectable, and reproducible workflow. Throughout the package, a respondent counts as observed at a wave when a row with that (id, wave) pair exists in the long-format data; a missed wave is simply an absent row, which is how real panel files are usually delivered. It offers two complementary pipelines: 1. A **scope pipeline** for interactive exploration of wave patterns. 2. A **plan pipeline** for comparing predefined selection scenarios and generating methods-section text. A simple rule of thumb: start in the scope pipeline when you do not yet know what participation looks like in your panel, and move to the plan pipeline as soon as you are choosing between selection rules you intend to defend in writing. Both pipelines use the same constraint vocabulary (`min_present`, `max_missing`, `n_gap_max`, `max_gap_len`, `require_endpoints`), so a pattern you isolate while exploring translates directly into a scenario. ```{r setup} library(weasel) ``` ## Synthetic example data We start with the built-in generator, which layers several realistic participation mechanisms (random skips, attention decay, permanent attrition, and block dropout) and removes the corresponding rows: ```{r} long_data <- generate_weasel_dummy_data( n_ids = 300, n_times = 12, seed = 2026 ) str(long_data) ``` Note that the grid is incomplete by design: ```{r} nrow(long_data) 300 * 12 ``` A small amount of item nonresponse (`NA` values inside outcome columns of observed rows) is also injected via `prop_item_missing`; it is distinct from wave-level missingness and does not affect selection. ## The scope pipeline The scope pipeline holds its state inside the package itself, not in your global environment, so it never interferes with your own objects. ```{r} set_weasel_scope( long_data, id = "id", wave = "time", lower = 2, upper = 11 ) evaluate_weasel_scope() weasel_reshape_to_wide() weasel_summarize_waves() ``` At any point, `weasel_scope_info()` shows what the scope contains and how far the pipeline has progressed: ```{r} weasel_scope_info() ``` The pattern summary groups respondents by their participation pattern. In the table, `waves` shows the pattern (a `.` marks a missing wave), `n` is the number of observed waves in that pattern, and `ids` counts the respondents who share it: ```{r} weasel_print_table(weasel_filter_wave_summary(), title = "Wave patterns", n = 10) ``` `weasel_filter_wave_summary()` narrows the table. For instance, patterns with at least 9 observed waves that are shared by at least 5 respondents: ```{r} weasel_print_table( weasel_filter_wave_summary(n_range = c(9, 10), ids_range = c(5, Inf)), title = "Dense, common patterns" ) ``` The `pattern` column is a stable id: it survives filtering, so a value you spot in a filtered table can be passed straight to `weasel_get_data_by_row()` (the pattern string from `waves` works as a selector too). That function pulls the original long-format rows behind any pattern (or several at once), ready for analysis: ```{r} subset_row1 <- weasel_get_data_by_row(1) weasel_print_table(head(subset_row1, 8), title = "Most common pattern, data") ``` You can also constrain patterns structurally when setting the scope. The `max_gap_len` argument limits the length of interior gaps (runs of missing waves strictly between a respondent's first and last observed wave), and `n_gap_max` limits how many such gaps are allowed; the same names are used by the plan pipeline's scenario tables: ```{r} set_weasel_scope(long_data, "id", "time", max_gap_len = 1, n_gap_max = 1) evaluate_weasel_scope() weasel_reshape_to_wide() weasel_summarize_waves() weasel_print_table(weasel_filter_wave_summary(), title = "Gap-constrained", n = 5) ``` When you are done, clear the scope: ```{r} weasel_clear_scope() ``` ## The plan pipeline For confirmatory work you usually want a small number of named, defensible scenarios rather than free exploration. `weasel_plan()` evaluates three defaults: * `anchored_strict`: observed endpoints, no missing waves. * `anchored_balanced`: observed endpoints, at most one missing wave and one interior gap. * `lenient_info_max`: no endpoint requirement, up to two missing waves and two interior gaps. ```{r} plan_obj <- weasel_plan(long_data, id = "id", wave = "time", span = "core") plan_obj ``` With `span = "core"` the package slides a window of `core_len` consecutive waves across the full wave range and picks the window with the highest average per-wave coverage; every candidate window is kept in `plan_obj$span_candidates`, and exact coverage ties warn. To supply your own window, pass explicit `lower`/`upper` bounds instead of `span`. The justification text describes these bounds as user-supplied, without inferring when or why they were chosen. ```{r} comparison <- weasel_compare_scenarios(plan_obj) weasel_print_table(comparison, title = "Scenario comparison", digits = 3) ``` The `score` column is computed from observed subset quality (coverage, endpoint rate, relative sample size, worst-case missingness, and gap structure), never from a scenario's configured tolerances; see `?weasel_compare_scenarios` for the exact formula and how to override the weights. `recommended` flags the highest composite score under the declared weights, nothing more: `near_tie` warns when scenarios are practically indistinguishable, and the active weights plus the per-scenario score decomposition travel with the table as attributes (`attr(comparison, "weights")`, `attr(comparison, "score_components")`). A sentence version is available too: ```{r} cat(weasel_compare_to_sentence(comparison), "\n") ``` Audit a scenario before committing to it: ```{r} s <- weasel_summarize_subset(plan_obj, "anchored_balanced") weasel_print_table(s$headline, title = "Headline", digits = 3) weasel_print_table(s$per_wave_coverage, title = "Per-wave coverage") cat(weasel_subset_to_sentence(s), "\n") ``` Finally, extract the analysis-ready subset: ```{r} analysis_data <- weasel_apply(plan_obj, "anchored_balanced") dim(analysis_data) ``` ## Justification text for your methods section Documenting the selection is as important as making it. The package drafts that paragraph for you: ```{r} cat(weasel_justify_subset(plan_obj, "anchored_balanced"), "\n") ``` See the "Advanced usage" vignette for custom scenarios, scoring weights, non-consecutive wave schedules (biennial panels), the sensitivity and selectivity diagnostics, and writing style options.