Skip to content

Commit 92b093f

Browse files
author
Alexey Stukalov
committed
score_basis_transform()
1 parent 9ceac93 commit 92b093f

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

src/frontend/predict.jl

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,99 @@ function SemScoresPredictMethod(method::Symbol)
456456
end
457457
end
458458

459+
"""
460+
score_basis_transform(model::SemLoss, params; method = :regression,
461+
latent_vars = nothing, alpha = 0,
462+
prior_cov_alpha = nothing)
463+
464+
Return the centered latent-score basis transform associated with a score-prediction
465+
method.
466+
467+
For [`SemRegressionScores`](@ref) and [`SemBartlettScores`](@ref), the result is the
468+
identity transform. For [`SemAndersonRubinScores`](@ref), the result stores the whitening
469+
map between the model's original latent-variable basis and the Anderson-Rubin basis.
470+
471+
The returned object can be used to map centered Anderson-Rubin scores back to the
472+
original latent-variable basis via:
473+
474+
```julia
475+
orig_scores = transform(ar_scores; inverse = true)
476+
```
477+
478+
`alpha` matters only for [`SemAndersonRubinScores`](@ref), because the whitening
479+
transform is built from the ridge-regularized Bartlett score operator. For regression and
480+
Bartlett scores the transform is the identity, so `alpha` does not affect the result.
481+
482+
`prior_cov_alpha` is currently accepted for API symmetry with
483+
[`predict_latent_scores`](@ref) and for forward compatibility, but it does not affect the
484+
returned transform for the currently implemented score methods.
485+
"""
486+
function score_basis_transform(
487+
method::SemScoresPredictMethod,
488+
model::SemLoss,
489+
params::AbstractVector;
490+
latent_vars::Union{AbstractVector, Nothing} = nothing,
491+
alpha::Number = 0,
492+
prior_cov_alpha::Union{Number, Nothing} = nothing,
493+
)
494+
length(params) == nparams(model) || throw(
495+
DimensionMismatch(
496+
"The length of parameters vector ($(length(params))) does not match the number of parameters in the model ($(nparams(model))).",
497+
),
498+
)
499+
alpha >= 0 ||
500+
throw(ArgumentError("The regularization parameter alpha must be non-negative"))
501+
isnothing(prior_cov_alpha) ||
502+
prior_cov_alpha >= 0 ||
503+
throw(
504+
ArgumentError(
505+
"The regularization parameter prior_cov_alpha must be non-negative",
506+
),
507+
)
508+
509+
implied = imply(model)
510+
ram = implied.ram_matrices
511+
lvar_inds =
512+
check_var_indices(ram, latent_vars, allow_observed = false, normalize = true)
513+
lvars = vars(ram)[lvar_inds]
514+
if !(method isa SemAndersonRubinScores) # identity transform
515+
return SemVariablesTransform(lvars, I, I)
516+
end
517+
518+
update!(EvaluationTargets(0.0, nothing, nothing), implied, params)
519+
520+
A = materialize(ram.A, params)
521+
S = materialize(ram.S, params)
522+
T = float(promote_type(eltype(A), eltype(S), typeof(alpha)))
523+
524+
I_A = Matrix{eltype(A)}(I, size(A, 1), size(A, 2)) - A
525+
lv_I_A⁻¹ = inverse_rows(I_A, lvar_inds)
526+
base_solver =
527+
QRScoresSolver(implied, lvar_inds, A, S, lv_I_A⁻¹; alpha, prior_cov_alpha = 0)
528+
nobs = nobserved_vars(implied)
529+
base_op = permutedims(base_solver(Matrix{T}(I, nobs, nobs)))
530+
score_cov = Symmetric(X_A_Xt(implied.Σ, base_op))
531+
score_cov_chol = cholesky!(score_cov)
532+
533+
return SemVariablesTransform(lvars, I / score_cov_chol.U, score_cov_chol.U)
534+
end
535+
536+
score_basis_transform(
537+
method::Symbol,
538+
model::SemLoss,
539+
params::Union{AbstractVector, Nothing} = nothing;
540+
latent_vars::Union{AbstractVector, Nothing} = nothing,
541+
alpha::Number = 0,
542+
prior_cov_alpha::Union{Number, Nothing} = nothing,
543+
) = score_basis_transform(
544+
SemScoresPredictMethod(method),
545+
model,
546+
params;
547+
latent_vars,
548+
alpha,
549+
prior_cov_alpha,
550+
)
551+
459552
predict_latent_scores(
460553
fit::SemFit,
461554
data::SemObserved = observed(sem_term(fit.model));

test/unit_tests/predict_scores.jl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,31 @@ end
238238
@test ar_op * Σ * ar_op' Matrix{Float64}(I, size(ar_op, 1), size(ar_op, 1)) rtol =
239239
1e-10 atol = 1e-10
240240

241+
ar_transform = SEM.score_basis_transform(
242+
model,
243+
params;
244+
method = :AndersonRubin,
245+
latent_vars = lv_vars,
246+
alpha = ar_alpha,
247+
)
248+
@test ar_transform.vars == lv_vars
249+
@test ar_transform.old_to_new * ar_transform.new_to_old Matrix{Float64}(I, 2, 2) atol =
250+
1e-10 rtol = 1e-10
251+
@test ar_transform(bartlett_scores) ar_scores rtol = 1e-10 atol = 1e-10
252+
@test ar_transform(ar_scores; inverse = true) bartlett_scores rtol = 1e-10 atol =
253+
1e-10
254+
255+
bartlett_transform = SEM.score_basis_transform(
256+
model,
257+
params;
258+
method = :Bartlett,
259+
latent_vars = lv_vars,
260+
alpha = bartlett_alpha,
261+
)
262+
@test bartlett_transform.old_to_new == I
263+
@test bartlett_transform.new_to_old == I
264+
@test bartlett_transform(bartlett_scores) bartlett_scores rtol = 1e-12 atol = 1e-12
265+
241266
ar_scores_0 = SEM.predict_latent_scores(
242267
model,
243268
params,

0 commit comments

Comments
 (0)