Train-Test Splitting for Symbolic Regression

# Install the Python backend (only needs to be done once). 
leaf::install_leaf()
# Load package 
library(leaf)
if (!backend_available()) {
  message("Install backend with leaf::install_leaf()")
}  
set.seed(42)

N <- 50L

# Generate features
x1 <- runif(N, min = 1, max = 40)
x2 <- runif(N, min = 0, max = 2)

# Generate target: y = 0.23·log(x1) + 2·x1·x2 + noise
y <- 0.23 * log(x1) + 2 * x1 * x2 + rnorm(N, mean = 0, sd = 1e-2)

data <- data.frame(x1 = x1, x2 = x2, y = y)

# Train/test split
train_ratio <- 0.8
train_index <- sample(seq_len(nrow(data)), size = floor(train_ratio * nrow(data)))

train_data <- data[train_index, ]
test_data  <- data[-train_index, ]
print(head(train_data))
#>           x1        x2         y
#> 24 37.920061 0.9587971 73.541917
#> 34 27.721619 1.2912638 72.354745
#> 25  4.215065 0.3948207  3.653854
#> 28 36.323787 0.7509799 55.387777
#> 43  2.459810 0.4171399  2.265693
#> 14 10.961724 1.1329768 25.403471
print(head(test_data))
#>           x1        x2         y
#> 6  21.244742 1.4975908 64.337530
#> 7  29.726944 1.3545537 81.320448
#> 8   6.251997 0.3425287  4.705442
#> 15 19.029420 1.6993794 65.346714
#> 18  5.582007 1.6563170 18.897027
#> 19 19.524886 1.3864096 54.831677
# Initialise the symbolic regressor
regressor = leaf::SymbolicRegressor$new(
  engine='rsrm',
  loss='MSE',
  num_iterations=2L, 
  threshold=1e-10,
  operators = c("+"),
  base=list(verbose = FALSE),
  mcts=list(times = 20L),
  gp=list(times = 6L)
)
# Stage 1: Discover equation skeletons
search_results = regressor$search_equations(
    data = train_data,
    formula = 'y ~ f( log(x1), x1*x2 )',
    normalization = 'divide_by_gmd'
)
#> 1. Processing data for equation search based on formula...
#> 2. Running engine 'rsrm' over 1 folds using up to 1 processes...
#> -- FINAL RESULTS --
#> Episode: 2/2
#> time: 7.88s
#> loss: 8.57761298360967e-05
#> form: F
#> HOF:
#>                  equation  complexity  loss
#> 0              41.2303*X2           2  0.18
#> 1  0.1944*X1 + 40.8084*X2           4  0.00
#> ---
#> 
task:dataset_d5a244a5-e53e-4009-bca7-6c3555d75360 expr:0.19442414717846374*X1 + 40.80839593452161*X2 Loss_MSE:0.00 Test 0/1.
#> final result:
#> success rate : 0%
#> average discovery time is 7.885 seconds
#> Number of equations looked at (per test) [Total, Timed out, Successful]:  [[6, 0, 6]]
#> 3. Found 2 raw skeletons. Deduplicating...
print("=== Search results ===")
#> [1] "=== Search results ==="
print(search_results)
#>                Equation Complexity
#> 0              β1⋅x1⋅x2          3
#> 1 β1⋅log(x1) + β2⋅x1⋅x2          6
# Stage 2: Fit parameters and compute loss
fit_results = regressor$fit(
    data = train_data
)
#> Fitting parameters for 2 equations...
#> Parameter fitting complete.
print("\n=== Fit results ===")
#> [1] "\n=== Fit results ==="
print(fit_results)
#>                Equation Complexity         Loss
#> 0              β1⋅x1⋅x2          3 1.754847e-01
#> 1 β1⋅log(x1) + β2⋅x1⋅x2          6 8.577613e-05
# Stage 3: Evaluate additional metrics
eval_table = regressor$evaluate(
  metrics = c('R2', 'Elbow'),
  data = test_data
)

print("\n=== Additional metrics ===")
#> [1] "\n=== Additional metrics ==="
print(regressor$get_pareto_front())
#>                Equation Complexity         Loss        R2       Elbow
#> 0              β1⋅x1⋅x2          3 1.754847e-01 0.9999509 0.990636907
#> 1 β1⋅log(x1) + β2⋅x1⋅x2          6 8.577613e-05 0.9999999 0.009363093