In this tutorial, we show how to use the ocf package to estimate and make inference about the conditional choice probabilities and the covariates’ marginal effects.

Before diving in the coding, we provide an overview of the statistical problem at hand.

Ordered Choice Models

We postulate the existence of a latent and continuous outcome variable \(Y_i^*\), assumed to obey the following regression model:

\[ Y_i^* = g \left( X_i \right) + \epsilon_i \] where \(X_i\) consists of a set of raw covariates, \(g \left( \cdot \right)\) is a potentially non-linear regression function, and \(\epsilon_i\) is independent of \(X_i\).

Then, an observational rule links the observed outcome \(Y_i\) to the latent outcome \(Y_{i}^*\) using unknown threshold parameters \(- \infty = \zeta_0 < \zeta_1 < \dots < \zeta_{M - 1} < \zeta_M = \infty\) that define intervals on the support of \(Y_i^*\), with each interval corresponding to one of the \(M\) categories or classes of \(Y_i\):

\[ \zeta_{m - 1} < Y_i^* \leq \zeta_{m} \implies Y_i = m, \quad m = 1, \dots, M \]

The statistical targets of interest are the conditional choice probabilities:

\[ p_m \left( X_i \right) := \mathbb{P} \left( Y_i = m | X_i \right) \]

and the marginal effect of the \(j\)-th covariate on \(p_m \left( \cdot \right)\):

\[ \nabla^j p_m \left( x \right) := \begin{cases} \frac{\partial p_m \left( x \right)}{\partial x_j}, & \text{if } x_j \text{ is continuous} \\ p_m \left( \lceil x_j \rceil \right) - p_m \left( \lfloor x_j \rfloor \right), & \text{if } x_j \text{ is discrete} \end{cases} \] where \(x_j\) is the \(j\)-th element of the vector \(x\) and \(\lceil x_j \rceil\) and \(\lfloor x_j \rfloor\) correspond to \(x\) with its \(j\)-th element rounded up and down to the closest integer.

Code

For illustration purposes, we generate a synthetic data set. Details about the employed DGP can be retrieved by running help(generate_ordered_data).

## Generate synthetic data.
set.seed(1986)

n <- 1000
data <- generate_ordered_data(n)

sample <- data$sample
Y <- sample$Y
X <- sample[, -1]

table(Y)
#> Y
#>   1   2   3 
#> 361 296 343

head(X)
#>            x1 x2          x3 x4         x5 x6
#> 1 -0.04625141  0  1.54395982  1  0.8871545  1
#> 2  0.28000082  0 -0.56514824  0 -0.2853152  0
#> 3  0.25317063  0  0.07254085  0  2.9951778  1
#> 4 -0.96411077  1  0.89457168  0  0.7660870  0
#> 5  0.49222664  1 -2.27801497  0  0.4388097  0
#> 6 -0.69874551  0 -0.03860101  1  0.7422111  0

Conditional Probabilities

To estimate the conditional probabilities, the ocf function constructs a collection of forests, one for each category of y (three in this case). We can then use the forests to predict out-of-sample using the predict method. predict returns a matrix with the predicted probabilities and a vector of predicted class labels (each observation is labelled to the highest-probability class).

## Training-test split.
train_idx <- sample(seq_len(length(Y)), floor(length(Y) * 0.5))

Y_tr <- Y[train_idx]
X_tr <- X[train_idx, ]

Y_test <- Y[-train_idx]
X_test <- X[-train_idx, ]

## Fit ocf on training sample. Use default settings.
forests <- ocf(Y_tr, X_tr)

## Summary of data and tuning parameters.
summary(forests)
#> Call: 
#> ocf(Y_tr, X_tr) 
#> 
#> Data info: 
#> Full sample size:   500 
#> N. covariates:      6 
#> Classes:            1 2 3 
#> 
#> Relative variable importance: 
#>    x1    x2    x3    x4    x5    x6 
#> 0.339 0.099 0.251 0.064 0.213 0.035 
#> 
#> Tuning parameters: 
#> N. trees:           2000 
#> mtry:               3 
#> min.node.size       5 
#> Subsampling scheme: No replacement 
#> Honesty:            FALSE 
#> Honest fraction:    0

## Out-of-sample predictions.
predictions <- predict(forests, X_test)

head(predictions$probabilities)
#>         P(Y=1)    P(Y=2)     P(Y=3)
#> [1,] 0.3215795 0.4732135 0.20520702
#> [2,] 0.3136190 0.4936867 0.19269430
#> [3,] 0.3624392 0.2213801 0.41618073
#> [4,] 0.3627696 0.4386894 0.19854103
#> [5,] 0.7408662 0.1769811 0.08215271
#> [6,] 0.3670253 0.4038256 0.22914902

table(Y_test, predictions$classification)
#>       
#> Y_test   1   2   3
#>      1 118  35  32
#>      2  48  40  61
#>      3  32  34 100

To produce consistent and asymptotically normal predictions, we need to set the honesty argument to TRUE. This makes the ocf function using different parts of the training sample to construct the forests and compute the predictions.

## Honest forests.
honest_forests <- ocf(Y_tr, X_tr, honesty = TRUE)

honest_predictions <- predict(honest_forests, X_test)
head(honest_predictions$probabilities)
#>         P(Y=1)    P(Y=2)    P(Y=3)
#> [1,] 0.4058651 0.3979374 0.1961975
#> [2,] 0.3933585 0.4458331 0.1608085
#> [3,] 0.3186119 0.2610821 0.4203060
#> [4,] 0.3723412 0.4319394 0.1957194
#> [5,] 0.6120843 0.2502868 0.1376289
#> [6,] 0.4831496 0.2666777 0.2501727

To estimate standard errors for the predicted probabilities, we need to set the inference argument to TRUE. However, this works only if honesty is TRUE, as the formula for the variance is valid only for honest predictions. Notice that the estimation of the standard errors can considerably slow down the routine. However, we can increase the number of threads used to construct the forests by using the n.threads argument.

## Compute standard errors.
honest_forests <- ocf(Y_tr, X_tr, honesty = TRUE, inference = TRUE, n.threads = 0) # Use all CPUs.

head(honest_forests$predictions$standard.errors)
#>          P(Y=1)     P(Y=2)     P(Y=3)
#> [1,] 0.04407476 0.08192900 0.06337627
#> [2,] 0.09331785 0.11346017 0.03140943
#> [3,] 0.06818327 0.09355095 0.06689751
#> [4,] 0.11118139 0.14927426 0.07616356
#> [5,] 0.03853504 0.06580477 0.12416180
#> [6,] 0.11411914 0.08089688 0.05264190

Covariates’ Marginal Effects

To estimate the covariates’ marginal effects, we can post-process the conditional probability predictions. This is performed by the marginal_effects function that can estimate mean marginal effects, marginal effects at the mean, and marginal effects at the median, according to the eval argument.

## Fit ocf on training sample.
forests <- ocf(Y_tr, X_tr)

## Marginal effects at the mean.
me_atmean <- marginal_effects(forests, eval = "atmean") # Try also 'eval = "atmean"' and 'eval = "mean"'.

print(me_atmean) # Try also 'latex = TRUE'.
#> ocf marginal effects results 
#> 
#> Data info: 
#> Number of classes:    3 
#> Sample size:          500 
#> 
#> Tuning parameters: 
#> Evaluation:           atmean 
#> Bandwidth:            0.1 
#> Number of trees:      2000 
#> Honest forests:       FALSE 
#> Honesty fraction:     0 
#> 
#> Marginal Effects: 
#>    P'(Y=1) P'(Y=2) P'(Y=3)
#> x1   0.202  -0.462   0.260
#> x2  -0.356  -0.023   0.379
#> x3   0.385  -0.107  -0.278
#> x4  -0.290   0.115   0.176
#> x5   0.019  -0.088   0.069
#> x6  -0.081   0.043   0.038

As before, we can set the inference argument to TRUE to estimate the standard errors. Again, this requires the use of honest forests and can considerably slow down the routine.

## Honest forests.
honest_forests <- ocf(Y_tr, X_tr, honesty = TRUE) # Notice we do not need inference here!

## Compute standard errors.
honest_me_atmean <- marginal_effects(honest_forests, eval = "atmean", inference = TRUE)

## LATEX.
print(honest_me_atmean) # Try also 'latex = TRUE'.
#> ocf marginal effects results 
#> 
#> Data info: 
#> Number of classes:    3 
#> Sample size:          500 
#> 
#> Tuning parameters: 
#> Evaluation:           atmean 
#> Bandwidth:            0.1 
#> Number of trees:      2000 
#> Honest forests:       TRUE 
#> Honesty fraction:     0.5 
#> 
#> Marginal Effects: 
#>    P'(Y=1) P'(Y=2) P'(Y=3)
#> x1  -0.086  -0.120   0.206
#> x2  -0.119   0.043   0.076
#> x3   0.051  -0.050  -0.002
#> x4  -0.146   0.081   0.065
#> x5   0.009   0.027  -0.035
#> x6  -0.048   0.049  -0.001