NEWS.md
This release incorporates 0.6.0, which was tagged but never submitted to CRAN, together with the further breaking changes below. The version on CRAN is 0.3.0, so 0.4.0, 0.5.x and 0.6.0 are all superseded here.
Model entry points now state their level and estimand. lca_fit() is replaced by item_lca_fit() for independent item-wise fits. person_item_lca_fit() jointly estimates one shared latent trajectory per person, common class proportions, and item-specific guessing rates. posterior_class_probs() and posterior_learned() now extract results from that explicit person/item fit instead of silently fitting a different model from an item-wise result.
Functions that were not IRT models no longer claim to be. lca_irt() is replaced by lca_difficulty() because it only reparameterizes the LCA guessing rate. estimate_ability() is replaced by estimate_logit_score(), its unsupported hand-built "rasch" branch is removed, and cross_sectional_irt() is replaced by cross_sectional_learning_score(). The latter is a descriptive bounded score, not a calibrated probability of learning.
Missing-response semantics are now explicit and consistent. Every function accepting raw responses uses na_as = c("dk", "missing"). The default treats NA as an observed don’t-know response and selects the nine-cell model; explicit "d"/"DK" always does the same. When NA is structural missingness, missing_action = "omit" excludes incomplete pairs and "error" rejects them. Structural missingness is not added to the latent-class model.
validate_recovery() no longer reports invalid confidence-interval coverage. The function had used one Monte Carlo standard deviation as every replication’s standard error, which is not replicate-specific interval coverage. It now reports bias, RMSE, and Monte Carlo SD, and averages estimates across every simulated item instead of silently keeping only the first.
The don’t-know model now matches the model in the paper. The likelihood implemented a different model from the one in Cor and Sood, equation (2). It kept the two latent classes the paper’s identifying assumption sets to zero, dropped the two the paper keeps, and reused the survivors in the d0 and d1 cells:
| cell | paper | what the code computed |
|---|---|---|
x10 |
gamma(1-gamma)*gg |
(1-gamma)*gamma*gg + kg |
x11 |
gamma^2*gg + gamma*gk + kk |
... + gamma*kg + kk |
x1d |
gamma*gd |
gamma*gd + kd |
xd0 |
(1-gamma)*dg |
kg |
xd1 |
gamma*dg + dk |
gamma*gk + kd |
In short, the know-to-guess class was conflated with don’t-know-to-guess, and know-to-don’t-know with don’t-know-to-know.
DK parameters are renamed. kg and kd are gone; dg (don’t know -> guess) and dk (don’t know -> know) take their place. The order is now gg, gk, gd, kk, dg, dk, dd, gamma. Code that indexes DK parameters by name or position must be updated. Learning is gk + dk – those who learned the item from guessing, plus those who learned it from confessed ignorance. It was gk + kd, which the vignette rationalised as “true learning plus those who learned but lost confidence”.
simulate_lca_dk() gains dg and dk arguments and loses kg and kd. The simulator drew the same classes the likelihood named, so it generated knowledge loss and could never generate anyone who moved from a don’t-know response to knowing – which is half of what the model defines as learning. No test could catch the likelihood bug while the simulator agreed with it.
fit_model() returns NA for data without don’t-know responses. That model has 3 free parameters against 3 free cell probabilities. It is saturated, so there are no degrees of freedom and no test to report. It previously reported a p-value computed on df = 3, counting none of the parameters estimated from the same counts. For the DK model the degrees of freedom are now 1 rather than 8, which is what makes the test able to reject at all.
The two consequences of the old cell equations were both invisible from outside:
They were not a distribution. They summed to between 1.02 and 2.29 rather than 1, so the objective carried a spurious -N log S(theta) term worth up to ~182,000 log-likelihood units, with no statistical content.
The parameters were not identified. Fed exact model-implied counts with no sampling noise, the old estimator returned parameters off by up to 0.041 while the negative log-likelihood differed by 0.0009 in 1,741,431 – observationally equivalent, so no dataset of any size could separate them. The same test now recovers every parameter to within 6e-05.
The paper’s model is identified in closed form – gamma/(1-gamma) is x10/x00, and every lambda follows – and over-identified by exactly one degree of freedom, the restriction x1d/x0d = x10/x00.
stnd_cor() now estimates paired learning from the same respondents in both corrected totals. With wave-specific missingness, the previous implementation subtracted marginal pre- and post-test totals calculated from different respondent sets, then divided that unmatched difference by the number of complete pairs. Marginal pre- and post-test scores still use everyone observed at each wave; learning now uses complete pairs throughout and returns NA when no pair is observed.
Zero-probability likelihood cells now contribute zero when their observed count is zero and infinite loss only when they are observed. Perplexity and cross-validation no longer discard impossible held-out observations or divide by unobserved pairs.
Items with and without observed don’t-know responses can now be combined safely: four-cell item transitions are promoted to the shared nine-cell schema with zero DK counts instead of being recycled into a malformed matrix.
Tibble inputs now use vector-safe column extraction and produce the same transitions, fits, corrections, and missing-response behavior as data frames.
dk_cell_probs() and nodk_cell_probs(), which every likelihood, expected-count and goodness-of-fit routine calls.End-to-end model-criticism tests verify that raw individual and aggregated item log-likelihoods and perplexities agree for binary, NA-coded DK, and structurally missing data. Additional tests cover cross-validation denominators, tibble pipelines, response-code validation, and simulation recovery.
New tests/testthat/test-dk-model-spec.R: the cells sum to 1, the structural zeros hold, the closed-form inversion recovers every parameter, the over-identifying restriction holds, the estimator recovers the truth from exact counts, the simulator can produce learning from confessed ignorance, and the goodness-of-fit test spends 1 degree of freedom.
test-econometric-likelihood.R used to restate the cell equations inline and assert only that they were non-negative. They were, and they also summed to 2.29. It now calls the function the likelihood uses and checks that they sum to 1.
stnd_cor() understated learning when responses were missing. The numerators counted observed responses but the denominator was nrow(), so every item with missing data was shrunk toward zero in proportion to its missingness. On one item with true learning 0.384, a 10% missing rate returned 0.341 and a 40% rate returned 0.227 – a 41% understatement. Scores are now divided by the number of responses each item actually has, and learn by the respondents who answered at both waves. This is the bias the package exists to remove, so it mattered more than its size suggests.
lca_se() failed for most item counts. A resamp_agg matrix was allocated 2 * n_items wide while a transition row is 4 wide (or 9 with don’t-know responses), and it indexed row n_items – the last item – rather than the aggregate row at n_items + 1. It errored outright on 3 and 5 items, and on every item count tried when the data contained DK responses, while silently recycling the row at 4 and 8 items. Nothing ever read the variable, so it has been removed. Bootstrapped standard errors now work for any item count, with or without DK.
Removed R/fit_nodk.R and R/fit_dk.R. Both were shadowed at load time by the wrappers in R/fit_unified.R, which defines the same names later in alphabetical order, so the code never ran. The dead copies passed model-expected counts to chisq.test() as the observed vector and observed proportions as p – had load order ever changed, they would have returned chi-square statistics between 0.83x and 2.08x the correct ones. fit_nodk() and fit_dk() remain exported and unchanged in behaviour.
The optimiser no longer prints its iteration trace. solnp() is called with control = list(trace = 0), so lca_fit() and lca_se() are silent. A 100-resample bootstrap previously emitted hundreds of lines, which is how a genuine warning gets lost.
lca_difficulty() and simulate_lca() documentation with ASCII. They produced LaTeX errors when building the PDF manual, so R CMD check --as-cran reported an ERROR and two WARNINGs on any machine with a working TeX installation.l prefix. The new naming pattern is {pre_state}{post_state}:
lgg → gg, lgk → gk, lkk → kk (gamma unchanged)lgg → gg, lgk → gk, lgd → gd, lkg → kg, lkk → kk, lkd → kd, ldd → dd
simulate_lca() and simulate_lca_dk() to generate data from known parameters for validation studiesvalidate_recovery() for Monte Carlo validation of parameter estimateslgg, lgk, lgc, lkk, lcg, lck, lcc, gamma
lgg, lgk, lgd, lkg, lkk, lkd, ldd, gamma
lca_cor() output when using DK dataparams["gamma", ]) instead of numeric indices, making the code more readable and robust to parameter reorderingfit_model(), fit_dk(), and fit_nodk() now use formulas consistent with the likelihood functioncell_probs() and calculate_expected_values() now use correct DK model formulascell_probs() to match the likelihood function exactlycalculate_expected_values()
helper-simulation.R with reusable functions for parameter recovery testingtest-econometric-formula-derivation.R: Validates likelihood formula componentstest-econometric-identification.R: Tests parameter identification conditionstest-econometric-likelihood.R: Verifies likelihood computation correctnesstest-econometric-parameter-recovery.R: Monte Carlo parameter recovery validationtest-econometric-se-validation.R: Standard error computation verificationutils-validation.R
stop() calls and :: namespace patternscheckmate dependency for robust input validationgoji dependency by implementing internal zero1 function@importFrom declarationsmake coverage command for quick coverage analysisMakefile for common development tasksT/F with TRUE/FALSE throughout|| for scalar comparisonsstringsAsFactors argumentsmapply() calls