From bae58b447f2133f9230ed708eb28078ed1dda98b Mon Sep 17 00:00:00 2001 From: Patrik Schilter Date: Sun, 17 Aug 2025 00:13:49 +0200 Subject: [PATCH 1/8] Method clv.fitted.get.LL --- R/all_generics.R | 5 ++++ R/f_generics_clvfitted.R | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/R/all_generics.R b/R/all_generics.R index 51eb2e86..4c731fc6 100644 --- a/R/all_generics.R +++ b/R/all_generics.R @@ -30,6 +30,11 @@ setGeneric("clv.controlflow.estimate.process.post.estimation", def=function(clv. standardGeneric("clv.controlflow.estimate.process.post.estimation")) +# .. Get LL ---------------------------------------------------------- +setGeneric("clv.fitted.get.LL", def = function(clv.fitted) + standardGeneric("clv.fitted.get.LL")) + + # . Predict ----------------------------------------------------------------------------------------------- setGeneric("clv.controlflow.predict.check.inputs", def = function(clv.fitted, verbose, uncertainty, num.boots, level, ...) standardGeneric("clv.controlflow.predict.check.inputs")) diff --git a/R/f_generics_clvfitted.R b/R/f_generics_clvfitted.R index 08e05676..253e9b89 100644 --- a/R/f_generics_clvfitted.R +++ b/R/f_generics_clvfitted.R @@ -35,3 +35,63 @@ setMethod("clv.fitted.estimate.same.specification.on.new.data", signature = "clv new.fitted@call <- cl return(new.fitted) }) + + +# . clv.fitted.get.LL ---------------------------------------------------------- +#' @include all_generics.R +setGeneric("clv.fitted.get.LL", signature = "clv.fitted", def = function(clv.fitted){ + + # Calling the LL with the exact same inputs/specification as when the fitting + # is not trivial as there are plenty of options. + # To reproduce it, the exact steps of the estimation are repeated here. + + # Start parameters are not really required, they are just stored as parameter + # `par` for `optimx()` + final.coefs <- drop(tail(coef(clv.fitted@optimx.estimation.output), n=1)) + + prepared.optimx.args <- clv.controlflow.estimate.prepare.optimx.args( + clv.fitted=clv.fitted, + start.params.all= final.coefs) + + prepared.optimx.args <- clv.model.prepare.optimx.args( + clv.model=clv.fitted@clv.model, + clv.fitted=clv.fitted, + prepared.optimx.args=prepared.optimx.args) + + prepared.optimx.args[["LL.param.names.to.optimx"]] <- names(prepared.optimx.args$par) + + # In the estimation procedure, the user can also supply custom `optimx.args` + # which override `prepared.optimx.args` here. Because optimx is not called + # here, there is no need to add `optimx.args` here. + + # The generated args also contain parameters for optimx. These are not required + # for the LL and need to be removed. This also removes the LL itself (`fn`). + names.optimx.args <- setdiff(formalArgs(optimx), "...") + call.args <- prepared.optimx.args[!(names(prepared.optimx.args) %in% names.optimx.args)] + + # Save memory as the returned method is a closure and has the environment in + # which is was defined attached. Hence all variables in this method here. + # rm(prepared.optimx.args) + fn.LL.entrypoint <- prepared.optimx.args$fn + # rm(final.coefs) + # rm(clv.fitted) + # rm() + + LL <- function(params){ + req.names <- call.args$LL.param.names.to.optimx + + # Ensure named same as original + if(!(identical(sort(names(params)), sort(req.names)))){ + check_err_msg(paste0("'params' has to be named ", paste(req.names, collapse = ", "),".")) + } + + # Ensure name and position are the same as original order. This is required as + # any param input will be re-named by slapping `LL.param.names.to.optimx` on + # them (using names() <- ) in the first interlayer and then further accessed + # by name + call.args$LL.params <- params[req.names] # bring to correct order + return(do.call(what = fn.LL.entrypoint, args = call.args)) + } + + return(LL) +}) From 6925ed768e1e26281562c4c85cc02cdcddd41d80 Mon Sep 17 00:00:00 2001 From: Patrik Schilter Date: Sun, 17 Aug 2025 16:43:26 +0200 Subject: [PATCH 2/8] `clv.fitted.get.LL`: Implement --- R/f_generics_clvfitted.R | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/R/f_generics_clvfitted.R b/R/f_generics_clvfitted.R index 253e9b89..51a1e99b 100644 --- a/R/f_generics_clvfitted.R +++ b/R/f_generics_clvfitted.R @@ -45,7 +45,7 @@ setGeneric("clv.fitted.get.LL", signature = "clv.fitted", def = function(clv.fit # is not trivial as there are plenty of options. # To reproduce it, the exact steps of the estimation are repeated here. - # Start parameters are not really required, they are just stored as parameter + # Start parameters are not really required, they are just stored as item # `par` for `optimx()` final.coefs <- drop(tail(coef(clv.fitted@optimx.estimation.output), n=1)) @@ -69,28 +69,30 @@ setGeneric("clv.fitted.get.LL", signature = "clv.fitted", def = function(clv.fit names.optimx.args <- setdiff(formalArgs(optimx), "...") call.args <- prepared.optimx.args[!(names(prepared.optimx.args) %in% names.optimx.args)] - # Save memory as the returned method is a closure and has the environment in - # which is was defined attached. Hence all variables in this method here. - # rm(prepared.optimx.args) - fn.LL.entrypoint <- prepared.optimx.args$fn - # rm(final.coefs) - # rm(clv.fitted) - # rm() + # Could save memory as the returned method is a closure and has the environment + # in which is was defined attached. Hence all variables in this method here + # which may be large. However, it can also be useful to have these objects. + # Wrapper to call the LL with the original args. + # It is preferred to return a method rather than calling it immediately + # because generating the call args may take time. LL <- function(params){ req.names <- call.args$LL.param.names.to.optimx # Ensure named same as original if(!(identical(sort(names(params)), sort(req.names)))){ - check_err_msg(paste0("'params' has to be named ", paste(req.names, collapse = ", "),".")) + check_err_msg(paste0( + "'params' has to be named ", + paste(req.names, collapse = ", "), + ". Often, `drop(coef(model@optimx.estimation.output))` is useful.")) } # Ensure name and position are the same as original order. This is required as # any param input will be re-named by slapping `LL.param.names.to.optimx` on # them (using names() <- ) in the first interlayer and then further accessed - # by name + # by name. call.args$LL.params <- params[req.names] # bring to correct order - return(do.call(what = fn.LL.entrypoint, args = call.args)) + return(do.call(what = prepared.optimx.args$fn, args = call.args)) } return(LL) From 299fbdb16661d48427c77c37311bd9d274957d66 Mon Sep 17 00:00:00 2001 From: Patrik Schilter Date: Sun, 17 Aug 2025 17:00:23 +0200 Subject: [PATCH 3/8] Tests for hessian(): Add test using constrained params --- tests/testthat/tests_correctness_hessian.R | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/testthat/tests_correctness_hessian.R b/tests/testthat/tests_correctness_hessian.R index 9e09e7a2..a9759915 100644 --- a/tests/testthat/tests_correctness_hessian.R +++ b/tests/testthat/tests_correctness_hessian.R @@ -34,6 +34,16 @@ test_that("hessian() produces same result - static cov", { fn.compare.hessian( fit.apparel.static(model=m, optimx.args = optimx.args) ) + + # With constrained covs + if(!identical(m, gg)){ + fn.compare.hessian( + fit.apparel.static( + model=m, + names.cov.constr = "Gender", + optimx.args = optimx.args) + ) + } } # With cor @@ -55,6 +65,10 @@ test_that("hessian() produces same result - dyn cov", { fn.compare.hessian( fit.apparel.dyncov(model = pnbd, use.cor=TRUE, optimx.args = optimx.args) ) + # With constrained covs + fn.compare.hessian( + fit.apparel.dyncov(model = pnbd, names.cov.constr = "Gender", optimx.args = optimx.args) + ) }) From f8f250036afe7910023e74fa9980a41b7f850a22 Mon Sep 17 00:00:00 2001 From: Patrik Schilter Date: Sun, 17 Aug 2025 17:00:38 +0200 Subject: [PATCH 4/8] Rename file --- .../{tests_correctness_hessian.R => test_correctness_hessian.R} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/testthat/{tests_correctness_hessian.R => test_correctness_hessian.R} (100%) diff --git a/tests/testthat/tests_correctness_hessian.R b/tests/testthat/test_correctness_hessian.R similarity index 100% rename from tests/testthat/tests_correctness_hessian.R rename to tests/testthat/test_correctness_hessian.R From 9c85e2b698a799929c481bd5fbc97ddb4f9524f6 Mon Sep 17 00:00:00 2001 From: Patrik Schilter Date: Sun, 17 Aug 2025 17:02:24 +0200 Subject: [PATCH 5/8] Use `clv.get.LL` within `hessian()` --- R/f_interface_hessian.R | 43 ++++++++++------------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/R/f_interface_hessian.R b/R/f_interface_hessian.R index 9f9dcfab..08ae213d 100644 --- a/R/f_interface_hessian.R +++ b/R/f_interface_hessian.R @@ -25,52 +25,29 @@ hessian.clv.fitted <- function(object, method.args = list()){ # Register for dispatch on a method defined in another package by using # @exportS3Method which adds `S3method(numDeriv::hessian,clv.fitted)` to NAMESPACE - # To calculate the Hessian, the LL needs to be called with the final parameters - # Calling the LL with the exact same inputs/specification as when the fitting - # is however not trivial as there are plenty of options. - # To reproduce it, the exact steps of the estimation are repeated here. - + # Get final parameters # To get coefficients at the same scale (log-transformed) and names as when - # estimating the model, get them from the optimx results. + # estimating the model, get them directly from the optimx output. # Cannot use coef() as the reported parameters are transformed back and named - # differently + # differently. final.coefs <- drop(tail(coef(object@optimx.estimation.output), n=1)) if(anyNA(final.coefs)){ check_err_msg("Cannot proceed because there are NAs in the estimated coefficients!") } - prepared.optimx.args <- clv.controlflow.estimate.prepare.optimx.args( - clv.fitted=object, - start.params.all=final.coefs) - - prepared.optimx.args <- clv.model.prepare.optimx.args( - clv.model=object@clv.model, - clv.fitted=object, - prepared.optimx.args=prepared.optimx.args) - - prepared.optimx.args[["LL.param.names.to.optimx"]] <- names(prepared.optimx.args$par) - - # In the estimation procedure, the user can also supply custom `optimx.args` - # which override `prepared.optimx.args` here. Because optimx is not called - # here, there is no need to add `optimx.args` here. + # Get LL + fn.LL <- clv.fitted.get.LL(object) - # The generated args also contain parameters for optimx. These are not required - # for the LL and need to be removed. - names.optimx.args <- setdiff(formalArgs(optimx), "...") - call.args <- prepared.optimx.args[!(names(prepared.optimx.args) %in% names.optimx.args)] - - - # Wrapper to call the LL with the original args and `par` given by numDeriv - fn.with.call.args <- function(par){ - call.args$LL.params <- par - return(do.call(prepared.optimx.args$fn, call.args)) - } + # fn.call.LL.named <- function(par){ + # names(par) <- names(final.coefs) + # return(fn.LL(par)) + # } # Have to refer to `numDeriv` namespace directly (`::`) as `hessian()` would # dispatch to `CLVTools::hessian` and fail if `numDeriv` is not attached H <- numDeriv::hessian( - func=fn.with.call.args, + func=fn.LL, x=final.coefs, method="Richardson", method.args = method.args) From f61fab2e66728a70ec1984b9a3f53e408fab26ef Mon Sep 17 00:00:00 2001 From: Patrik Schilter Date: Sun, 17 Aug 2025 20:48:08 +0200 Subject: [PATCH 6/8] Tests: Add more tests for `hessian()` --- tests/testthat/test_correctness_hessian.R | 46 +++++++++++++++++------ 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/tests/testthat/test_correctness_hessian.R b/tests/testthat/test_correctness_hessian.R index a9759915..de6926be 100644 --- a/tests/testthat/test_correctness_hessian.R +++ b/tests/testthat/test_correctness_hessian.R @@ -30,23 +30,40 @@ test_that("hessian() produces same result - no cov", { test_that("hessian() produces same result - static cov", { skip_on_cran() - for(m in list(pnbd, bgnbd, ggomnbd, gg)){ + for(m in list(pnbd, bgnbd, ggomnbd)){ + # Default specification fn.compare.hessian( fit.apparel.static(model=m, optimx.args = optimx.args) ) # With constrained covs - if(!identical(m, gg)){ - fn.compare.hessian( - fit.apparel.static( - model=m, - names.cov.constr = "Gender", - optimx.args = optimx.args) - ) - } + fn.compare.hessian( + fit.apparel.static( + model=m, + names.cov.constr = "Gender", + optimx.args = optimx.args) + ) + + # With regularization + fn.compare.hessian( + fit.apparel.static( + model=m, + reg.lambdas = c(life = 10, trans=5), + optimx.args = optimx.args) + ) + + # With constrained covs & regularization + fn.compare.hessian( + fit.apparel.static( + model=m, + names.cov.constr = "Channel", + reg.lambdas = c(life = 10, trans=5), + optimx.args = optimx.args) + ) } - # With cor + + # PNBD only: With cor fn.compare.hessian( fit.apparel.static(model = pnbd, use.cor=TRUE, optimx.args = optimx.args) ) @@ -56,7 +73,7 @@ test_that("hessian() produces same result - static cov", { test_that("hessian() produces same result - dyn cov", { skip_on_cran() - # No cor + # Default fn.compare.hessian( fit.apparel.dyncov(model = pnbd, optimx.args = optimx.args) ) @@ -65,10 +82,17 @@ test_that("hessian() produces same result - dyn cov", { fn.compare.hessian( fit.apparel.dyncov(model = pnbd, use.cor=TRUE, optimx.args = optimx.args) ) + # With constrained covs fn.compare.hessian( fit.apparel.dyncov(model = pnbd, names.cov.constr = "Gender", optimx.args = optimx.args) ) + + # With regularization + fn.compare.hessian( + fit.apparel.dyncov(model = pnbd, reg.lambdas = c(trans=10, life=5), optimx.args = optimx.args) + ) + }) From 011e8ecf026ff9c045116e8b96d067e99885a727 Mon Sep 17 00:00:00 2001 From: Patrik Schilter Date: Thu, 28 Aug 2025 17:31:10 +0200 Subject: [PATCH 7/8] Tests: Internal `clv.fitted.get.LL` --- tests/testthat/test_correctness_hessian.R | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/testthat/test_correctness_hessian.R b/tests/testthat/test_correctness_hessian.R index de6926be..57975e07 100644 --- a/tests/testthat/test_correctness_hessian.R +++ b/tests/testthat/test_correctness_hessian.R @@ -98,8 +98,37 @@ test_that("hessian() produces same result - dyn cov", { test_that("hessian() fails if parameters are non-finite",{ + skip_on_cran() + p.cdnow <- fit.cdnow(optimx.args=optimx.args) p.cdnow@optimx.estimation.output[1, "log.r"] <- NA_real_ expect_error(hessian(p.cdnow), regexp = "Cannot proceed") }) + +test_that("Internal clv.fitted.get.LL: Params position and order checked", { + # Indirectly tested for correctness by being used in hessian() + skip_on_cran() + + p.reg.constr <- fit.apparel.static( + model = pnbd, + reg.lambdas = c(trans = 4, life = 9), + names.cov.constr = "Gender", + optimx.args= optimx.args) + + LL.reg.constr <- clv.fitted.get.LL(p.reg.constr) + final.coefs <- drop(coef(p.reg.constr@optimx.estimation.output)) + + # Have to be named + expect_error(LL.reg.constr(setNames(final.coefs, NULL)), regexp = "has to be named") + + # Does not work with extra coefs + expect_error(LL.reg.constr(coef(p.reg.constr)), regexp = "has to be named") + + # Results are independent of order + expect_identical( + LL.reg.constr(sort(final.coefs, decreasing = FALSE)), + LL.reg.constr(sort(final.coefs, decreasing = TRUE)) + ) + +}) From b5920f85658b7cb38acc168e9ad87a0db268914a Mon Sep 17 00:00:00 2001 From: Patrik Schilter Date: Thu, 28 Aug 2025 17:32:46 +0200 Subject: [PATCH 8/8] `clv.fitted.get.LL`: No longer as generic but as regular method --- R/all_generics.R | 5 ---- R/class_clv_fitted.R | 60 +++++++++++++++++++++++++++++++++++++++ R/f_generics_clvfitted.R | 61 ---------------------------------------- 3 files changed, 60 insertions(+), 66 deletions(-) diff --git a/R/all_generics.R b/R/all_generics.R index 4c731fc6..51eb2e86 100644 --- a/R/all_generics.R +++ b/R/all_generics.R @@ -30,11 +30,6 @@ setGeneric("clv.controlflow.estimate.process.post.estimation", def=function(clv. standardGeneric("clv.controlflow.estimate.process.post.estimation")) -# .. Get LL ---------------------------------------------------------- -setGeneric("clv.fitted.get.LL", def = function(clv.fitted) - standardGeneric("clv.fitted.get.LL")) - - # . Predict ----------------------------------------------------------------------------------------------- setGeneric("clv.controlflow.predict.check.inputs", def = function(clv.fitted, verbose, uncertainty, num.boots, level, ...) standardGeneric("clv.controlflow.predict.check.inputs")) diff --git a/R/class_clv_fitted.R b/R/class_clv_fitted.R index 1a168c95..d3042107 100644 --- a/R/class_clv_fitted.R +++ b/R/class_clv_fitted.R @@ -49,3 +49,63 @@ setClass(Class = "clv.fitted", contains = "VIRTUAL", optimx.hessian = matrix(data = numeric(0)))) + + +clv.fitted.get.LL <- function(clv.fitted){ + + # Calling the LL with the exact same inputs/specification as when the fitting + # is not trivial as there are plenty of options. + # To reproduce it, the exact steps of the estimation are repeated here. + + # Start parameters are not really required, they are just stored as item + # `par` for `optimx()` + final.coefs <- drop(tail(coef(clv.fitted@optimx.estimation.output), n=1)) + + prepared.optimx.args <- clv.controlflow.estimate.prepare.optimx.args( + clv.fitted=clv.fitted, + start.params.all= final.coefs) + + prepared.optimx.args <- clv.model.prepare.optimx.args( + clv.model=clv.fitted@clv.model, + clv.fitted=clv.fitted, + prepared.optimx.args=prepared.optimx.args) + + prepared.optimx.args[["LL.param.names.to.optimx"]] <- names(prepared.optimx.args$par) + + # In the estimation procedure, the user can also supply custom `optimx.args` + # which override `prepared.optimx.args` here. Because optimx is not called + # here, there is no need to add `optimx.args` here. + + # The generated args also contain parameters for optimx. These are not required + # for the LL and need to be removed. This also removes the LL itself (`fn`). + names.optimx.args <- setdiff(formalArgs(optimx), "...") + call.args <- prepared.optimx.args[!(names(prepared.optimx.args) %in% names.optimx.args)] + + # Could save memory as the returned method is a closure and has the environment + # in which is was defined attached. Hence all variables in this method here + # which may be large. However, it can also be useful to have these objects. + + # Wrapper to call the LL with the original args. + # It is preferred to return a method rather than calling it immediately + # because generating the call args may take time. + LL <- function(params){ + req.names <- call.args$LL.param.names.to.optimx + + # Ensure named same as original + if(!(identical(sort(names(params)), sort(req.names)))){ + check_err_msg(paste0( + "'params' has to be named ", + paste(req.names, collapse = ", "), + ". Often, `drop(coef(model@optimx.estimation.output))` is useful.")) + } + + # Ensure name and position are the same as original order. This is required as + # any param input will be re-named by slapping `LL.param.names.to.optimx` on + # them (using names() <- ) in the first interlayer and then further accessed + # by name. + call.args$LL.params <- params[req.names] # bring to correct order + return(do.call(what = prepared.optimx.args$fn, args = call.args)) + } + + return(LL) +} diff --git a/R/f_generics_clvfitted.R b/R/f_generics_clvfitted.R index 51a1e99b..7e2a1423 100644 --- a/R/f_generics_clvfitted.R +++ b/R/f_generics_clvfitted.R @@ -36,64 +36,3 @@ setMethod("clv.fitted.estimate.same.specification.on.new.data", signature = "clv return(new.fitted) }) - -# . clv.fitted.get.LL ---------------------------------------------------------- -#' @include all_generics.R -setGeneric("clv.fitted.get.LL", signature = "clv.fitted", def = function(clv.fitted){ - - # Calling the LL with the exact same inputs/specification as when the fitting - # is not trivial as there are plenty of options. - # To reproduce it, the exact steps of the estimation are repeated here. - - # Start parameters are not really required, they are just stored as item - # `par` for `optimx()` - final.coefs <- drop(tail(coef(clv.fitted@optimx.estimation.output), n=1)) - - prepared.optimx.args <- clv.controlflow.estimate.prepare.optimx.args( - clv.fitted=clv.fitted, - start.params.all= final.coefs) - - prepared.optimx.args <- clv.model.prepare.optimx.args( - clv.model=clv.fitted@clv.model, - clv.fitted=clv.fitted, - prepared.optimx.args=prepared.optimx.args) - - prepared.optimx.args[["LL.param.names.to.optimx"]] <- names(prepared.optimx.args$par) - - # In the estimation procedure, the user can also supply custom `optimx.args` - # which override `prepared.optimx.args` here. Because optimx is not called - # here, there is no need to add `optimx.args` here. - - # The generated args also contain parameters for optimx. These are not required - # for the LL and need to be removed. This also removes the LL itself (`fn`). - names.optimx.args <- setdiff(formalArgs(optimx), "...") - call.args <- prepared.optimx.args[!(names(prepared.optimx.args) %in% names.optimx.args)] - - # Could save memory as the returned method is a closure and has the environment - # in which is was defined attached. Hence all variables in this method here - # which may be large. However, it can also be useful to have these objects. - - # Wrapper to call the LL with the original args. - # It is preferred to return a method rather than calling it immediately - # because generating the call args may take time. - LL <- function(params){ - req.names <- call.args$LL.param.names.to.optimx - - # Ensure named same as original - if(!(identical(sort(names(params)), sort(req.names)))){ - check_err_msg(paste0( - "'params' has to be named ", - paste(req.names, collapse = ", "), - ". Often, `drop(coef(model@optimx.estimation.output))` is useful.")) - } - - # Ensure name and position are the same as original order. This is required as - # any param input will be re-named by slapping `LL.param.names.to.optimx` on - # them (using names() <- ) in the first interlayer and then further accessed - # by name. - call.args$LL.params <- params[req.names] # bring to correct order - return(do.call(what = prepared.optimx.args$fn, args = call.args)) - } - - return(LL) -})