This section shows how to conduct monotone fitting with monotone splines with the existing optimization toolbox. The smoothing parameter can be tuned by cross-validation. We also compare the monotone splines with the popular smoothing splines (R's implementation smooth.spline).

using MonotoneSplines
using Plots
using Random
using RCall

Cubic Curve

n = 100
seed = 1234
σ = 0.2
Random.seed!(seed)
x = rand(n) * 2 .- 1
y = x .^3 + randn(n) * σ
λs = exp.(range(-10, -4, length = 100));

Perform cross-validation for monotone fitting with smoothing splines,

@time errs, B, L, J = MonotoneSplines.cv_mono_ss(x, y, λs)
([0.03836494968310219, 0.038356054988443196, 0.038346860326416564, 0.03833810480828669, 0.03833030336201492, 0.0383181815212615, 0.038306086526776, 0.03829277370690645, 0.03827569988291665, 0.03825256780043086  …  0.041377653962167346, 0.04164026736341345, 0.041913261668632816, 0.04219672525984907, 0.042490701739591435, 0.04279519591471907, 0.04311015485047943, 0.04343543081198881, 0.0437708461692481, 0.04411615119567598], [0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.0 … 0.0 0.0], [3722.3861277938377 0.0 … 0.0 0.0; -5484.443813834549 2256.9009327110152 … 0.0 0.0; … ; 0.0 0.0 … 5.503905144035493 0.0; 0.0 0.0 … -5.4879608135793845 0.011543876496072695], 64)

Then plot the CV curve

scatter(log.(λs), errs, title = "seed = $seed")

Then we can choose λ which minimized the CV error.

idx = argmin(errs)
λopt = λs[idx]
0.0008846636600765526

Fit with λopt

βhat, yhat = MonotoneSplines.mono_ss(B, y, L, J, λopt);

Alternatively,

res = MonotoneSplines.mono_ss(x, y, λopt);
yhat = res.fitted
βhat = res.β
64-element Vector{Float64}:
 -1.0288757376477982
 -1.010651571190564
 -0.9869589289078865
 -0.8657114782087676
 -0.7478717956212413
 -0.6000033126726187
 -0.5064227831953195
 -0.42894948406762573
 -0.36508688261887273
 -0.3056529887751692
  ⋮
  0.7140294931741021
  0.784167204977876
  0.8180581915475537
  0.8494013724090633
  0.8659588322917697
  0.9073886912733284
  0.9556781925759968
  0.9941873855077844
  1.0025953675523138

Plot it

scatter(x, y)
scatter!(x, yhat)

We can also compare it with smooth.spline,

spl = R"smooth.spline($x, $y)"
RCall.RObject{RCall.VecSxp}
Call:
smooth.spline(x = `#JL`$x, y = `#JL`$y)

Smoothing Parameter  spar= 0.8358156  lambda= 0.001106585 (13 iterations)
Equivalent Degrees of Freedom (Df): 7.05542
Penalized Criterion (RSS): 3.174281
GCV: 0.03674491

it also determine λ by cross-validation,

λ = rcopy(R"$spl$lambda")
0.0011065854744521935

we can plot its fitting values together,

yhat_ss = rcopy(R"predict($spl, $x)$y")
scatter!(x, yhat_ss)

For ease of demonstrating other examples, we wrap up the above procedures as a function

function demo_mono_ss(x, y, λs)
    errs, B, L, J = MonotoneSplines.cv_mono_ss(x, y, λs)
    fig1 = plot(log.(λs), errs, xlab = "λ", ylab = "CV error", legend=false)
    λopt = λs[argmin(errs)]
    λ_mono_ss = [round(λopt, sigdigits = 4), round(log(λopt), sigdigits=4)]
    yhat = MonotoneSplines.mono_ss(x, y, λopt).fitted
    fig2 = scatter(x, y, label = "obs.")
    scatter!(fig2, x, yhat, label = "mono_ss (λ = $(λ_mono_ss[1]), logλ = $(λ_mono_ss[2]))")
    # ss
    spl = R"smooth.spline($x, $y)"
    λ = rcopy(R"$spl$lambda")
    λ_ss = [round(λ, sigdigits = 4), round(log(λ), sigdigits=4)]
    yhat_ss = rcopy(R"predict($spl, $x)$y")
    scatter!(fig2, x, yhat_ss, label = "ss (λ = $(λ_ss[1]), logλ = $(λ_ss[2]))")
    return plot(fig1, fig2, size = (1240, 420))
end
demo_mono_ss (generic function with 1 method)

Growth Curve

λs = exp.(range(-10, 0, length = 100));

σ = 3.0

σ = 3.0
Random.seed!(seed)
x, y, x0, y0 = MonotoneSplines.gen_data(n, σ, z->1/(1-0.42log(z)), xmin = 0, xmax = 10)
scatter(x, y)
scatter!(x0, y0)

demo_mono_ss(x, y, λs)

σ = 2.0

σ = 2.0
Random.seed!(seed)
x, y, x0, y0 = MonotoneSplines.gen_data(n, σ, z->1/(1-0.42log(z)), xmin = 0, xmax = 10)
scatter(x, y)
scatter!(x0, y0)

demo_mono_ss(x, y, λs)

σ = 0.5

σ = 0.5
Random.seed!(seed)
x, y, x0, y0 = MonotoneSplines.gen_data(n, σ, z->1/(1-0.42log(z)), xmin = 0, xmax = 10)
scatter(x, y)
scatter!(x0, y0)

demo_mono_ss(x, y, λs)

Logistic Curve

λs = exp.(range(-10, 0, length = 100));

σ = 0.2

σ = 0.2
Random.seed!(seed)
x, y, x0, y0 = MonotoneSplines.gen_data(n, σ, z->exp(z)/(1+exp(z)), xmin = -5, xmax = 5)
scatter(x, y)
scatter!(x0, y0)

demo_mono_ss(x, y, λs)

σ = 1.0

σ = 1.0
Random.seed!(seed)
x, y, x0, y0 = MonotoneSplines.gen_data(n, σ, z->exp(z)/(1+exp(z)), xmin = -5, xmax = 5)
scatter(x, y)
scatter!(x0, y0)

demo_mono_ss(x, y, λs)