Solutions to Exercises

using MLJ, Downloads, CSV, DataFrames, Plots

Exercise 1 solution

scitype(42)
ScientificTypesBase.Count
questions = ["who", "why", "what", "when"]
scitype(questions)
AbstractVector{Textual} (alias for AbstractArray{ScientificTypesBase.Textual, 1})
elscitype(questions)
ScientificTypesBase.Textual
t = (3.141, 42, "how")
scitype(t)
Tuple{ScientificTypesBase.Continuous, ScientificTypesBase.Count, ScientificTypesBase.Textual}
A = rand(2, 3)
2×3 Matrix{Float64}:
 0.426011  0.161761  0.375289
 0.762629  0.073642  0.419511
scitype(A)
AbstractMatrix{Continuous} (alias for AbstractArray{ScientificTypesBase.Continuous, 2})
elscitype(A)
ScientificTypesBase.Continuous
using SparseArrays
Asparse = sparse(A)
2×3 SparseArrays.SparseMatrixCSC{Float64, Int64} with 6 stored entries:
 0.426011  0.161761  0.375289
 0.762629  0.073642  0.419511
scitype(Asparse)
AbstractMatrix{Continuous} (alias for AbstractArray{ScientificTypesBase.Continuous, 2})
C = coerce(A, Multiclass)
2×3 CategoricalArrays.CategoricalArray{Float64,2,UInt32}:
 0.426011  0.161761  0.375289
 0.762629  0.073642  0.419511
scitype(C)
AbstractMatrix{Multiclass{6}} (alias for AbstractArray{ScientificTypesBase.Multiclass{6}, 2})
elscitype(C)
ScientificTypesBase.Multiclass{6}
v = [1, 2, missing, 4]
scitype(v)
AbstractVector{Union{Missing, Count}} (alias for AbstractArray{Union{Missing, ScientificTypesBase.Count}, 1})
elscitype(v)
Union{Missing, ScientificTypesBase.Count}
scitype(v[1:2])
AbstractVector{Union{Missing, Count}} (alias for AbstractArray{Union{Missing, ScientificTypesBase.Count}, 1})

Exercise 2 solution

From the question statement:

quality = ["good", "poor", "poor", "excellent", missing, "good", "excellent"]
7-element Vector{Union{Missing, String}}:
 "good"
 "poor"
 "poor"
 "excellent"
 missing
 "good"
 "excellent"
quality = coerce(quality, OrderedFactor);
levels!(quality, ["poor", "good", "excellent"]);
elscitype(quality)
Union{Missing, ScientificTypesBase.OrderedFactor{3}}

Exercise 3 solution

From the question statement:

url = "https://raw.githubusercontent.com/ablaom/"*
    "MachineLearningInJulia2020/for-MLJ-version-0.16/"*
    "data/house.csv";
house = CSV.read(Downloads.download(url), DataFrames.DataFrame)
first(house, 4)
4×19 DataFrame
Rowpricebedroomsbathroomssqft_livingsqft_lotfloorswaterfrontviewconditiongradesqft_abovesqft_basementyr_builtzipcodelatlongsqft_living15sqft_lot15is_renovated
Float64Int64Float64Int64Int64Float64Int64Int64Int64Int64Int64Int64Int64Int64Float64Float64Int64Int64Bool
1221900.031.0118056501.000371180019559817847.5112-122.25713405650true
2538000.032.25257072422.00037217040019519812547.721-122.31916907639false
3180000.021.0770100001.00036770019339802847.7379-122.23327208062true
4604000.043.0196050001.00057105091019659813647.5208-122.39313605000true

First pass:

coerce!(house, autotype(house));
schema(house)
┌───────────────┬───────────────────┬───────────────────────────────────┐
│ names         │ scitypes          │ types                             │
├───────────────┼───────────────────┼───────────────────────────────────┤
│ price         │ Continuous        │ Float64                           │
│ bedrooms      │ OrderedFactor{13} │ CategoricalValue{Int64, UInt32}   │
│ bathrooms     │ OrderedFactor{30} │ CategoricalValue{Float64, UInt32} │
│ sqft_living   │ Count             │ Int64                             │
│ sqft_lot      │ Count             │ Int64                             │
│ floors        │ OrderedFactor{6}  │ CategoricalValue{Float64, UInt32} │
│ waterfront    │ OrderedFactor{2}  │ CategoricalValue{Int64, UInt32}   │
│ view          │ OrderedFactor{5}  │ CategoricalValue{Int64, UInt32}   │
│ condition     │ OrderedFactor{5}  │ CategoricalValue{Int64, UInt32}   │
│ grade         │ OrderedFactor{12} │ CategoricalValue{Int64, UInt32}   │
│ sqft_above    │ Count             │ Int64                             │
│ sqft_basement │ Count             │ Int64                             │
│ yr_built      │ Count             │ Int64                             │
│ zipcode       │ OrderedFactor{70} │ CategoricalValue{Int64, UInt32}   │
│ lat           │ Continuous        │ Float64                           │
│ long          │ Continuous        │ Float64                           │
│ sqft_living15 │ Count             │ Int64                             │
│ sqft_lot15    │ Count             │ Int64                             │
│ is_renovated  │ OrderedFactor{2}  │ CategoricalValue{Bool, UInt32}    │
└───────────────┴───────────────────┴───────────────────────────────────┘

All the "sqft" fields refer to "square feet" so are really Continuous. We'll regard :yr_built (the other Count variable above) as Continuous as well. So:

coerce!(house, Count => Continuous);

And :zipcode should not be ordered:

coerce!(house, :zipcode => Multiclass);
schema(house)
┌───────────────┬───────────────────┬───────────────────────────────────┐
│ names         │ scitypes          │ types                             │
├───────────────┼───────────────────┼───────────────────────────────────┤
│ price         │ Continuous        │ Float64                           │
│ bedrooms      │ OrderedFactor{13} │ CategoricalValue{Int64, UInt32}   │
│ bathrooms     │ OrderedFactor{30} │ CategoricalValue{Float64, UInt32} │
│ sqft_living   │ Continuous        │ Float64                           │
│ sqft_lot      │ Continuous        │ Float64                           │
│ floors        │ OrderedFactor{6}  │ CategoricalValue{Float64, UInt32} │
│ waterfront    │ OrderedFactor{2}  │ CategoricalValue{Int64, UInt32}   │
│ view          │ OrderedFactor{5}  │ CategoricalValue{Int64, UInt32}   │
│ condition     │ OrderedFactor{5}  │ CategoricalValue{Int64, UInt32}   │
│ grade         │ OrderedFactor{12} │ CategoricalValue{Int64, UInt32}   │
│ sqft_above    │ Continuous        │ Float64                           │
│ sqft_basement │ Continuous        │ Float64                           │
│ yr_built      │ Continuous        │ Float64                           │
│ zipcode       │ Multiclass{70}    │ CategoricalValue{Int64, UInt32}   │
│ lat           │ Continuous        │ Float64                           │
│ long          │ Continuous        │ Float64                           │
│ sqft_living15 │ Continuous        │ Float64                           │
│ sqft_lot15    │ Continuous        │ Float64                           │
│ is_renovated  │ OrderedFactor{2}  │ CategoricalValue{Bool, UInt32}    │
└───────────────┴───────────────────┴───────────────────────────────────┘

:bathrooms looks like it has a lot of levels, but on further inspection we see why, and OrderedFactor remains appropriate.

import StatsBase.countmap
d = countmap(house.bathrooms)
for (level, count) in d
    println("$level \t=> $count")
end
5.0 	=> 21
5.25 	=> 13
1.25 	=> 9
8.0 	=> 2
6.75 	=> 2
1.0 	=> 3852
5.5 	=> 10
0.0 	=> 10
6.0 	=> 6
6.25 	=> 2
4.75 	=> 23
3.25 	=> 589
3.0 	=> 753
2.25 	=> 2047
0.5 	=> 4
7.5 	=> 1
5.75 	=> 4
1.5 	=> 1446
3.75 	=> 155
4.0 	=> 136
4.25 	=> 79
2.0 	=> 1930
2.75 	=> 1185
3.5 	=> 731
6.5 	=> 2
1.75 	=> 3048
0.75 	=> 72
2.5 	=> 5380
4.5 	=> 100
7.75 	=> 1

Exercise 4 solution

From the question statement:

import Distributions
poisson = Distributions.Poisson

age = 18 .+ 60*rand(10);
salary = coerce(rand(["small", "big", "huge"], 10), OrderedFactor);
levels!(salary, ["small", "big", "huge"]);
small = salary[1]

X4 = DataFrames.DataFrame(age=age, salary=salary)

n_devices(salary) = salary > small ? rand(poisson(1.3)) : rand(poisson(2.9))
y4 = [n_devices(row.salary) for row in eachrow(X4)]
10-element Vector{Int64}:
 1
 0
 2
 3
 1
 1
 3
 3
 1
 5

4(a)

models(matching(X4, y4))
2-element Vector{NamedTuple{(:name, :package_name, :is_supervised, :abstract_type, :constructor, :deep_properties, :docstring, :fit_data_scitype, :human_name, :hyperparameter_ranges, :hyperparameter_types, :hyperparameters, :implemented_methods, :inverse_transform_scitype, :is_pure_julia, :is_wrapper, :iteration_parameter, :load_path, :package_license, :package_url, :package_uuid, :predict_scitype, :prediction_type, :reporting_operations, :reports_feature_importances, :supports_class_weights, :supports_online, :supports_training_losses, :supports_weights, :tags, :target_in_fit, :transform_scitype, :input_scitype, :target_scitype, :output_scitype)}}:
 (name = EvoTreeCount, package_name = EvoTrees, ... )
 (name = LinearCountRegressor, package_name = GLM, ... )

4(b)

y4 = coerce(y4, Continuous);
models(matching(X4, y4))
13-element Vector{NamedTuple{(:name, :package_name, :is_supervised, :abstract_type, :constructor, :deep_properties, :docstring, :fit_data_scitype, :human_name, :hyperparameter_ranges, :hyperparameter_types, :hyperparameters, :implemented_methods, :inverse_transform_scitype, :is_pure_julia, :is_wrapper, :iteration_parameter, :load_path, :package_license, :package_url, :package_uuid, :predict_scitype, :prediction_type, :reporting_operations, :reports_feature_importances, :supports_class_weights, :supports_online, :supports_training_losses, :supports_weights, :tags, :target_in_fit, :transform_scitype, :input_scitype, :target_scitype, :output_scitype)}}:
 (name = CatBoostRegressor, package_name = CatBoost, ... )
 (name = ConstantRegressor, package_name = MLJModels, ... )
 (name = DecisionTreeRegressor, package_name = BetaML, ... )
 (name = DecisionTreeRegressor, package_name = DecisionTree, ... )
 (name = DeterministicConstantRegressor, package_name = MLJModels, ... )
 (name = EvoLinearRegressor, package_name = EvoLinear, ... )
 (name = EvoTreeGaussian, package_name = EvoTrees, ... )
 (name = EvoTreeMLE, package_name = EvoTrees, ... )
 (name = EvoTreeRegressor, package_name = EvoTrees, ... )
 (name = LinearRegressor, package_name = GLM, ... )
 (name = NeuralNetworkRegressor, package_name = MLJFlux, ... )
 (name = RandomForestRegressor, package_name = BetaML, ... )
 (name = RandomForestRegressor, package_name = DecisionTree, ... )

Exercise 5 solution

data = (
    a = [1, 2, 3, 4],
    b = rand(4),
    c = rand(4),
    d = coerce(["male", "female", "female", "male"], OrderedFactor),
);

using Tables
y, X, w = unpack(
    data,
    ==(:a),
    name -> elscitype(Tables.getcolumn(data, name)) == Continuous,
)
y
4-element Vector{Int64}:
 1
 2
 3
 4
pretty(X)
┌────────────┬────────────┐
│ b          │ c          │
│ Float64    │ Float64    │
│ Continuous │ Continuous │
├────────────┼────────────┤
│ 0.526857   │ 0.563734   │
│ 0.254198   │ 0.833533   │
│ 0.21846    │ 0.997917   │
│ 0.426727   │ 0.62664    │
└────────────┴────────────┘
w
4-element CategoricalArrays.CategoricalArray{String,1,UInt32}:
 "male"
 "female"
 "female"
 "male"

Exercise 6 solution

From the question statement:

import Downloads
import CSV
url = "https://raw.githubusercontent.com/ablaom/"*
    "MachineLearningInJulia2020/"*
    "for-MLJ-version-0.16/data/horse.csv"
csv_file = Downloads.download(url)
horse = CSV.read(csv_file, DataFrames.DataFrame)
coerce!(horse, autotype(horse));
coerce!(horse, Count => Continuous);
coerce!(
    horse,
    :surgery               => Multiclass,
    :age                   => Multiclass,
    :mucous_membranes      => Multiclass,
    :capillary_refill_time => Multiclass,
    :outcome               => Multiclass,
    :cp_data               => Multiclass,
);
schema(horse)
┌─────────────────────────┬──────────────────┬─────────────────────────────────┐
│ names                   │ scitypes         │ types                           │
├─────────────────────────┼──────────────────┼─────────────────────────────────┤
│ surgery                 │ Multiclass{2}    │ CategoricalValue{Int64, UInt32} │
│ age                     │ Multiclass{2}    │ CategoricalValue{Int64, UInt32} │
│ rectal_temperature      │ Continuous       │ Float64                         │
│ pulse                   │ Continuous       │ Float64                         │
│ respiratory_rate        │ Continuous       │ Float64                         │
│ temperature_extremities │ OrderedFactor{4} │ CategoricalValue{Int64, UInt32} │
│ mucous_membranes        │ Multiclass{6}    │ CategoricalValue{Int64, UInt32} │
│ capillary_refill_time   │ Multiclass{3}    │ CategoricalValue{Int64, UInt32} │
│ pain                    │ OrderedFactor{5} │ CategoricalValue{Int64, UInt32} │
│ peristalsis             │ OrderedFactor{4} │ CategoricalValue{Int64, UInt32} │
│ abdominal_distension    │ OrderedFactor{4} │ CategoricalValue{Int64, UInt32} │
│ packed_cell_volume      │ Continuous       │ Float64                         │
│ total_protein           │ Continuous       │ Float64                         │
│ outcome                 │ Multiclass{3}    │ CategoricalValue{Int64, UInt32} │
│ surgical_lesion         │ OrderedFactor{2} │ CategoricalValue{Int64, UInt32} │
│ cp_data                 │ Multiclass{2}    │ CategoricalValue{Int64, UInt32} │
└─────────────────────────┴──────────────────┴─────────────────────────────────┘

6(a)

y, X = unpack(
    horse,
    ==(:outcome),
    name -> elscitype(Tables.getcolumn(horse, name)) == Continuous,
)
schema(X)
┌────────────────────┬────────────┬─────────┐
│ names              │ scitypes   │ types   │
├────────────────────┼────────────┼─────────┤
│ rectal_temperature │ Continuous │ Float64 │
│ pulse              │ Continuous │ Float64 │
│ respiratory_rate   │ Continuous │ Float64 │
│ packed_cell_volume │ Continuous │ Float64 │
│ total_protein      │ Continuous │ Float64 │
└────────────────────┴────────────┴─────────┘

6(b)(i)

train, test = partition(eachindex(y), 0.7)
LogisticClassifier = @load LogisticClassifier pkg=MLJLinearModels
model = LogisticClassifier()
model.lambda = 100
mach = machine(model, X, y)
fit!(mach, rows=train)
fitted_params(mach)
(classes = CategoricalArrays.CategoricalValue{Int64, UInt32}[CategoricalValue(CategoricalArrays.CategoricalPool{Int64, UInt32}([1, 2, 3]), 1), CategoricalValue(CategoricalArrays.CategoricalPool{Int64, UInt32}([1, 2, 3]), 2), CategoricalValue(CategoricalArrays.CategoricalPool{Int64, UInt32}([1, 2, 3]), 3)], coefs = Pair{Symbol, SubArray{Float64, 1, Matrix{Float64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}[:rectal_temperature => [0.01679940181352405, -0.006529534963799633, -0.010269866849724429], :pulse => [-0.002078922183617693, 0.00248508277505068, -0.00040616059143293954], :respiratory_rate => [-0.002078922183617693, 0.00248508277505068, -0.00040616059143293954], :packed_cell_volume => [0.0026345470622363147, 0.002707772252298626, -0.005342319314534946], :total_protein => [0.009110258500266177, -0.01679787903626504, 0.0076876205359988755]], intercept = [0.00043563961897106053, -0.00016706727792654562, -0.0005913867104289383])
coefs_given_feature = Dict(fitted_params(mach).coefs)
coefs_given_feature[:pulse]

#6(b)(ii)

yhat = predict(mach, rows=test); # or predict(mach, X[test,:])
err = log_loss(yhat, y[test])
0.8334775485441969

6(b)(iii)

The predicted probabilities of the actual observations in the test are given by

p = broadcast(pdf, yhat, y[test]);

The number of times this probability exceeds 50% is:

n50 = filter(x -> x > 0.5, p) |> length
58

Or, as a proportion:

n50/length(test)
0.5272727272727272

6(b)(iv)

misclassification_rate(mode.(yhat), y[test])
0.3181818181818182

6(c)(i)

RandomForestClassifier = @load RandomForestClassifier pkg=DecisionTree
model = RandomForestClassifier()
mach = machine(model, X, y)
evaluate!(mach, resampling=CV(nfolds=6), measure=log_loss)
PerformanceEvaluation object with these fields:
  model, tag, measure, operation,
  measurement, uncertainty_radius_95, per_fold, per_observation,
  fitted_params_per_fold, report_per_fold,
  train_test_rows, resampling, repeats
Tag: RandomForestClassifier-373
Extract:
┌──────────────────────┬───────────┬─────────────┐
│ measure              │ operation │ measurement │
├──────────────────────┼───────────┼─────────────┤
│ LogLoss(             │ predict   │ 1.17        │
│   tol = 2.22045e-16) │           │             │
└──────────────────────┴───────────┴─────────────┘
┌────────────────────────────────────────┬─────────┐
│ per_fold                               │ 1.96*SE │
├────────────────────────────────────────┼─────────┤
│ [0.747, 1.37, 2.32, 0.773, 1.2, 0.598] │ 0.557   │
└────────────────────────────────────────┴─────────┘
r = range(model, :n_trees, lower=10, upper=70, scale=:log10)
NumericRange(10 ≤ n_trees ≤ 70; origin=40.0, unit=30.0; on log10 scale)

Since random forests are inherently randomized, we generate multiple curves:

curves = learning_curve(
    mach,
    range=r,
    resampling=Holdout(),
    measure=log_loss,
    rngs=4,
    rng_name=:rng,
)

plt = plot(curves.parameter_values, curves.measurements)
xlabel!(plt, "n_trees")
ylabel!(plt, "cross entropy")
savefig("exercise_6ci.png")
"/home/runner/work/MLJTutorial.jl/MLJTutorial.jl/docs/src/notebooks/99_solution_to_exercises/exercise_6ci.png"

6(c)(ii)

evaluate!(mach, resampling=CV(nfolds=9),
                measure=log_loss,
                rows=train).measurement[1]

model.n_trees = 90
90

6(c)(iii)

err_forest =
    evaluate!(mach, resampling=Holdout(), measure=log_loss).measurement[1]
0.9859147358185245

Exercise 7

7(a)

KMeans = @load KMeans pkg=Clustering
EvoTreeClassifier = @load EvoTreeClassifier
pipe = Standardizer |>
    ContinuousEncoder |>
    KMeans(k=10) |>
    EvoTreeClassifier(nrounds=50)
ProbabilisticPipeline(
  standardizer = Standardizer(
        features = Symbol[], 
        ignore = false, 
        ordered_factor = false, 
        count = false), 
  continuous_encoder = ContinuousEncoder(
        drop_last = false, 
        one_hot_ordered_factors = false), 
  k_means = KMeans(
        k = 10, 
        metric = Distances.SqEuclidean(0.0), 
        init = :kmpp), 
  evo_tree_classifier = EvoTreeClassifier(
        loss = :mlogloss, 
        metric = :mlogloss, 
        nrounds = 50, 
        bagging_size = 1, 
        early_stopping_rounds = 9223372036854775807, 
        L2 = 1.0, 
        lambda = 0.0, 
        gamma = 0.0, 
        eta = 0.1, 
        max_depth = 6, 
        min_weight = 1.0, 
        rowsample = 1.0, 
        colsample = 1.0, 
        nbins = 64, 
        tree_type = :binary, 
        seed = 123, 
        device = :cpu), 
  cache = true)

7(b)

mach = machine(pipe, X, y)
evaluate!(mach, resampling=CV(nfolds=6), measure=log_loss)
PerformanceEvaluation object with these fields:
  model, tag, measure, operation,
  measurement, uncertainty_radius_95, per_fold, per_observation,
  fitted_params_per_fold, report_per_fold,
  train_test_rows, resampling, repeats
Tag: ProbabilisticPipeline-811
Extract:
┌──────────────────────┬───────────┬─────────────┐
│ measure              │ operation │ measurement │
├──────────────────────┼───────────┼─────────────┤
│ LogLoss(             │ predict   │ 0.829       │
│   tol = 2.22045e-16) │           │             │
└──────────────────────┴───────────┴─────────────┘
┌───────────────────────────────────────────┬─────────┐
│ per_fold                                  │ 1.96*SE │
├───────────────────────────────────────────┼─────────┤
│ [0.916, 0.95, 0.876, 0.787, 0.775, 0.669] │ 0.0917  │
└───────────────────────────────────────────┴─────────┘

7(c)

r = range(pipe, :(evo_tree_classifier.max_depth), lower=1, upper=10)
curve = learning_curve(
    mach,
    range=r,
    resampling=CV(nfolds=6),
    measure=log_loss,
)
plt = plot(curve.parameter_values, curve.measurements)
xlabel!(plt, "max_depth")
ylabel!(plt, "CV estimate of cross entropy")
savefig("exercise_7c.png")
"/home/runner/work/MLJTutorial.jl/MLJTutorial.jl/docs/src/notebooks/99_solution_to_exercises/exercise_7c.png"

Exercise 8

From the question statement:

y, X = unpack(house, ==(:price), rng=123); # from Exercise 3

EvoTreeRegressor = @load EvoTreeRegressor
tree_booster = EvoTreeRegressor(nrounds = 70)
model = ContinuousEncoder |> tree_booster

r2 = range(model, :(evo_tree_regressor.colsample), lower=0.5, upper=1.0)
NumericRange(0.5 ≤ evo_tree_regressor.colsample ≤ 1.0; origin=0.75, unit=0.25)

(a)

r1 = range(model, :(evo_tree_regressor.max_depth), lower=1, upper=12)
NumericRange(1 ≤ evo_tree_regressor.max_depth ≤ 12; origin=6.5, unit=5.5)

(b)

tuned_model = TunedModel(
    model;
    ranges=[r1, r2],
    resampling=Holdout(),
    measures=mae,
    tuning=RandomSearch(rng=123),
    n=40,
)

tuned_mach = machine(tuned_model, X, y) |> fit!
plt = plot(tuned_mach)
savefig("exercise_8c.png")
"/home/runner/work/MLJTutorial.jl/MLJTutorial.jl/docs/src/notebooks/99_solution_to_exercises/exercise_8c.png"

(c)

best_model = report(tuned_mach).best_model;
best_mach = machine(best_model, X, y);
best_err = evaluate!(best_mach, resampling=CV(nfolds=3), measure=mae)
PerformanceEvaluation object with these fields:
  model, tag, measure, operation,
  measurement, uncertainty_radius_95, per_fold, per_observation,
  fitted_params_per_fold, report_per_fold,
  train_test_rows, resampling, repeats
Tag: DeterministicPipeline-153
Extract:
┌──────────┬───────────┬─────────────┐
│ measure  │ operation │ measurement │
├──────────┼───────────┼─────────────┤
│ LPLoss(  │ predict   │ 80300.0     │
│   p = 1) │           │             │
└──────────┴───────────┴─────────────┘
┌─────────────────────────────┬─────────┐
│ per_fold                    │ 1.96*SE │
├─────────────────────────────┼─────────┤
│ [79700.0, 81000.0, 80300.0] │ 852.0   │
└─────────────────────────────┴─────────┘
tuned_err = evaluate!(tuned_mach, resampling=CV(nfolds=3), measure=mae)
PerformanceEvaluation object with these fields:
  model, tag, measure, operation,
  measurement, uncertainty_radius_95, per_fold, per_observation,
  fitted_params_per_fold, report_per_fold,
  train_test_rows, resampling, repeats
Tag: DeterministicTunedModel-941
Extract:
┌──────────┬───────────┬─────────────┐
│ measure  │ operation │ measurement │
├──────────┼───────────┼─────────────┤
│ LPLoss(  │ predict   │ 126000.0    │
│   p = 1) │           │             │
└──────────┴───────────┴─────────────┘
┌──────────────────────────────┬──────────┐
│ per_fold                     │ 1.96*SE  │
├──────────────────────────────┼──────────┤
│ [66500.0, 75600.0, 236000.0] │ 132000.0 │
└──────────────────────────────┴──────────┘

This page was generated using Literate.jl.