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.038395854934803304, 0.03839375928415281, 0.03838826738987779, 0.0383809710334689, 0.03837032302482713, 0.03835874092192127, 0.038343576755696655, 0.03832796392734008, 0.03830780462522126, 0.038275376136259295  …  0.04137759437681002, 0.04164021277916051, 0.041913206221465035, 0.042196674158723774, 0.042490657941377304, 0.0427951479690365, 0.0431100944414775, 0.04343538190253497, 0.043770785937548115, 0.04411610106206591], [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], [7049.07849620016 0.0 … 0.0 0.0; -9701.411896739612 2348.196637868324 … 0.0 0.0; … ; 0.0 0.0 … 5.534829241098749 0.0; 0.0 0.0 … -5.518834323116495 0.011477206321819491], 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.0288973266267285
 -1.0169967436017602
 -0.9933147704827087
 -0.872131550938475
 -0.7482172252734769
 -0.6003933892529125
 -0.5091857299915032
 -0.4314585858082689
 -0.3829045954368462
 -0.3418844599722128
  ⋮
  0.7071221238215786
  0.7630132296751112
  0.8023649437876629
  0.8337982230387274
  0.8622202511290816
  0.9051040121500407
  0.9533887247878259
  0.9941865936505095
  1.0025945371846579

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.001106585471596121

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)