Skip to content

Commit 6836565

Browse files
queeliusclaude
andcommitted
Prepare for CRAN submission
Simplify: - Vectorize loglik exact/right hot path; cache extract_md_data in closure so optim/numDeriv don't re-validate; unify left/interval branches; short-circuit runif when p=0; drop unused closure captures Architecture: - Promote maskedcauses from Suggests to Imports so the series_md protocol generics (conditional_cause_probability, cause_probability) come from exactly one place; delete redeclared stubs; re-export for API continuity Vignettes: - Add four vignettes: maskedhaz (overview), custom-components (mixed distribution series), censoring-and-masking (four omega types plus cross-validation against maskedcauses), hypothesis-tests (LRT, Wald, score, test algebra via hypothesize). All build cleanly. Examples and documentation: - Add @examples to every exported method (13 total); all verified via devtools::run_examples(run_donttest = TRUE) - Add inst/WORDLIST for technical terms - Write cran-comments.md - Expand NEWS.md to describe the 0.1.0 release in detail Infrastructure: - R CMD check --as-cran: 0 errors, 0 warnings, 0 notes (besides the sandbox "unable to verify current time" that disappears on CRAN) - 148 tests passing, coverage 96.81% - rhub v2 GitHub Actions workflow for multi-platform checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c6914a6 commit 6836565

37 files changed

Lines changed: 1498 additions & 248 deletions

.Rbuildignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
^pkgdown$
88
^\.github$
99
^CLAUDE\.md$
10+
^cran-comments\.md$

.github/workflows/rhub.yaml

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# R-hub's generic GitHub Actions workflow file. It's canonical location is at
2+
# https://github.com/r-hub/actions/blob/v1/workflows/rhub.yaml
3+
# You can update this file to a newer version using the rhub2 package:
4+
#
5+
# rhub::rhub_setup()
6+
#
7+
# It is unlikely that you need to modify this file manually.
8+
9+
name: R-hub
10+
run-name: "${{ github.event.inputs.id }}: ${{ github.event.inputs.name || format('Manually run by {0}', github.triggering_actor) }}"
11+
12+
on:
13+
workflow_dispatch:
14+
inputs:
15+
config:
16+
description: 'A comma separated list of R-hub platforms to use.'
17+
type: string
18+
default: 'linux,windows,macos'
19+
name:
20+
description: 'Run name. You can leave this empty now.'
21+
type: string
22+
id:
23+
description: 'Unique ID. You can leave this empty now.'
24+
type: string
25+
26+
jobs:
27+
28+
setup:
29+
runs-on: ubuntu-latest
30+
outputs:
31+
containers: ${{ steps.rhub-setup.outputs.containers }}
32+
platforms: ${{ steps.rhub-setup.outputs.platforms }}
33+
34+
steps:
35+
# NO NEED TO CHECKOUT HERE
36+
- uses: r-hub/actions/setup@v1
37+
with:
38+
config: ${{ github.event.inputs.config }}
39+
id: rhub-setup
40+
41+
linux-containers:
42+
needs: setup
43+
if: ${{ needs.setup.outputs.containers != '[]' }}
44+
runs-on: ubuntu-latest
45+
name: ${{ matrix.config.label }}
46+
strategy:
47+
fail-fast: false
48+
matrix:
49+
config: ${{ fromJson(needs.setup.outputs.containers) }}
50+
container:
51+
image: ${{ matrix.config.container }}
52+
53+
steps:
54+
- uses: r-hub/actions/checkout@v1
55+
- uses: r-hub/actions/platform-info@v1
56+
with:
57+
token: ${{ secrets.RHUB_TOKEN }}
58+
job-config: ${{ matrix.config.job-config }}
59+
- uses: r-hub/actions/setup-deps@v1
60+
with:
61+
token: ${{ secrets.RHUB_TOKEN }}
62+
job-config: ${{ matrix.config.job-config }}
63+
- uses: r-hub/actions/run-check@v1
64+
with:
65+
token: ${{ secrets.RHUB_TOKEN }}
66+
job-config: ${{ matrix.config.job-config }}
67+
68+
other-platforms:
69+
needs: setup
70+
if: ${{ needs.setup.outputs.platforms != '[]' }}
71+
runs-on: ${{ matrix.config.os }}
72+
name: ${{ matrix.config.label }}
73+
strategy:
74+
fail-fast: false
75+
matrix:
76+
config: ${{ fromJson(needs.setup.outputs.platforms) }}
77+
78+
steps:
79+
- uses: r-hub/actions/checkout@v1
80+
- uses: r-hub/actions/setup-r@v1
81+
with:
82+
job-config: ${{ matrix.config.job-config }}
83+
token: ${{ secrets.RHUB_TOKEN }}
84+
- uses: r-hub/actions/platform-info@v1
85+
with:
86+
token: ${{ secrets.RHUB_TOKEN }}
87+
job-config: ${{ matrix.config.job-config }}
88+
- uses: r-hub/actions/setup-deps@v1
89+
with:
90+
job-config: ${{ matrix.config.job-config }}
91+
token: ${{ secrets.RHUB_TOKEN }}
92+
- uses: r-hub/actions/run-check@v1
93+
with:
94+
job-config: ${{ matrix.config.job-config }}
95+
token: ${{ secrets.RHUB_TOKEN }}

DESCRIPTION

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ Imports:
2222
flexhaz,
2323
algebraic.dist,
2424
likelihood.model,
25+
maskedcauses,
2526
generics,
2627
numDeriv,
2728
stats
2829
Suggests:
2930
testthat (>= 3.0.0),
30-
maskedcauses,
31+
algebraic.mle,
32+
hypothesize,
3133
knitr,
3234
rmarkdown
3335
VignetteBuilder: knitr

NAMESPACE

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export(sample_components)
3737
export(sampler)
3838
export(score)
3939
export(surv)
40-
importFrom(algebraic.dist,cdf)
4140
importFrom(algebraic.dist,hazard)
4241
importFrom(algebraic.dist,params)
4342
importFrom(algebraic.dist,sampler)
@@ -54,6 +53,8 @@ importFrom(likelihood.model,hess_loglik)
5453
importFrom(likelihood.model,loglik)
5554
importFrom(likelihood.model,rdata)
5655
importFrom(likelihood.model,score)
56+
importFrom(maskedcauses,cause_probability)
57+
importFrom(maskedcauses,conditional_cause_probability)
5758
importFrom(numDeriv,grad)
5859
importFrom(numDeriv,hessian)
5960
importFrom(serieshaz,component)

NEWS.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,34 @@
11
# maskedhaz 0.1.0
22

3-
* Initial release
4-
* `dfr_series_md()` constructor for masked-cause likelihood models
5-
* Log-likelihood supporting exact, right, left, and interval censoring
6-
* Score and Hessian via `numDeriv`
7-
* MLE fitting via `optim` returning `fisher_mle` objects
8-
* Data generation with configurable censoring and masking
9-
* Conditional and marginal cause-of-failure probabilities
10-
* Cross-validated against `maskedcauses` for exponential components
3+
Initial CRAN release.
4+
5+
## Features
6+
7+
* `dfr_series_md()` constructor builds a masked-cause likelihood model for a
8+
series system whose components are arbitrary `dfr_dist` objects from the
9+
`flexhaz` and `serieshaz` packages. The resulting model implements the
10+
`series_md` protocol defined in `maskedcauses`.
11+
* Full support for four observation types via the `omega` column:
12+
`"exact"`, `"right"`, `"left"`, `"interval"`. Left- and interval-censored
13+
contributions use `stats::integrate()`; exact and right-censored rows use
14+
vectorised closed-form expressions.
15+
* `loglik()`, `score()`, `hess_loglik()`, `fit()`, and `rdata()` methods
16+
that dispatch through the `likelihood.model` generics. `fit()` returns a
17+
`fisher_mle` object, which realises the `mle_fit` and `algebraic.dist`
18+
interfaces, so standard MLE diagnostics (`coef`, `vcov`, `confint`, `se`,
19+
`bias`, `observed_fim`, `as_dist`, `sampler`, `expectation`) all work
20+
uniformly.
21+
* Methods for the `maskedcauses` domain generics
22+
`conditional_cause_probability()` and `cause_probability()`.
23+
* Cross-validated against the closed-form exponential-series likelihood
24+
from `maskedcauses` to confirm the numerical integration path matches
25+
analytical results to integrator tolerance.
26+
27+
## Vignettes
28+
29+
* `maskedhaz` (overview): protocol, component, and MLE-result stacks;
30+
quick tour from construction to diagnostics.
31+
* `custom-components`: mixed-distribution series with Weibull, exponential,
32+
Gompertz, and log-logistic components.
33+
* `censoring-and-masking`: the four observation types with worked examples
34+
and cross-validation against `maskedcauses`.

R/dfr_series_md.R

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ dfr_series_md <- function(series = NULL, components = NULL,
124124
#'
125125
#' @param x Object to test.
126126
#' @return Logical scalar.
127+
#' @examples
128+
#' model <- dfr_series_md(components = list(
129+
#' dfr_exponential(0.1), dfr_exponential(0.2)
130+
#' ))
131+
#' is_dfr_series_md(model) # TRUE
132+
#' is_dfr_series_md(42) # FALSE
127133
#' @export
128134
is_dfr_series_md <- function(x) {
129135
inherits(x, "dfr_series_md")
@@ -135,21 +141,23 @@ is_dfr_series_md <- function(x) {
135141
#' @param x A \code{dfr_series_md} object.
136142
#' @param ... Additional arguments (unused).
137143
#' @return Invisibly returns \code{x}.
144+
#' @examples
145+
#' model <- dfr_series_md(components = list(
146+
#' dfr_weibull(shape = 2, scale = 100),
147+
#' dfr_exponential(0.05)
148+
#' ))
149+
#' print(model)
138150
#' @export
139151
print.dfr_series_md <- function(x, ...) {
140-
m <- x$series$m
152+
series <- x$series
153+
m <- series$m
141154
cat(sprintf("Masked-cause likelihood model (%d-component series)\n", m))
142155
for (j in seq_len(m)) {
143-
np <- x$series$n_par[j]
144-
par_j <- if (!is.null(x$series$par)) {
145-
x$series$par[x$series$layout[[j]]]
146-
}
147-
par_str <- if (!is.null(par_j)) {
156+
par_j <- if (is.null(series$par)) NULL else series$par[series$layout[[j]]]
157+
par_str <- if (is.null(par_j)) "unknown" else
148158
paste(format(par_j, digits = 4), collapse = ", ")
149-
} else {
150-
"unknown"
151-
}
152-
cat(sprintf(" Component %d: %d param(s) [%s]\n", j, np, par_str))
159+
cat(sprintf(" Component %d: %d param(s) [%s]\n",
160+
j, series$n_par[j], par_str))
153161
}
154162
cat("Data columns:", x$lifetime, "(lifetime),",
155163
x$omega, "(type),", x$candset, "* (candidates)\n")

R/fit.R

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,23 @@
1111
#'
1212
#' @details
1313
#' Uses \code{\link[stats]{optim}} to maximize the log-likelihood. The score
14-
#' function (gradient) is provided for gradient-based methods. The Hessian at
15-
#' the MLE is computed for variance-covariance estimation.
14+
#' function (gradient) is computed from the same \code{\link{loglik}} closure
15+
#' via \code{\link[numDeriv]{grad}}, and the Hessian at the MLE via
16+
#' \code{\link[numDeriv]{hessian}}. One-parameter problems auto-upgrade from
17+
#' Nelder-Mead to BFGS with a warning, because Nelder-Mead is unreliable in
18+
#' one dimension.
1619
#'
20+
#' @examples
21+
#' \donttest{
22+
#' model <- dfr_series_md(components = list(
23+
#' dfr_exponential(0.1), dfr_exponential(0.2)
24+
#' ))
25+
#' set.seed(1)
26+
#' df <- rdata(model)(theta = c(0.1, 0.2), n = 200, tau = 10, p = 0)
27+
#' solver <- fit(model)
28+
#' result <- solver(df, par = c(0.15, 0.15))
29+
#' coef(result)
30+
#' }
1731
#' @importFrom generics fit
1832
#' @importFrom likelihood.model fisher_mle
1933
#' @importFrom stats optim
@@ -22,38 +36,37 @@
2236
#' @export
2337
fit.dfr_series_md <- function(object, ...) {
2438
ll_fn <- loglik(object)
25-
s_fn <- score(object)
26-
H_fn <- hess_loglik(object)
2739

2840
function(df, par,
2941
method = c("Nelder-Mead", "BFGS", "SANN", "CG",
3042
"L-BFGS-B", "Brent"),
3143
..., control = list()) {
3244
stopifnot(!is.null(par))
33-
defaults <- list(fnscale = -1)
34-
control <- modifyList(defaults, control)
45+
user_picked_method <- !missing(method)
3546
method <- match.arg(method)
47+
control <- modifyList(list(fnscale = -1), control)
3648

37-
if (length(par) == 1 && method == "Nelder-Mead")
49+
if (length(par) == 1 && method == "Nelder-Mead") {
50+
if (user_picked_method)
51+
warning("Nelder-Mead is unreliable for 1-parameter problems; ",
52+
"switching to BFGS.")
3853
method <- "BFGS"
54+
}
55+
56+
obj <- function(p) ll_fn(df, p)
57+
gr <- function(p) numDeriv::grad(obj, p)
3958

4059
sol <- optim(
41-
par = par,
42-
fn = function(p) ll_fn(df, p),
43-
gr = if (method == "SANN") NULL else function(p) s_fn(df, p),
44-
hessian = FALSE,
45-
method = method,
46-
control = control
60+
par = par, fn = obj,
61+
gr = if (method == "SANN") NULL else gr,
62+
hessian = FALSE, method = method, control = control
4763
)
4864

49-
hessian <- H_fn(df, sol$par)
50-
score_at_mle <- s_fn(df, sol$par)
51-
5265
fisher_mle(
5366
par = sol$par,
5467
loglik_val = sol$value,
55-
hessian = hessian,
56-
score_val = score_at_mle,
68+
hessian = numDeriv::hessian(obj, sol$par),
69+
score_val = numDeriv::grad(obj, sol$par),
5770
nobs = nrow(df),
5871
converged = (sol$convergence == 0)
5972
)

0 commit comments

Comments
 (0)