The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.

Extending EDI with Your Own Inference and Design Classes

EDI is implemented with R6 classes. Advanced users can define their own R6 classes outside the package and reuse EDI’s design storage, response handling, randomization, bootstrap, and summary methods. This page is the supported extension contract: which base classes to build on, the one method each asks you to implement, and the rules that keep your class working with the rest of the package. It is written for authors working outside EDI; contributing a class to EDI itself is a different, heavier process (see the last section).

library(EDI)
library(R6)

How EDI classes are built

Both the Inference* and Design* hierarchies are shallow and component-based. Inheritance answers only one question — “is every child substitutable for this parent as the same kind of estimator / design?” — and every optional behavior is a registered component composed by a factory, with every optional public method backed by a capability:

The consequence for you is simple: build on the custom shells below (they are themselves factory-built, so the components and capabilities are already wired), implement the one documented hook, and call your class directly — registry-driven discovery will never list an external class.

The shells are intentionally internal while the extension contract is experimental. Retrieve them with getFromNamespace():

InferenceCustomAsymp <- getFromNamespace("InferenceCustomAsymp", "EDI")
InferenceCustomRand <- getFromNamespace("InferenceCustomRand", "EDI")
InferenceCustomBoot <- getFromNamespace("InferenceCustomBoot", "EDI")
DesignFixedCustom <- getFromNamespace("DesignFixedCustom", "EDI")
DesignCustomSequential <- getFromNamespace("DesignCustomSequential", "EDI")

The inference extension contract

A custom asymptotic inference class inherits from InferenceCustomAsymp (built on Inference with the Wald and NonparametricBootstrap components) and implements a public fit(estimate_only = FALSE) method that returns a named list with:

When estimate_only = TRUE (resampling loops) only estimate is needed; skip the variance work.

Read data through the public accessors, never private fields: get_response(), get_treatment(), get_covariates(), get_analysis_data(), get_design_object(), get_response_type().

InferenceMedianDiff <- R6Class(
  "InferenceMedianDiff",
  inherit = InferenceCustomAsymp,
  # Required when subclassing EDI's factory-built classes: lazily loaded
  # components install their real methods onto the object after construction,
  # which needs an unlocked environment.
  lock_objects = FALSE,
  public = list(
    fit = function(estimate_only = FALSE) {
      dat <- self$get_analysis_data()
      y_t <- dat$y[dat$w == 1]
      y_c <- dat$y[dat$w == 0]

      est <- stats::median(y_t) - stats::median(y_c)
      if (estimate_only) {
        return(list(estimate = est))
      }

      list(
        estimate = est,
        se = sqrt(stats::var(y_t) / length(y_t) + stats::var(y_c) / length(y_c)),
        df = length(y_t) + length(y_c) - 2,
        model = NULL
      )
    }
  )
)

des <- DesignFixedBernoulli$new(n = 20, response_type = "continuous", verbose = FALSE)
des$add_all_subjects_to_experiment(data.frame(x = seq_len(20)))
des$overwrite_all_subject_assignments(rep(c(0, 1), each = 10))
des$add_all_subject_responses(rnorm(20))

inf <- InferenceMedianDiff$new(des)
inf$compute_estimate()
#> [1] 0.1369039
inf$compute_asymp_two_sided_pval()
#> [1] 0.8177541
inf$compute_asymp_confidence_interval()
#>      2.5%     97.5% 
#> -1.093143  1.366950
inf$compute_bootstrap_two_sided_pval(B = 101, show_progress = FALSE)
#> [1] 0.7920792
inf$capabilities()
#> [1] "jackknife"               "wald"                   
#> [3] "randomization_test"      "randomization_ci"       
#> [5] "nonparametric_bootstrap"
inf$supports("wald")
#> wald 
#> TRUE

Randomization and bootstrap shells

InferenceMedianDiffRand <- R6Class(
  "InferenceMedianDiffRand",
  inherit = InferenceCustomRand,
  lock_objects = FALSE,
  public = list(
    fit = function(estimate_only = FALSE) {
      dat <- self$get_analysis_data()
      list(estimate = stats::median(dat$y[dat$w == 1]) - stats::median(dat$y[dat$w == 0]))
    }
  )
)
inf_rand <- InferenceMedianDiffRand$new(des)
inf_rand$compute_estimate()
#> [1] 0.1369039
inf_rand$compute_rand_two_sided_pval(r = 200, show_progress = FALSE)
#> [1] 0.74
inf_rand$capabilities()
#> [1] "randomization_test" "randomization_ci"

InferenceMedianDiffBoot <- R6Class(
  "InferenceMedianDiffBoot",
  inherit = InferenceCustomBoot,
  lock_objects = FALSE,
  public = list(
    fit = function(estimate_only = FALSE) {
      dat <- self$get_analysis_data()
      list(estimate = stats::median(dat$y[dat$w == 1]) - stats::median(dat$y[dat$w == 0]))
    }
  )
)
inf_boot <- InferenceMedianDiffBoot$new(des)
inf_boot$compute_bootstrap_confidence_interval(B = 101, show_progress = FALSE)
#>      2.5%     97.5% 
#> -1.702487  1.242510

Subclassing rules and capability detection

# Not registered ...
"InferenceMedianDiff" %in% des$applicable_inference_class_names()
#> [1] FALSE
# ... but capabilities resolve through the registered shell it inherits from.
identical(inf$capabilities(), InferenceCustomAsymp$new(des)$capabilities())
#> [1] TRUE

Custom designs

The design shells are factory-built bases (DesignFixedCustom inherits DesignFixed; DesignCustomSequential inherits DesignSeqOneByOne) that route all randomization through one user hook:

EDI handles subject storage, response recording, and validation. Pass lock_objects = FALSE here too. When your inference code needs to know what a design can do, use des$capabilities() / des$supports() — the vocabulary is "blocking", "matching", "cluster", "batch_w_pregeneration", "resampling", "randomization_draw", "resampling_replay" — rather than class-identity checks. The same unregistered-subclass fallbacks apply on this side: instance capability queries work for any subclass, unregistered names are treated as concrete (freely instantiable), and the package’s inference classes are discoverable on a custom design exactly as on a built-in one because discovery keys on design metadata, not design class.

DesignFixedAlternating <- R6Class(
  "DesignFixedAlternating",
  inherit = DesignFixedCustom,
  lock_objects = FALSE,
  public = list(
    draw_assignments = function(r = 1) {
      n <- self$get_n()
      matrix(rep_len(c(0, 1), n), nrow = n, ncol = r)
    }
  )
)
des_alt <- DesignFixedAlternating$new(n = 10, response_type = "continuous", verbose = FALSE)
des_alt$add_all_subjects_to_experiment(data.frame(x = 1:10))
des_alt$assign_w_to_all_subjects()
des_alt$get_w()
#>  [1] 0 1 0 1 0 1 0 1 0 1
des_alt$capabilities()
#> [1] "resampling"         "randomization_draw" "resampling_replay"
des_alt$add_all_subject_responses(rnorm(10))
head(des_alt$applicable_inference_class_names())
#> [1] "InferenceAllSimpleAverageDiff"       "InferenceAllSimpleMeanDiffPooledVar"
#> [3] "InferenceAllSimpleWilcox"            "InferenceContinLin"                 
#> [5] "InferenceContinOLS"                  "InferenceContinQuantileRegr"

DesignSeqEveryOther <- R6Class(
  "DesignSeqEveryOther",
  inherit = DesignCustomSequential,
  lock_objects = FALSE,
  public = list(
    assignment_rule = function() as.numeric(self$get_t() %% 2 == 0)
  )
)
des_seq <- DesignSeqEveryOther$new(n = 6, response_type = "continuous", verbose = FALSE)
for (i in 1:6) des_seq$add_one_subject_to_experiment_and_assign(data.frame(x = i))
des_seq$get_w()
#> [1] 0 1 0 1 0 1

What the shells deliberately do not cover

The current shell set — DesignFixedCustom, DesignCustomSequential, InferenceCustomAsymp, InferenceCustomRand, InferenceCustomBoot — is sufficient for the extension contract above. There is no exact-test or parametric-bootstrap shell: the ExactTest component dispatches through private exact-test implementations, and the ParametricLikelihoodBootstrap component requires likelihood-null simulation/refit hooks; neither is a simple fit() shell, so exposing them would need a separate API design. Likewise there are no response-family-specific shells — the generic analysis-data accessors are the intended surface.

Contributing a class to EDI itself

Adding a class inside the package is a different contract: the class must go through define_inference_class() / define_design_class() with exact component, capability, and registry metadata, meet the package documentation standard, and be registered with the test harnesses, C++ kernels, Python bindings, and benchmarks. That process lives in the repository, in R/package_metadata/contracts/new_model_creation.md, which builds on the architecture summarized in this vignette rather than repeating it.

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.