Author: Massimo Aria, Agostino Gnasso
Date: 2026-05-15

# Introduction to e2tree: Explainable Ensemble Trees

#### Massimo Aria, Agostino Gnasso

#### 2026-05-15

## Overview

The e2tree package implements the Explainable Ensemble Tree (E2Tree) methodology, which constructs a single interpretable decision tree that approximates the predictions of an ensemble model (such as a random forest). The key idea is to use the dissimilarity structure induced by the ensemble to guide the construction of the tree, thereby preserving the predictive information encoded in the ensemble while providing a transparent, human-readable model.

The methodology is described in:

- Aria, M., Gnasso, A., Iorio, C., & Pandolfo, G. (2024). Explainable ensemble trees. Computational Statistics, 39(1), 3–19.
- Aria, M., Gnasso, A., Iorio, C., & Fokkema, M. (2026). Extending Explainable Ensemble Trees to Regression Contexts. Applied Stochastic Models in Business and Industry, 42(1), e70064.

## Installation

    # Install from CRAN (when available):
    install.packages("e2tree")

    # Or install the development version from GitHub:
    # devtools::install_github("massimoaria/e2tree")

## Workflow

A typical e2tree analysis follows these steps:

1. Train an ensemble model (random forest via `randomForest` or `ranger`)
2. Compute the dissimilarity matrix from the ensemble using `createDisMatrix()`
3. Build the E2Tree using `e2tree()`
4. Inspect and visualize the tree (print, summary, plot)
5. Predict on new data with `predict()`
6. Validate the E2Tree structure with `eValidation()` and `loi()`

## Classification Example

We use the classic `iris` dataset to demonstrate a classification workflow.

### Step 1: Prepare data and train ensemble

    library(e2tree)
    #> Explainable Ensemble Trees (E2Tree)
    #>
    #> If you use e2tree in research, please cite:
    #>
    #> - Aria, M., Gnasso, A., Iorio, C., & Pandolfo, G. (2024). Explainable ensemble trees.
    #> Computational Statistics, 39(1), 3-19. DOI: 10.1007/s00180-022-01312-6
    #>
    #> - Aria, M., Gnasso, A., Iorio, C., & Fokkema, M. (2026). Extending Explainable Ensemble Trees to Regression Contexts. Applied Stochastic Models in Business and Industry, 42(1), e70064. DOI: 10.1002/asmb.70064

    data(iris)
    set.seed(42)
    smp_size <- floor(0.75 * nrow(iris))
    train_ind <- sample(seq_len(nrow(iris)), size = smp_size)
    training <- iris[train_ind, ]
    validation <- iris[-train_ind, ]

    if (!require("randomForest")) install.packages("randomForest",
                                                   repos="https://cran.r-project.org")
    #> Loading required package: randomForest
    #> randomForest 4.7-1.2
    #> Type rfNews() to see new features/changes/bug fixes.

    library(randomForest)

    ensemble <- randomForest(Species ~ ., data = training,
                             importance = TRUE, proximity = TRUE)
    ensemble
    #>
    #> Call:
    #>  randomForest(formula = Species ~ ., data = training, importance = TRUE,      proximity = TRUE)
    #>                Type of random forest: classification
    #>                      Number of trees: 500
    #> No. of variables tried at each split: 2
    #>
    #>         OOB estimate of  error rate: 5.36%
    #> Confusion matrix:
    #>            setosa versicolor virginica class.error
    #> setosa         39          0         0  0.00000000
    #> versicolor      0         36         2  0.05263158
    #> virginica       0          4        31  0.11428571

### Step 2: Compute dissimilarity matrix

The ensemble induces a co-occurrence structure that captures how frequently pairs of observations fall into the same terminal nodes across the ensemble’s trees. This structure is encoded in the co-occurrence matrix \(\mathbf{O} = [o_{ij}]\), where each entry \(o_{ij} \in [0, 1]\) reflects the weighted frequency with which observations \(i\) and \(j\) are grouped together. The corresponding dissimilarity matrix is \(\mathbf{D} = \mathbf{1} - \mathbf{O}\).

    D <- createDisMatrix(ensemble, data = training, label = "Species",
                         parallel = list(active = FALSE, no_cores = 1))
    dim(D)
    #> [1] 112 112

### Step 3: Build the E2Tree

The `e2tree()` function constructs a single tree that best represents the ensemble’s dissimilarity structure. The `setting` argument controls the stopping rules:

- `impTotal`: minimum impurity to attempt a split
- `maxDec`: minimum decrease in impurity required
- `n`: minimum number of observations in a node
- `level`: maximum tree depth

    setting <- list(impTotal = 0.1, maxDec = 0.01, n = 2, level = 5)
    tree <- e2tree(Species ~ ., training, D, ensemble, setting)

### Step 4: Inspect the tree

    print(tree)
    #>
    #>   Explainable Ensemble Tree (E2Tree)
    #>   -----------------------------------
    #>   Task:            Classification
    #>   Response:        Species
    #>   Predictors:      4 (Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)
    #>   Observations:    112
    #>   Nodes:           11 (total), 6 (terminal)
    #>   Max depth:       5
    #>   Split variables: Petal.Length, Petal.Width, Sepal.Length
    #>   Classes:         setosa, versicolor, virginica

    summary(tree)
    #>
    #> ======================================================================
    #>                     E2TREE MODEL SUMMARY
    #> ======================================================================
    #>
    #> MODEL INFORMATION
    #> ----------------------------------------
    #>   Task:              Classification
    #>   Response:          Species
    #>   Observations:      112
    #>   Total Nodes:       11
    #>   Terminal Nodes:    6
    #>   Max Depth:         5
    #>   Split Variables:   Petal.Length, Petal.Width, Sepal.Length
    #>   Classes:           setosa, versicolor, virginica
    #>
    #>
    #> TERMINAL NODES
    #> ----------------------------------------------------------------------
    #>   Node      Prediction            n  Purity      Wt
    #> -------------------------------------------------------
    #>   2         setosa               39  100.0%      --
    #>   12        versicolor           35   97.1%      --
    #>   26        versicolor            2  100.0%      --
    #>   54        versicolor            2   50.0%      --
    #>   55        virginica             2  100.0%      --
    #>   7         virginica            32   96.9%      --
    #>
    #>
    #> DECISION RULES
    #> ----------------------------------------------------------------------
    #>
    #> Rule 1 (Node 2, n=39):
    #>     IF   Petal.Length <=1.9
    #>   THEN: setosa
    #>
    #> Rule 2 (Node 12, n=35):
    #>     IF   Petal.Length >1.9
    #>     AND  Petal.Width <=1.7
    #>     AND  Petal.Length <=4.8
    #>   THEN: versicolor
    #>
    #> Rule 3 (Node 26, n=2):
    #>     IF   Petal.Length >1.9
    #>     AND  Petal.Width <=1.7
    #>     AND  Petal.Length >4.8
    #>     AND  Petal.Length <=4.9
    #>   THEN: versicolor
    #>
    #> Rule 4 (Node 54, n=2):
    #>     IF   Petal.Length >1.9
    #>     AND  Petal.Width <=1.7
    #>     AND  Petal.Length >4.8
    #>     AND  Petal.Length >4.9
    #>     AND  Sepal.Length <=6
    #>   THEN: versicolor
    #>
    #> Rule 5 (Node 55, n=2):
    #>     IF   Petal.Length >1.9
    #>     AND  Petal.Width <=1.7
    #>     AND  Petal.Length >4.8
    #>     AND  Petal.Length >4.9
    #>     AND  Sepal.Length >6
    #>   THEN: virginica
    #>
    #> Rule 6 (Node 7, n=32):
    #>     IF   Petal.Length >1.9
    #>     AND  Petal.Width >1.7
    #>   THEN: virginica
    #>
    #> ======================================================================

The `nodes()` accessor extracts the tree structure as a data frame:

    # Terminal nodes only
    nodes(tree, terminal = TRUE)
    #>    node  n       pred      prob   impTotal impChildren decImp decImpSur
    #> 2     2 39     setosa 1.0000000 0.02581558          NA     NA        NA
    #> 12   12 35 versicolor 0.9714286 0.23840014          NA     NA        NA
    #> 26   26  2 versicolor 1.0000000 0.30526614          NA     NA        NA
    #> 54   54  2 versicolor 0.5000000 0.60063498          NA     NA        NA
    #> 55   55  2  virginica 1.0000000 0.53804112          NA     NA        NA
    #> 7     7 32  virginica 0.9687500 0.27410535          NA     NA        NA
    #>    variable split splitLabel variableSur splitLabelSur parent children terminal
    #> 2      <NA>    NA       <NA>        <NA>          <NA>      1       NA     TRUE
    #> 12     <NA>    NA       <NA>        <NA>          <NA>      6       NA     TRUE
    #> 26     <NA>    NA       <NA>        <NA>          <NA>     13       NA     TRUE
    #> 54     <NA>    NA       <NA>        <NA>          <NA>     27       NA     TRUE
    #> 55     <NA>    NA       <NA>        <NA>          <NA>     27       NA     TRUE
    #> 7      <NA>    NA       <NA>        <NA>          <NA>      3       NA     TRUE
    #>                                                                                                                                                          obs
    #> 2  1, 8, 9, 14, 18, 20, 22, 24, 27, 30, 32, 33, 34, 35, 37, 38, 41, 45, 47, 52, 53, 55, 57, 60, 61, 63, 66, 68, 69, 70, 82, 85, 88, 89, 95, 98, 99, 101, 111
    #> 12             2, 3, 11, 12, 25, 28, 29, 39, 40, 44, 46, 48, 49, 50, 58, 67, 71, 72, 75, 76, 77, 78, 79, 81, 84, 86, 87, 90, 91, 97, 103, 104, 108, 110, 112
    #> 26                                                                                                                                                    65, 83
    #> 54                                                                                                                                                    23, 73
    #> 55                                                                                                                                                    42, 92
    #> 7                           4, 5, 6, 7, 10, 13, 15, 16, 17, 19, 21, 26, 31, 36, 43, 51, 54, 56, 59, 62, 64, 74, 80, 93, 94, 96, 100, 102, 105, 106, 107, 109
    #>                                                                                                       path
    #> 2                                                                                       Petal.Length <=1.9
    #> 12                                            !Petal.Length <=1.9 & Petal.Width <=1.7 & Petal.Length <=4.8
    #> 26                      !Petal.Length <=1.9 & Petal.Width <=1.7 & !Petal.Length <=4.8 & Petal.Length <=4.9
    #> 54  !Petal.Length <=1.9 & Petal.Width <=1.7 & !Petal.Length <=4.8 & !Petal.Length <=4.9 & Sepal.Length <=6
    #> 55 !Petal.Length <=1.9 & Petal.Width <=1.7 & !Petal.Length <=4.8 & !Petal.Length <=4.9 & !Sepal.Length <=6
    #> 7                                                                 !Petal.Length <=1.9 & !Petal.Width <=1.7
    #>    ncat pred_val    yval2.V1    yval2.V2    yval2.V3    yval2.V4    yval2.V5
    #> 2    NA        1  1.00000000 39.00000000  0.00000000  0.00000000  1.00000000
    #> 12   NA        2  2.00000000  0.00000000 34.00000000  1.00000000  0.00000000
    #> 26   NA        2  2.00000000  0.00000000  2.00000000  0.00000000  0.00000000
    #> 54   NA        2  2.00000000  0.00000000  1.00000000  1.00000000  0.00000000
    #> 55   NA        3  3.00000000  0.00000000  0.00000000  2.00000000  0.00000000
    #> 7    NA        3  3.00000000  0.00000000  1.00000000 31.00000000  0.00000000
    #>       yval2.V6    yval2.V7 yval2.nodeprob
    #> 2   0.00000000  0.00000000     0.34821429
    #> 12  0.97142857  0.02857143     0.31250000
    #> 26  1.00000000  0.00000000     0.01785714
    #> 54  0.50000000  0.50000000     0.01785714
    #> 55  0.00000000  1.00000000     0.01785714
    #> 7   0.03125000  0.96875000     0.28571429

### Step 5: Visualization

The `plot()` method renders the tree using `rpart.plot`:

    plot(tree, ensemble = ensemble, main = "E2Tree - Iris Classification")

You can also convert the tree to `rpart` or `partykit` formats for alternative visualizations:

    # Convert to rpart
    rpart_obj <- as.rpart(tree, ensemble)

    # Convert to partykit (if installed)
    if (requireNamespace("partykit", quietly = TRUE)) {
      party_obj <- partykit::as.party(tree)
      plot(party_obj)
    }

### Step 6: Prediction

Use the standard `predict()` method on new data:

    pred <- predict(tree, newdata = validation)
    head(pred)
    #>      fit accuracy score
    #> 1 setosa        1     0
    #> 2 setosa        1     0
    #> 3 setosa        1     0
    #> 4 setosa        1     0
    #> 5 setosa        1     0
    #> 6 setosa        1     0

Fitted values for the training data:

    head(fitted(tree))
    #> [1] "setosa"     "versicolor" "versicolor" "virginica"  "virginica"
    #> [6] "virginica"

### Step 7: Variable importance

    vi <- vimp(tree, data = training)
    vi$vimp
    #> # A tibble: 3 × 9
    #>   Variable     MeanImpurityDecrease MeanAccuracyDecrease `ImpDec_ setosa`
    #>   <chr>                       <dbl>                <dbl>            <dbl>
    #> 1 Petal.Length              0.339               8.93e- 3            0.302
    #> 2 Petal.Width               0.208              -7.24e-17           NA
    #> 3 Sepal.Length              0.00280             0                  NA
    #> # ℹ 5 more variables: `ImpDec_ versicolor` <dbl>, `ImpDec_ virginica` <dbl>,
    #> #   `AccDec_ setosa` <dbl>, `AccDec_ versicolor` <dbl>,
    #> #   `AccDec_ virginica` <dbl>
    vi$g_imp

## Regression Example

The same workflow applies to regression problems. Here we use the `mtcars` dataset.

    data(mtcars)
    set.seed(123)
    smp_size <- floor(0.75 * nrow(mtcars))
    train_ind <- sample(seq_len(nrow(mtcars)), size = smp_size)
    training_reg <- mtcars[train_ind, ]
    validation_reg <- mtcars[-train_ind, ]

    ensemble_reg <- randomForest(mpg ~ ., data = training_reg, ntree = 500,
                                 importance = TRUE, proximity = TRUE)

    D_reg <- createDisMatrix(ensemble_reg, data = training_reg, label = "mpg",
                             parallel = list(active = FALSE, no_cores = 1))

    setting_reg <- list(impTotal = 0.1, maxDec = 1e-6, n = 2, level = 5)
    tree_reg <- e2tree(mpg ~ ., training_reg, D_reg, ensemble_reg, setting_reg)
    print(tree_reg)
    #>
    #>   Explainable Ensemble Tree (E2Tree)
    #>   -----------------------------------
    #>   Task:            Regression
    #>   Response:        mpg
    #>   Predictors:      10 (cyl, disp, hp, drat, wt)
    #>                    ... and 5 more
    #>   Observations:    24
    #>   Nodes:           7 (total), 4 (terminal)
    #>   Max depth:       3
    #>   Split variables: cyl, wt

    plot(tree_reg, ensemble = ensemble_reg, main = "E2Tree - MPG Regression")

For regression models, predictions include the standard deviation within each terminal node, providing a measure of prediction uncertainty:

    pred_reg <- predict(tree_reg, newdata = validation_reg)
    head(pred_reg)
    #>        fit       sd
    #> 1 19.82000 1.446375
    #> 2 19.82000 1.446375
    #> 3 14.97778 2.132943
    #> 4 14.97778 2.132943
    #> 5 14.97778 2.132943
    #> 6 22.85000 1.226105

Residuals are also available:

    res <- residuals(tree_reg)
    summary(res)
    #>    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.
    #> -4.5778 -0.6344  0.1222  0.0000  0.9117  3.8333

## Validation of the E2Tree Structure

A fundamental question arises when using E2Tree: how well does the single interpretable tree capture the original ensemble’s structure? The E2Tree reconstructs the ensemble’s co-occurrence matrix \(\mathbf{O}\) through its terminal node structure, producing an approximation \(\hat{\mathbf{O}}_T\). Assessing the quality of this reconstruction is essential to ensure that the explanatory model faithfully represents the ensemble’s decision logic.

The `eValidation()` function provides two complementary approaches to validation, selectable through the `test` argument:

- `test = "mantel"`: The Mantel test, a classical non-parametric test of association between similarity matrices.
- `test = "measures"`: A family of divergence and similarity measures that assess agreement between the matrices, with permutation-based inference.
- `test = "both"` (default): Performs both.

### Association vs. Agreement

The distinction between these two approaches mirrors the classical distinction between correlation and concordance in method comparison studies (Bland & Altman, 1986; Lin, 1989). Two proximity matrices can be perfectly correlated (\(r = 1\)) and yet systematically disagree: if \(\hat{\mathbf{O}}_T = c \cdot \mathbf{O}\) for any constant \(c \neq 1\), the Mantel test would declare perfect association, yet the actual proximity values would not be faithfully reproduced.

This distinction matters in the E2Tree context. The ensemble matrix \(\mathbf{O}\) contains continuous values in \((0, 1)\), reflecting varying degrees of co-occurrence across the ensemble’s trees. The E2Tree matrix \(\hat{\mathbf{O}}_T\), by contrast, has a crisp block-diagonal structure: observations within the same terminal node share a common co-occurrence value, while observations in different nodes have zero co-occurrence. Even when the block structure perfectly matches the grouping pattern of \(\mathbf{O}\), the element-wise values will differ because \(\mathbf{O}\) is fuzzy and \(\hat{\mathbf{O}}_T\) is crisp. A scale-sensitive divergence measure can quantify this discrepancy; a correlation-based measure cannot.

### The Mantel Test

The Mantel test evaluates whether the E2Tree proximity matrix is associated with the ensemble proximity matrix. It computes a correlation-like statistic between the two matrices and assesses significance through permutation.

    val_mantel <- eValidation(training, tree, D, test = "mantel", graph = FALSE)
    print(val_mantel)
    #>
    #> ##############################################################################
    #>    E2Tree Validation
    #> ##############################################################################
    #>
    #>   Matrix dimension:    112 x 112
    #>   Pairs:               6216
    #>
    #>   Mantel test:         z = 1512.22, p = 0.0010
    #>
    #> ##############################################################################

A significant Mantel test confirms that E2Tree preserves the overall pattern of the ensemble’s proximity structure. However, it does not tell us how closely the actual proximity values are reproduced, nor does it indicate where the reconstruction fails.

### Divergence and Similarity Measures

To assess agreement rather than mere association, we use a family of five measures, each capturing a distinct aspect of reconstruction quality:

Measure | Type | Range | What it captures
--- | --- | --- | ---
nLoI (normalized Loss of Interpretability) | divergence | [0, 1] | Scale-sensitive per-pair discrepancy with diagnostic decomposition
Hellinger distance | divergence | [0, 1] | Variance-stabilized element-wise difference (a true metric)
wRMSE (weighted Root Mean Squared Error) | divergence | [0, 1] | Weighted average discrepancy emphasizing high-proximity regions
RV coefficient | similarity | [0, 1] | Global structural similarity (matrix-level correlation)
SSIM (Structural Similarity Index) | similarity | [-1, 1] | Local spatial pattern similarity (luminance, contrast, structure)

For divergence measures, lower values indicate better reconstruction. For similarity measures, higher values indicate better reconstruction.

Statistical significance is assessed through a unified permutation testing framework based on simultaneous row/column permutation of \(\hat{\mathbf{O}}_T\). This preserves the block-diagonal structure of the E2Tree matrix while breaking the correspondence between observation labels and node assignments, providing the appropriate null hypothesis: under \(H_0\), E2Tree groups observations randomly with respect to the ensemble structure.

    val_meas <- eValidation(training, tree, D, test = "measures",
                            graph = FALSE, n_perm = 499)
    print(val_meas)
    #>
    #> ##############################################################################
    #>    E2Tree Validation
    #> ##############################################################################
    #>
    #>   Matrix dimension:    112 x 112
    #>   Pairs:               6216
    #>
    #> ------------------------------------------------------------------------------
    #>   Measure          Type        Observed    Null mean    Z-stat     p-value
    #> ------------------------------------------------------------------------------
    #>   nLoI           [div]     0.0468     0.4060     -74.99   0.0020 **
    #>   Hellinger      [div]     0.2063     0.6243    -106.18   0.0020 **
    #>   wRMSE          [div]     0.2002     0.8320    -184.50   0.0020 **
    #>   RV             [sim]     0.9624     0.3273     +76.04   0.0020 **
    #>   SSIM           [sim]     0.6718     0.0076    +148.36   0.0020 **
    #> ------------------------------------------------------------------------------
    #>   Permutations: 499 (row/column), conf.level: 95%
    #>
    #>   LoI Decomposition (per-pair avg):  mean_in = 0.020614,  mean_out = 0.057709
    #>
    #>   [div] = divergence (lower=better), [sim] = similarity (higher=better)
    #>   Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1
    #>
    #> ##############################################################################

All divergence measures are significantly lower than their null expectations (negative Z-statistics), while all similarity measures are significantly higher (positive Z-statistics), confirming that the E2Tree faithfully reconstructs the ensemble’s proximity structure.

    measures(val_meas)
    #>      method       type   observed   null_mean     null_sd     z_stat p_value
    #> 1      nLoI divergence 0.04675804 0.405974055 0.004789926  -74.99407   0.002
    #> 2 Hellinger divergence 0.20634450 0.624258879 0.003936005 -106.17731   0.002
    #> 3     wRMSE divergence 0.20024399 0.832018752 0.003424183 -184.50378   0.002
    #> 4        RV similarity 0.96244104 0.327255499 0.008352769   76.04491   0.002
    #> 5      SSIM similarity 0.67183834 0.007606083 0.004477240  148.35753   0.002
    #>     ci_lower   ci_upper
    #> 1 0.04044819 0.05828068
    #> 2 0.20092189 0.21610662
    #> 3 0.19557892 0.20859867
    #> 4 0.94198847 0.97359096
    #> 5 0.66025940 0.67907518

### Visualizing the Reconstruction

The `plot()` method for `eValidation` objects provides a visual summary including heatmaps of both proximity matrices, the null distribution of nLoI (when permutation tests are performed), and the LoI decomposition:

    val_full <- eValidation(training, tree, D, test = "both",
                            graph = FALSE, n_perm = 499)
    plot(val_full)

### The Normalized Loss of Interpretability (nLoI)

Among the divergence measures, the nLoI deserves special attention because of its unique decomposability into within-node and between-node components.

The nLoI is defined as: \[\text{nLoI}(\mathbf{O}, \hat{\mathbf{O}}_T) = \frac{1}{M} \sum_{i<j} \frac{(o_{ij} - \hat{o}_{ij})^2}{\max(o_{ij}, \hat{o}_{ij})}\] where \(M = n(n-1)/2\) is the number of unique pairs.

This statistic is connected to the Cressie-Read power divergence family at \(\lambda = -2\) (Neyman-type statistic), with a robustified denominator that avoids singularities when \(\hat{o}_{ij} = 0\) (which is common in E2Tree due to the block-diagonal structure of \(\hat{\mathbf{O}}_T\)).

The `loi()` function computes the nLoI and its decomposition directly from two proximity matrices:

    prox <- proximity(val_full)
    result <- loi(prox$ensemble, prox$e2tree)
    summary(result)
    #>
    #> ##############################################################################
    #>    Loss of Interpretability (LoI) --Decomposition
    #> ##############################################################################
    #>
    #>   nLoI (normalized):   0.0468
    #>   LoI (raw):           290.6480
    #>   n = 112, pairs = 6216 (within: 1835, between: 4381)
    #>
    #> ------------------------------------------------------------------------------
    #>   Component              Total       Pairs     Mean/pair
    #> ------------------------------------------------------------------------------
    #>   Within-node (LoI_in)   37.8270       1835     0.020614
    #>   Between-node (LoI_out)252.8210       4381     0.057709
    #> ------------------------------------------------------------------------------
    #>
    #>   Per-pair interpretation (comparable across components):
    #>
    #>     mean_in  = 0.020614  (avg calibration error within nodes)
    #>     mean_out = 0.057709  (avg ensemble proximity lost by separation)
    #>
    #>   Diagnostic: LOW mean_out. The partition correctly separates pairs
    #>   that have low ensemble proximity --the tree structure is well-placed.
    #>
    #>   Diagnostic: MODERATE mean_in. Some within-node calibration error,
    #>   typical of the fuzzy-to-crisp structural transition.
    #>
    #> ##############################################################################

#### LoI Decomposition: Diagnosing Reconstruction Quality

The LoI decomposes into two interpretable components:

- Within-node component (\(\text{LoI}_{in}\)): Measures reconstruction fidelity for pairs that E2Tree groups together. It captures the calibration error — whether the single tree assigns co-occurrence values that match the ensemble’s.

- Between-node component (\(\text{LoI}_{out}\)): Measures the cost of the partition. It accumulates the ensemble co-occurrence values (\(o_{ij}\)) for pairs that E2Tree separates into different terminal nodes (where \(\hat{o}_{ij} = 0\)), representing irrecoverable loss.

Since between-node pairs vastly outnumber within-node pairs (especially with many terminal nodes), the raw totals are not directly comparable. The per-pair averages (`mean_in` and `mean_out`) provide the meaningful diagnostic:

- A high `mean_out` (e.g., > 0.3) indicates that E2Tree is separating pairs with substantial ensemble proximity. The practitioner should consider increasing the tree’s complexity (more terminal nodes) or relaxing pruning constraints.

- A high `mean_in` (e.g., > 0.1) indicates poor within-node calibration: the partition boundaries are well-placed, but the within-node proximity values deviate from the ensemble’s. This typically reflects the inherent fuzzy-to-crisp structural transition.

- Low values of both confirm that the E2Tree faithfully captures the ensemble structure — the partition is well-placed and the calibration is accurate.

This diagnostic capability is fundamentally impossible with the Mantel test or any other scalar association measure, which can only report that the overall reconstruction is “good” or “poor” without indicating the source of the discrepancy.

    plot(result)

#### Permutation Test for LoI

The `loi_perm()` function provides a standalone permutation test for the nLoI, useful when a quick assessment of reconstruction significance is needed without computing all five measures:

    perm <- loi_perm(prox$ensemble, prox$e2tree, n_perm = 499, seed = 42)
    print(perm)
    #>
    #> ==============================================================================
    #>    Permutation Test for Loss of Interpretability (LoI)
    #> ==============================================================================
    #>
    #>   Observed nLoI:       0.0468
    #>   Null mean:           0.4059
    #>   Null SD:             0.0048
    #>   Z-statistic:         -75.4053
    #>
    #> ------------------------------------------------------------------------------
    #>   Hypothesis Test (H1: nLoI < expected by chance)
    #> ------------------------------------------------------------------------------
    #>   p-value:             0.0020 **
    #>   95% CI:             [0.0402, 0.0590]
    #>   Permutations:        499 (row/column)
    #>
    #> ------------------------------------------------------------------------------
    #>   Decomposition (per-pair averages)
    #> ------------------------------------------------------------------------------
    #>   mean_in  (within):   0.020614  (n_pairs = 1835)
    #>   mean_out (between):  0.057709  (n_pairs = 4381)
    #>
    #> ==============================================================================
    #>   Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1
    plot(perm)

### Validation of the Regression Example

The same validation framework applies to regression E2Trees:

    val_reg <- eValidation(training_reg, tree_reg, D_reg, test = "measures",
                           graph = FALSE, n_perm = 499)
    print(val_reg)
    #>
    #> ##############################################################################
    #>    E2Tree Validation
    #> ##############################################################################
    #>
    #>   Matrix dimension:    24 x 24
    #>   Pairs:               276
    #>
    #> ------------------------------------------------------------------------------
    #>   Measure          Type        Observed    Null mean    Z-stat     p-value
    #> ------------------------------------------------------------------------------
    #>   nLoI           [div]     0.1412     0.3381     -13.54   0.0020 **
    #>   Hellinger      [div]     0.3357     0.5414     -14.84   0.0020 **
    #>   wRMSE          [div]     0.3956     0.6971     -21.39   0.0020 **
    #>   RV             [sim]     0.8533     0.3466     +13.81   0.0020 **
    #>   SSIM           [sim]     0.5259     0.1525      +9.93   0.0020 **
    #> ------------------------------------------------------------------------------
    #>   Permutations: 499 (row/column), conf.level: 95%
    #>
    #>   LoI Decomposition (per-pair avg):  mean_in = 0.178400,  mean_out = 0.129262
    #>
    #>   [div] = divergence (lower=better), [sim] = similarity (higher=better)
    #>   Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1
    #>
    #> ##############################################################################

Regression E2Trees typically show higher nLoI and Hellinger values than classification, consistent with the greater complexity of regression proximity structures and the typically smaller sample sizes. Nonetheless, all measures should achieve significant p-values, confirming that the reconstruction captures genuine structure.

## Comparison with Related Packages

The e2tree package complements existing R infrastructure for tree-based models:

- partykit: Provides generic classes for representing trees. E2Tree objects can be converted to `party` objects via `as.party()`, enabling the use of partykit’s rich visualization and prediction infrastructure.
- stablelearner: Explores the stability of ensemble trees via `stabletree()`. While stablelearner focuses on variable and cutpoint stability, e2tree constructs a single tree that captures the ensemble’s dissimilarity-based structure — a complementary perspective.
- rpart: The standard recursive partitioning package. E2Tree objects can be converted to `rpart` objects via `as.rpart()` for visualization with `rpart.plot`.

## Session Info

    sessionInfo()
    #> R version 4.6.0 (2026-04-24)
    #> Platform: aarch64-apple-darwin23
    #> Running under: macOS Tahoe 26.5
    #>
    #> Matrix products: default
    #> BLAS:   /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
    #> LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
    #>
    #> locale:
    #> [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
    #>
    #> time zone: Europe/Rome
    #> tzcode source: internal
    #>
    #> attached base packages:
    #> [1] stats     graphics  grDevices utils     datasets  methods   base
    #>
    #> other attached packages:
    #> [1] future_1.70.0        randomForest_4.7-1.2 e2tree_1.2.0
    #>
    #> loaded via a namespace (and not attached):
    #>  [1] gmp_0.7-5.1         utf8_1.2.6          tidyr_1.3.2
    #>  [4] sass_0.4.10         generics_0.1.4      lattice_0.22-9
    #>  [7] inum_1.0-5          listenv_0.10.1      digest_0.6.39
    #> [10] magrittr_2.0.5      evaluate_1.0.5      grid_4.6.0
    #> [13] RColorBrewer_1.1-3  mvtnorm_1.3-7       fastmap_1.2.0
    #> [16] jsonlite_2.0.0      Matrix_1.7-5        ape_5.8-1
    #> [19] Formula_1.2-5       survival_3.8-6      rpart.plot_3.1.4
    #> [22] purrr_1.2.2         scales_1.4.0        codetools_0.2-20
    #> [25] jquerylib_0.1.4     Rdpack_2.6.6        cli_3.6.6
    #> [28] rlang_1.2.0         rbibutils_2.4.1     parallelly_1.47.0
    #> [31] future.apply_1.20.2 splines_4.6.0       withr_3.0.2
    #> [34] cachem_1.1.0        yaml_2.3.12         otel_0.2.0
    #> [37] tools_4.6.0         parallel_4.6.0      polynom_1.4-1
    #> [40] dplyr_1.2.1         ggplot2_4.0.3       partitions_1.10-9
    #> [43] globals_0.19.1      vctrs_0.7.3         R6_2.6.1
    #> [46] rpart_4.1.27        lifecycle_1.0.5     libcoin_1.0-12
    #> [49] partykit_1.2-27     pkgconfig_2.0.3     pillar_1.11.1
    #> [52] bslib_0.10.0        gtable_0.3.6        glue_1.8.1
    #> [55] Rcpp_1.1.1-1.1      xfun_0.57           tibble_3.3.1
    #> [58] tidyselect_1.2.1    rstudioapi_0.18.0   knitr_1.51
    #> [61] farver_2.1.2        nlme_3.1-169        htmltools_0.5.9
    #> [64] labeling_0.4.3      rmarkdown_2.31      compiler_4.6.0
    #> [67] S7_0.2.2