---
title: "Introduction to boostPM"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Introduction to boostPM}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  comment = "#>",
  fig.width = 7,
  fig.height = 4.8,
  fig.align = "center",
  warning = FALSE,
  message = FALSE
)

density_palette <- grDevices::hcl.colors(32, "YlOrRd", rev = TRUE)

draw_density <- function(x1, x2, values, title, display_cap) {
  surface <- matrix(values, nrow = length(x1), ncol = length(x2))
  surface <- pmin(surface, display_cap)
  image(
    x1,
    x2,
    surface,
    zlim = c(0, display_cap),
    col = density_palette,
    xlim = c(0, 1),
    ylim = c(0, 1),
    asp = 1,
    xlab = "x1",
    ylab = "x2",
    main = title
  )
  contour(
    x1,
    x2,
    surface,
    add = TRUE,
    drawlabels = FALSE,
    col = "grey30",
    nlevels = 7
  )
}

draw_points <- function(x, title, color = "black", cex = 0.35) {
  plot(
    x,
    xlim = c(0, 1),
    ylim = c(0, 1),
    xaxs = "i",
    yaxs = "i",
    xaxt = "n",
    yaxt = "n",
    asp = 1,
    pch = 16,
    cex = cex,
    col = grDevices::adjustcolor(color, alpha.f = 0.35),
    xlab = "x1",
    ylab = "x2",
    main = title
  )
  axis(1, at = seq(0, 1, by = 0.2))
  axis(2, at = seq(0, 1, by = 0.2))
}
```

The `boostPM` package estimates probability distributions with an ensemble
of tree-based probability measures. The method is useful when a distribution
contains nonlinear dependence, multimodality, or other structure that is
difficult to describe with a single parametric family.

This vignette considers two distributions on the unit square:

1. a product of two beta distributions;
2. a sinusoidal conditional-density ridge.

For each example, we fit an ensemble, evaluate its density on a grid, and draw
new observations from the fitted distribution.

## Simulated training data

Observations occupy rows and variables occupy columns. Both examples use 2,400
training observations and remain practical for routine package checks.

### Scenario 1: independent beta variables

The first coordinate follows a beta distribution concentrated toward the lower
boundary, while the second is concentrated toward the upper boundary. The
coordinates are independent, giving a simple parametric reference case with an
analytic joint density.

```{r beta-data}
library(boostPM)

n_beta <- 2400L
n_sinusoidal <- 2400L
support <- cbind(c(0, 0), c(1, 1))

set.seed(11001)
beta_data <- cbind(
  x1 = rbeta(n_beta, shape1 = 2, shape2 = 5),
  x2 = rbeta(n_beta, shape1 = 5, shape2 = 2)
)

beta_density <- function(x1, x2) {
  dbeta(x1, shape1 = 2, shape2 = 5) *
    dbeta(x2, shape1 = 5, shape2 = 2)
}
```

### Scenario 2: sinusoidal ridge

The first coordinate is uniform. The conditional mean of the second coordinate
follows a sine wave. This produces nonlinear dependence that is not well
represented by an elliptical distribution.

```{r sinusoidal-data}
set.seed(12001)
x1 <- runif(n_sinusoidal)
mu <- 0.15 + 0.70 * (0.5 + 0.5 * sin(2 * pi * x1))
concentration <- 40
x2 <- rbeta(
  n_sinusoidal,
  mu * concentration,
  (1 - mu) * concentration
)
sinusoidal_data <- cbind(x1 = x1, x2 = x2)

sinusoidal_density <- function(x1, x2) {
  mu <- 0.15 + 0.70 * (0.5 + 0.5 * sin(2 * pi * x1))
  dbeta(x2, mu * concentration, (1 - mu) * concentration)
}
```

```{r training-data-plot, fig.width=7, fig.height=3.6}
old_par <- par(mfrow = c(1, 2), mar = c(4, 4, 3, 1))
draw_points(beta_data, "Independent beta variables")
draw_points(sinusoidal_data, "Sinusoidal ridge")
par(old_par)
```

## Fit the ensembles

The tree-number, learning-rate, and stopping controls follow the
accuracy-oriented experimental configuration. The marginal stage is capped at
100 trees per dimension, and the dependence stage is capped at 5,000 trees. A
learning rate of 0.1 is combined with adaptive stopping based on the most recent
50 validation improvements. The split prior of 0.9 follows the archived
implementation and public experiment code.

```{r fit-beta}
set.seed(31001)
fit_beta <- fit_boostpm(
  beta_data,
  Omega = support,
  max_marginal_trees = 100,
  max_dependence_trees = 5000,
  n_bins = 100,
  max_split_depth = 15,
  min_node_observations = 10,
  c0 = 0.1,
  gamma = 0.5,
  early_stop = c(0, 50),
  prior_split_prob = 0.9,
  add_noise = FALSE
)
```

```{r fit-sinusoidal}
set.seed(32001)
fit_sinusoidal <- fit_boostpm(
  sinusoidal_data,
  Omega = support,
  max_marginal_trees = 100,
  max_dependence_trees = 5000,
  n_bins = 100,
  max_split_depth = 15,
  min_node_observations = 10,
  c0 = 0.1,
  gamma = 0.5,
  early_stop = c(0, 50),
  prior_split_prob = 0.9,
  add_noise = FALSE
)
```

## Inspect fitted objects

`fit_boostpm()` is silent by default. Set `progress = "stage"` when stage-level
messages are useful. The fitted object supports standard S3 methods.

```{r inspect}
print(fit_beta)
summary(fit_sinusoidal)
```

Variable-importance, number-of-nodes, and maximum-depth plots are available
through `plot()`.

```{r variable-importance, fig.width=6.5, fig.height=4.5}
plot(fit_sinusoidal, type = "variable_importance")
```

## Evaluate the fitted densities

`predict()` returns log density by default. Here both fitted densities are
evaluated on an 80 by 80 midpoint grid.

```{r density-evaluation}
grid_size <- 80L
x1_grid <- (seq_len(grid_size) - 0.5) / grid_size
x2_grid <- (seq_len(grid_size) - 0.5) / grid_size
evaluation_grid <- as.matrix(expand.grid(
  x1 = x1_grid,
  x2 = x2_grid
))

fitted_beta <- predict(
  fit_beta,
  evaluation_grid,
  type = "density"
)
fitted_sinusoidal <- predict(
  fit_sinusoidal,
  evaluation_grid,
  type = "density"
)

true_beta <- beta_density(
  evaluation_grid[, 1],
  evaluation_grid[, 2]
)
true_sinusoidal <- sinusoidal_density(
  evaluation_grid[, 1],
  evaluation_grid[, 2]
)
```

The true and fitted panels share a color scale within each scenario. Values
above the scenario-specific 99th percentile are clipped for display only.

```{r density-comparison, fig.width=9, fig.height=7}
beta_cap <- as.numeric(stats::quantile(
  c(true_beta, fitted_beta),
  0.99,
  names = FALSE
))
sinusoidal_cap <- as.numeric(stats::quantile(
  c(true_sinusoidal, fitted_sinusoidal),
  0.99,
  names = FALSE
))

old_par <- par(
  mfrow = c(2, 3),
  mar = c(4.5, 4.2, 2.8, 0.8),
  mgp = c(2.6, 0.8, 0)
)
draw_points(beta_data, "Training: beta")
draw_density(
  x1_grid,
  x2_grid,
  true_beta,
  "True density",
  beta_cap
)
draw_density(
  x1_grid,
  x2_grid,
  fitted_beta,
  "Fitted density",
  beta_cap
)
draw_points(sinusoidal_data, "Training: sinusoidal")
draw_density(
  x1_grid,
  x2_grid,
  true_sinusoidal,
  "True density",
  sinusoidal_cap
)
draw_density(
  x1_grid,
  x2_grid,
  fitted_sinusoidal,
  "Fitted density",
  sinusoidal_cap
)
par(old_par)
```

The fitted densities visually capture the main features of both the
beta-product density and the nonlinear sinusoidal ridge.

Points outside the support have log density `-Inf` and density zero.

```{r outside-support}
outside <- rbind(c(-0.10, 0.50), c(0.50, 0.50), c(1.10, 0.50))
predict(fit_sinusoidal, outside, type = "log_density")
```

## Generate and compare samples

The fitted ensemble is also a generative model. The following calls draw
1,000 observations from each fit.

```{r simulate}
set.seed(41001)
generated_beta <- simulate(fit_beta, nsim = 1000)

set.seed(42001)
generated_sinusoidal <- simulate(fit_sinusoidal, nsim = 1000)
```

```{r generated-joint-plot, fig.width=8, fig.height=7.2}
old_par <- par(mfrow = c(2, 2), mar = c(4, 4, 3, 1))
draw_points(beta_data, "Training: beta")
draw_points(
  generated_beta,
  "Generated: beta",
  color = "#2166AC",
  cex = 0.35
)
draw_points(sinusoidal_data, "Training: sinusoidal")
draw_points(
  generated_sinusoidal,
  "Generated: sinusoidal",
  color = "#2166AC",
  cex = 0.35
)
par(old_par)
```

The generated observations reproduce the main visible features of the beta
marginal shapes and nonlinear sinusoidal structure in the training samples.
Marginal empirical distribution functions provide a second comparison.

```{r marginal-comparison, fig.width=8, fig.height=7.2}
data_pairs <- list(
  list("Independent beta", beta_data, generated_beta),
  list("Sinusoidal", sinusoidal_data, generated_sinusoidal)
)

old_par <- par(mfrow = c(2, 2), mar = c(4, 4, 3, 1))
for (data_pair in data_pairs) {
  for (j in seq_len(2L)) {
    plot(
      stats::ecdf(data_pair[[2]][, j]),
      do.points = FALSE,
      verticals = TRUE,
      xlim = support[j, ],
      lwd = 2,
      col = "black",
      xlab = "",
      ylab = "Empirical cumulative probability",
      main = paste(data_pair[[1]], paste0("x", j))
    )
    mtext(colnames(data_pair[[2]])[j], side = 1, line = 2.5)
    lines(
      stats::ecdf(data_pair[[3]][, j]),
      verticals = TRUE,
      lwd = 2,
      col = "#2166AC"
    )
    legend(
      "bottomright",
      legend = c("Training", "Generated"),
      col = c("black", "#2166AC"),
      lwd = 2,
      bty = "n"
    )
  }
}
par(old_par)
```

## Practical notes

- Use a finite numeric matrix. Constant columns are not supported.
- `Omega` must have two columns and positive width in each dimension.
- `n_bins` controls the uniform candidate split grid and must be at least two.
- `c0` is strictly between zero and one; `gamma` is non-negative.
- More trees and deeper weak learners increase flexibility and computation.

## References

Awaya, N. and Ma, L. (2024). Unsupervised Tree Boosting for Learning
Probability Distributions. *Journal of Machine Learning Research*, 25, 1--52.
