Using Custom Model Components
In addition to the built-in component models provided by the TemporalMixtureModels.jl package, you can also define your own custom component models to use within the temporal mixture modeling framework. This allows you to tailor the modeling approach to the specific characteristics of your data and research questions. In this section, an example custom component model is implemented to illustrate how to create and use custom components. In this example, we will create an ExponentialDecay component model that fits an exponential decay function to the time series data.
Defining a Custom Component Model
To define a custom component model, you need to create a new struct that inherits from the Component abstract type. You also need to implement the required methods for the component model, including initialize_parameters, n_parameters, predict, and fit!.
The ExponentialDecay Component
The ExponentialDecay component model fits an exponential decay function of the form:
\[y(t) = A \cdot e^{-B \cdot t} + C\]
The parameters of the model are A, B, and C, which represent the initial value, decay rate, and offset, respectively.
using TemporalMixtureModels, LinearAlgebra, CairoMakie
import TemporalMixtureModels: Component, initialize_parameters, n_parameters, predict, fit!
struct ExponentialDecay <: Component
# No additional fields needed for this simple model
end
function n_parameters(::ExponentialDecay)
return 3 # A, B, C
end
function initialize_parameters(::ExponentialDecay)
return randn(3) .* 0.1 # Small random initialization
end
function predict(::ExponentialDecay, params::AbstractVector, t::AbstractVector, inputs=nothing)
A, B, C = params
return A .* exp.(-B .* t) .+ C
endpredict (generic function with 11 methods)These are quite straightforward implementations. The n_parameters function returns the number of parameters in the model, initialize_parameters provides an initial guess for the parameters, and predict computes the predicted values given the parameters and time points.
Model Fitting
We also need to implement the fit! method to estimate the parameters from the data. Here, we will use a linearization approach to fit the exponential decay model. We need both an unweighted and a weighted version of the fit! function.
function fit!(parameters::AbstractVector{T}, ::ExponentialDecay, t::AbstractVector{T}, y::AbstractVecOrMat{T}, ::Any) where T<:Real
# Linearize the model: y - C = A * exp(-B * t)
# Take logarithm: log(y - C) = log(A) - B * t
# This requires y > C, so we need to estimate C first
C_est = minimum(y) - 0.1 # Slightly below the minimum observed value
y_adjusted = y .- C_est
valid_mask = y_adjusted .> 0 # Only consider valid points for log
if sum(valid_mask) < 2
error("Not enough valid data points to fit ExponentialDecay model.")
end
log_y = log.(y_adjusted[valid_mask])
t_valid = t[valid_mask]
# Fit linear model: log_y = log(A) - B * t_valid
X = hcat(ones(length(t_valid)), -t_valid)
coeffs = X \ log_y
A_est = exp(coeffs[1])
B_est = coeffs[2]
parameters[1] = A_est
parameters[2] = B_est
parameters[3] = C_est
end
function fit!(parameters::AbstractVector{T}, ::ExponentialDecay, t::AbstractVector{T}, y::AbstractVecOrMat{T}, w::AbstractVector{T}, ::Any) where T<:Real
C_est = minimum(y) - 0.1 # Slightly below the minimum observed value
y_adjusted = y .- C_est
valid_mask = (y_adjusted .> 0) .& (w .> 0) # Only consider valid points for log and positive weights
if sum(valid_mask) < 2
error("Not enough valid data points to fit ExponentialDecay model.")
end
log_y = log.(y_adjusted[valid_mask])
t_valid = t[valid_mask]
w_valid = w[valid_mask]
# Fit weighted linear model: log_y = log(A) - B * t_valid
X = hcat(ones(length(t_valid)), -t_valid)
W = Diagonal(w_valid)
coeffs = (X' * W * X) \ (X' * W * log_y)
A_est = exp(coeffs[1])
B_est = coeffs[2]
parameters[1] = A_est
parameters[2] = B_est
parameters[3] = C_est
endfit! (generic function with 10 methods)Using the Custom Component Model
Once you have defined your custom component model, you can use it in the same way as the built-in component models. Here is an example of how to fit a temporal mixture model using the ExponentialDecay component:
using TemporalMixtureModels, Random
Random.seed!(27052023) # For reproducibility
# Generate synthetic data
t, y, ids, class_labels = example_bp_data(;n_subjects_drug=21, n_subjects_placebo=22, n_timepoints=5)
# Define the custom component model
component_model = ExponentialDecay()
num_components = 2 # For example, we want to fit 2 clusters
model = fit_mixture(component_model, num_components, t, y[:, 1], ids)MixtureResult(Main.ExponentialDecay(), 2, [[50.12072480402992, 0.2512978901298546, 71.15237781307899], [50.152546272291325, 0.022915149115559356, 71.15237781307899]], [[79.99558306845877], [92.76453015837888]], [0.3215130256216843, 0.6784869743783156], [0.9235790113191404 0.07642098868085946; 0.9876663314155105 0.012333668584489515; … ; 2.0362708507697925e-8 0.9999999796372916; 0.0018931663530993305 0.9981068336469008], -812.1395770604607, true, 19, NormalError())This will fit a temporal mixture model with 2 ExponentialDecay components to the systolic blood pressure data. You can then make predictions and analyze the fitted model as usual.
We can use the fitted model to make predictions for new time points:
new_time_points = 0:0.1:5.0
predictions = predict(model, new_time_points)