diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 37ef5474..ca263a9a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,8 @@ jobs:
fail-fast: false
matrix:
version:
- - '1.6'
+ - '1.6' # previous LTS release
+ - '1.10' # new LTS release
- '1' # automatically expands to the latest stable 1.x release of Julia.
os:
- ubuntu-latest
@@ -65,4 +66,4 @@ jobs:
using Documenter: DocMeta, doctest
using LearnAPI
DocMeta.setdocmeta!(LearnAPI, :DocTestSetup, :(using LearnAPI); recursive=true)
- doctest(LearnAPI)'
\ No newline at end of file
+ doctest(LearnAPI)'
diff --git a/Project.toml b/Project.toml
index f8431fdd..ee543d1a 100644
--- a/Project.toml
+++ b/Project.toml
@@ -14,9 +14,8 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
-SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
-test = ["DataFrames", "LinearAlgebra", "MLUtils", "Serialization", "SparseArrays", "Tables", "Test"]
+test = ["DataFrames", "LinearAlgebra", "MLUtils", "Serialization", "Tables", "Test"]
diff --git a/docs/Project.toml b/docs/Project.toml
index caa42f70..47eb52e6 100644
--- a/docs/Project.toml
+++ b/docs/Project.toml
@@ -1,5 +1,6 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
+LearnAPI = "92ad9a40-7767-427a-9ee6-6e577f1266cb"
MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54"
ScientificTypesBase = "30f210dd-8aff-4c5f-94ba-8e64358c1161"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
diff --git a/docs/make.jl b/docs/make.jl
index fd54ce70..a0b0bb37 100644
--- a/docs/make.jl
+++ b/docs/make.jl
@@ -6,18 +6,24 @@ const REPO = Remotes.GitHub("JuliaAI", "LearnAPI.jl")
makedocs(
modules=[LearnAPI,],
- format=Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"),
+ format=Documenter.HTML(
+ prettyurls = get(ENV, "CI", nothing) == "true",
+ collapselevel = 1,
+ ),
pages=[
"Home" => "index.md",
"Anatomy of an Implementation" => "anatomy_of_an_implementation.md",
- "Reference" => "reference.md",
- "Kinds of Target Proxy" => "kinds_of_target_proxy.md",
- "fit" => "fit.md",
- "predict, transform, and relatives" => "predict_transform.md",
- "minimize" => "minimize.md",
- "obs" => "obs.md",
- "Accessor Functions" => "accessor_functions.md",
- "Algorithm Traits" => "traits.md",
+ "Reference" => [
+ "Overview" => "reference.md",
+ "fit/update" => "fit_update.md",
+ "predict/transform" => "predict_transform.md",
+ "Kinds of Target Proxy" => "kinds_of_target_proxy.md",
+ "minimize" => "minimize.md",
+ "target/weights/features" => "target_weights_features.md",
+ "obs" => "obs.md",
+ "Accessor Functions" => "accessor_functions.md",
+ "Algorithm Traits" => "traits.md",
+ ],
"Common Implementation Patterns" => "common_implementation_patterns.md",
"Testing an Implementation" => "testing_an_implementation.md",
],
diff --git a/docs/src/accessor_functions.md b/docs/src/accessor_functions.md
index 07c30f1f..e6e50864 100644
--- a/docs/src/accessor_functions.md
+++ b/docs/src/accessor_functions.md
@@ -1,7 +1,7 @@
# [Accessor Functions](@id accessor_functions)
-The sole argument of an accessor function is the output, `model`, of [`fit`](@ref) or
-[`obsfit`](@ref).
+The sole argument of an accessor function is the output, `model`, of
+[`fit`](@ref). Algorithms are free to implement any number of these, or none of them.
- [`LearnAPI.algorithm(model)`](@ref)
- [`LearnAPI.extras(model)`](@ref)
@@ -12,9 +12,13 @@ The sole argument of an accessor function is the output, `model`, of [`fit`](@re
- [`LearnAPI.feature_importances(model)`](@ref)
- [`LearnAPI.training_labels(model)`](@ref)
- [`LearnAPI.training_losses(model)`](@ref)
+- [`LearnAPI.training_predictions(model)`](@ref)
- [`LearnAPI.training_scores(model)`](@ref)
- [`LearnAPI.components(model)`](@ref)
+Algorithm-specific accessor functions may also be implemented. The names of all accessor
+functions are included in the list returned by [`LearnAPI.functions(algorithm)`](@ref).
+
## Implementation guide
All new implementations must implement [`LearnAPI.algorithm`](@ref). While, all others are
@@ -33,6 +37,7 @@ LearnAPI.tree
LearnAPI.trees
LearnAPI.feature_importances
LearnAPI.training_losses
+LearnAPI.training_predictions
LearnAPI.training_scores
LearnAPI.training_labels
LearnAPI.components
diff --git a/docs/src/anatomy_of_an_implementation.md b/docs/src/anatomy_of_an_implementation.md
index 2bc24a39..3c2a7d5f 100644
--- a/docs/src/anatomy_of_an_implementation.md
+++ b/docs/src/anatomy_of_an_implementation.md
@@ -1,11 +1,38 @@
# Anatomy of an Implementation
This section explains a detailed implementation of the LearnAPI for naive [ridge
-regression](https://en.wikipedia.org/wiki/Ridge_regression). Most readers will want to
-scan the [demonstration](@ref workflow) of the implementation before studying the
-implementation itself.
-
-## Defining an algorithm type
+regression](https://en.wikipedia.org/wiki/Ridge_regression) with no intercept. The kind of
+workflow we want to enable has been previewed in [Sample workflow](@ref). Readers can also
+refer to the [demonstration](@ref workflow) of the implementation given later.
+
+A transformer ordinarily implements `transform` instead of
+`predict`. For more on `predict` versus `transform`, see [Predict or transform?](@ref)
+
+!!! note
+
+ New implementations of `fit`, `predict`, etc,
+ always have a *single* `data` argument, as in
+ `LearnAPI.fit(algorithm, data; verbosity=1) = ...`.
+ For convenience, user-calls, such as `fit(algorithm, X, y)`, automatically fallback
+ to `fit(algorithm, (X, y))`.
+
+!!! note
+
+ By default, it is assumed that `data` supports the [`LearnAPI.RandomAccess`](@ref)
+ interface; this includes all matrices, with observations-as-columns, most tables, and
+ tuples thereof). See [`LearnAPI.RandomAccess`](@ref) for details. If this is not the
+ case then an implementation must either:
+
+ If the `data` object consumed by `fit`, `predict`, or `transform` is not
+ not a suitable table¹, array³, tuple of tables and arrays, or some
+ other object implementing
+ the MLUtils.jl `getobs`/`numobs` interface,
+ then an implementation must: (i) overload [`obs`](@ref) to articulate how
+ provided data can be transformed into a form that does support
+ it, as illustrated below under
+ [Providing an advanced data interface](@ref); or (ii) overload the trait
+ [`LearnAPI.data_interface`](@ref) to specify a more relaxed data
+ API.
The first line below imports the lightweight package LearnAPI.jl whose methods we will be
extending. The second imports libraries needed for the core algorithm.
@@ -16,102 +43,47 @@ using LinearAlgebra, Tables
nothing # hide
```
-A struct stores the regularization hyperparameter `lambda` of our ridge regressor:
+## Defining algorithms
+
+Here's a new type whose instances specify ridge regression parameters:
```@example anatomy
-struct Ridge
- lambda::Float64
+struct Ridge{T<:Real}
+ lambda::T
end
nothing # hide
```
-Instances of `Ridge` are [algorithms](@ref algorithms), in LearnAPI.jl parlance.
+Instances of `Ridge` will be [algorithms](@ref algorithms), in LearnAPI.jl parlance.
-A keyword argument constructor provides defaults for all hyperparameters:
+Associated with each new type of LearnAPI [algorithm](@ref algorithms) will be a keyword
+argument constructor, providing default values for all properties (struct fields) that are
+not other algorithms, and we must implement [`LearnAPI.constructor(algorithm)`](@ref), for
+recovering the constructor from an instance:
```@example anatomy
-Ridge(; lambda=0.1) = Ridge(lambda)
-nothing # hide
-```
-
-## Implementing `fit`
-
-A ridge regressor requires two types of data for training: *input features* `X`, which
-here we suppose are tabular, and a [target](@ref proxy) `y`, which we suppose is a
-vector. Users will accordingly call [`fit`](@ref) like this:
-
-```julia
-algorithm = Ridge(lambda=0.05)
-fit(algorithm, X, y; verbosity=1)
-```
+"""
+ Ridge(; lambda=0.1)
-However, a new implementation does not overload `fit`. Rather it
-implements
-
-```julia
-obsfit(algorithm::Ridge, obsdata; verbosity=1)
-```
-
-for each `obsdata` returned by a data-preprocessing call `obs(fit, algorithm, X, y)`. You
-can read "obs" as "observation-accessible", for reasons explained shortly. The
-LearnAPI.jl definition
-
-```julia
-fit(algorithm, data...; verbosity=1) =
- obsfit(algorithm, obs(fit, algorithm, data...), verbosity)
-```
-then takes care of `fit`.
-
-The `obs` and `obsfit` method are public, and the user can call them like this:
-
-```julia
-obsdata = obs(fit, algorithm, X, y)
-model = obsfit(algorithm, obsdata)
-```
-
-We begin by defining a struct¹ for the output of our data-preprocessing operation, `obs`,
-which will store `y` and the matrix representation of `X`, together with it's column names
-(needed for recording named coefficients for user inspection):
-
-```@example anatomy
-struct RidgeFitData{T}
- A::Matrix{T} # p x n
- names::Vector{Symbol}
- y::Vector{T}
-end
+Instantiate a ridge regression algorithm, with regularization of `lambda`.
+"""
+Ridge(; lambda=0.1) = Ridge(lambda)
+LearnAPI.constructor(::Ridge) = Ridge
nothing # hide
```
-And we overload [`obs`](@ref) like this
+For example, in this case, if `algorithm = Ridge(0.2)`, then
+`LearnAPI.constructor(algorithm)(lambda=0.2) == algorithm` is true. Note that we attach
+the docstring to the *constructor*, not the struct.
-```@example anatomy
-function LearnAPI.obs(::typeof(fit), ::Ridge, X, y)
- table = Tables.columntable(X)
- names = Tables.columnnames(table) |> collect
- return RidgeFitData(Tables.matrix(table, transpose=true), names, y)
-end
-nothing # hide
-```
-so that `obs(fit, Ridge(), X, y)` returns a combined `RidgeFitData` object with everything
-the core algorithm will need.
-
-Since `obs` is public, the user will have access to this object, but to make it useful to
-her (and to fulfill the [`obs`](@ref) contract) this object must implement the
-[MLUtils.jl](https://github.com/JuliaML/MLUtils.jl) `getobs`/`numobs` interface, to enable
-observation-resampling (which will be efficient, because observations are now columns). It
-usually suffices to overload `Base.getindex` and `Base.length` (which are the
-`getobs`/`numobs` fallbacks) so we won't actually need to depend on MLUtils.jl:
+## Implementing `fit`
-```@example anatomy
-Base.getindex(data::RidgeFitData, I) =
- RidgeFitData(data.A[:,I], data.names, y[I])
-Base.length(data::RidgeFitData, I) = length(data.y)
-nothing # hide
-```
+A ridge regressor requires two types of data for training: input features `X`, which here
+we suppose are tabular¹, and a [target](@ref proxy) `y`, which we suppose is a vector.
-Next, we define a second struct for storing the outcomes of training, including named
-versions of the learned coefficients:
+It is convenient to define a new type for the `fit` output, which will include
+coefficients labelled by feature name for inspection after training:
```@example anatomy
struct RidgeFitted{T,F}
@@ -122,21 +94,25 @@ end
nothing # hide
```
-We include `algorithm`, which must be recoverable from the output of `fit`/`obsfit` (see
-[Accessor functions](@ref) below).
+Note that we also include `algorithm` in the struct, for it must be possible to recover
+`algorithm` from the output of `fit`; see [Accessor functions](@ref) below.
-We are now ready to implement a suitable `obsfit` method to execute the core training:
+The core implementation of `fit` looks like this:
```@example anatomy
-function LearnAPI.obsfit(algorithm::Ridge, obsdata::RidgeFitData, verbosity)
+function LearnAPI.fit(algorithm::Ridge, data; verbosity=1)
+
+ X, y = data
+
+ # data preprocessing:
+ table = Tables.columntable(X)
+ names = Tables.columnnames(table) |> collect
+ A = Tables.matrix(table, transpose=true)
lambda = algorithm.lambda
- A = obsdata.A
- names = obsdata.names
- y = obsdata.y
# apply core algorithm:
- coefficients = (A*A' + algorithm.lambda*I)\(A*y) # 1 x p matrix
+ coefficients = (A*A' + algorithm.lambda*I)\(A*y) # vector
# determine named coefficients:
named_coefficients = [names[j] => coefficients[j] for j in eachindex(names)]
@@ -145,63 +121,64 @@ function LearnAPI.obsfit(algorithm::Ridge, obsdata::RidgeFitData, verbosity)
verbosity > 0 && @info "Coefficients: $named_coefficients"
return RidgeFitted(algorithm, coefficients, named_coefficients)
-
end
-nothing # hide
```
-Users set `verbosity=0` for warnings only, and `verbosity=-1` for silence.
-
## Implementing `predict`
-The primary `predict` call will look like this:
+Users will be able to call `predict` like this:
```julia
-predict(model, LiteralTarget(), Xnew)
+predict(model, Point(), Xnew)
```
-where `Xnew` is a table (of the same form as `X` above). The argument `LiteralTarget()`
-signals that we want literal predictions of the target variable, as opposed to a proxy for
-the target, such as probability density functions. `LiteralTarget` is an example of a
-[`LearnAPI.KindOfProxy`](@ref proxy_types) type. Targets and target proxies are defined
-[here](@ref proxy).
+where `Xnew` is a table (of the same form as `X` above). The argument `Point()`
+signals that literal predictions of the target variable are sought, as opposed to some
+proxy for the target, such as probability density functions. `Point` is an
+example of a [`LearnAPI.KindOfProxy`](@ref proxy_types) type. Targets and target proxies
+are discussed [here](@ref proxy).
-Rather than overload the primary signature above, however, we overload for
-"observation-accessible" input, as we did for `fit`,
+We provide this implementation for our ridge regressor:
```@example anatomy
-LearnAPI.obspredict(model::RidgeFitted, ::LiteralTarget, Anew::Matrix) =
- ((model.coefficients)'*Anew)'
-nothing # hide
+LearnAPI.predict(model::RidgeFitted, ::Point, Xnew) =
+ Tables.matrix(Xnew)*model.coefficients
```
-and overload `obs` to make the table-to-matrix conversion:
+If the kind of proxy is omitted, as in `predict(model, Xnew)`, then a fallback grabs the
+first element of the tuple returned by [`LearnAPI.kinds_of_proxy(algorithm)`](@ref), which
+we overload appropriately below.
+
+
+## Extracting the target from training data
+
+The `fit` method consumes data which includes a [target variable](@ref proxy), i.e., the
+algorithm is a supervised learner. We must therefore declare how the target variable can be extracted
+from training data, by implementing [`LearnAPI.target`](@ref):
```@example anatomy
-LearnAPI.obs(::typeof(predict), ::Ridge, Xnew) = Tables.matrix(Xnew, transpose=true)
+LearnAPI.target(algorithm, data) = last(data)
```
-As matrices (with observations as columns) already implement the MLUtils.jl
-`getobs`/`numobs` interface, we already satisfy the [`obs`](@ref) contract, and there was
-no need to create a wrapper for `obs` output.
-
-The primary `predict` method, handling tabular input, is provided by a
-LearnAPI.jl fallback similar to the `fit` fallback.
+There is a similar method, [`LearnAPI.features`](@ref) for declaring how training features
+can be extracted (for passing to `predict`, for example) but this method has a fallback
+which typically suffices: return `first(data)` if `data` is a tuple, and otherwise return
+`data`.
## Accessor functions
-An [accessor function](@ref accessor_functions) has the output of [`fit`](@ref) (a
-"model") as it's sole argument. Every new implementation must implement the accessor
-function [`LearnAPI.algorithm`](@ref) for recovering an algorithm from a fitted object:
+An [accessor function](@ref accessor_functions) has the output of [`fit`](@ref) as it's
+sole argument. Every new implementation must implement the accessor function
+[`LearnAPI.algorithm`](@ref) for recovering an algorithm from a fitted object:
```@example anatomy
LearnAPI.algorithm(model::RidgeFitted) = model.algorithm
```
Other accessor functions extract learned parameters or some standard byproducts of
-training, such as feature importances or training losses.² Implementing the
-[`LearnAPI.coefficients`](@ref) accessor function is straightforward:
+training, such as feature importances or training losses.² Here we implement an accessor
+function to extract the linear coefficients:
```@example anatomy
LearnAPI.coefficients(model::RidgeFitted) = model.named_coefficients
@@ -218,53 +195,59 @@ LearnAPI.minimize(model::RidgeFitted) =
RidgeFitted(model.algorithm, model.coefficients, nothing)
```
+Crucially, we can still use `LearnAPI.minimize(model)` in place of `model` to make new
+predictions.
+
+
## Algorithm traits
Algorithm [traits](@ref traits) record extra generic information about an algorithm, or
-make specific promises of behavior. They usually have an algorithm as the single argument.
+make specific promises of behavior. They usually have an algorithm as the single argument,
+and so we regard [`LearnAPI.constructor`](@ref) defined above as a trait.
-In LearnAPI.jl `predict` always outputs a [target or target proxy](@ref proxy), where
-"target" is understood very broadly. We overload a trait to record the fact that the
-target variable explicitly appears in training (i.e, the algorithm is supervised) and
-where exactly it appears:
+Because we have implemented `predict`, we are required to overload the
+[`LearnAPI.kinds_of_proxy`](@ref) trait. Because we can only make point predictions of the
+target, we make this definition:
```julia
-LearnAPI.position_of_target(::Ridge) = 2
+LearnAPI.kinds_of_proxy(::Ridge) = (Point(),)
```
-Or, you can use the shorthand
-```julia
-@trait Ridge position_of_target = 2
-```
-
-The macro can also be used to specify multiple traits simultaneously:
+A macro provides a shortcut, convenient when multiple traits are to be defined:
```@example anatomy
@trait(
Ridge,
- position_of_target = 2,
- kinds_of_proxy=(LiteralTarget(),),
- descriptors = (:regression,),
+ constructor = Ridge,
+ kinds_of_proxy=(Point(),),
+ tags = (:regression,),
functions = (
- fit,
- obsfit,
- minimize,
- predict,
- obspredict,
- obs,
- LearnAPI.algorithm,
- LearnAPI.coefficients,
- )
+ :(LearnAPI.fit),
+ :(LearnAPI.algorithm),
+ :(LearnAPI.minimize),
+ :(LearnAPI.obs),
+ :(LearnAPI.features),
+ :(LearnAPI.target),
+ :(LearnAPI.predict),
+ :(LearnAPI.coefficients),
+ )
)
nothing # hide
```
-Implementing the last trait, [`LearnAPI.functions`](@ref), which must include all
-non-trait functions overloaded for `Ridge`, is compulsory. This is the only universally
-compulsory trait. It is worthwhile studying the [list of all traits](@ref traits_list) to
-see which might apply to a new implementation, to enable maximum buy into functionality
-provided by third party packages, and to assist third party algorithms that match machine
-learning algorithms to user defined tasks.
+The last trait, `functions`, returns a list of all LearnAPI.jl methods that can be
+meaninfully applied to the algorithm or associated model. See [`LearnAPI.functions`](@ref)
+for a checklist. [`LearnAPI.functions`](@ref) and [`LearnAPI.constructor`](@ref), are the
+only universally compulsory traits. However, it is worthwhile studying the [list of all
+traits](@ref traits_list) to see which might apply to a new implementation, to enable
+maximum buy into functionality provided by third party packages, and to assist third party
+algorithms that match machine learning algorithms to user-defined tasks.
+
+Note that we know `Ridge` instances are supervised algorithms because `:(LearnAPI.target)
+in LearnAPI.functions(algorithm)`, for every instance `algorithm`. With [some
+exceptions](@ref trait_contract), the value of a trait should depend only on the *type* of
+the argument.
+
## [Demonstration](@id workflow)
@@ -279,38 +262,23 @@ test = 7:10
a, b, c = rand(n), rand(n), rand(n)
X = (; a, b, c)
y = 2a - b + 3c + 0.05*rand(n)
-
-algorithm = Ridge(lambda=0.5)
-LearnAPI.functions(algorithm)
+nothing # hide
```
-### Naive user workflow
-
-Training and predicting with external resampling:
-
```@example anatomy
-using Tables
-model = fit(algorithm, Tables.subset(X, train), y[train])
-ŷ = predict(model, LiteralTarget(), Tables.subset(X, test))
+algorithm = Ridge(lambda=0.5)
+foreach(println, LearnAPI.functions(algorithm))
```
-### Advanced workflow
-
-We now train and predict using internal data representations, resampled using the generic
-MLUtils.jl interface.
+Training and predicting:
```@example anatomy
-import MLUtils
-fit_data = obs(fit, algorithm, X, y)
-predict_data = obs(predict, algorithm, X)
-model = obsfit(algorithm, MLUtils.getobs(fit_data, train))
-ẑ = obspredict(model, LiteralTarget(), MLUtils.getobs(predict_data, test))
-@assert ẑ == ŷ
-nothing # hide
+Xtrain = Tables.subset(X, train)
+ytrain = y[train]
+model = fit(algorithm, (Xtrain, ytrain)) # `fit(algorithm, Xtrain, ytrain)` will also work
+ŷ = predict(model, Tables.subset(X, test))
```
-### Applying an accessor function and serialization
-
Extracting coefficients:
```@example anatomy
@@ -319,21 +287,221 @@ LearnAPI.coefficients(model)
Serialization/deserialization:
-```julia
+```@example anatomy
using Serialization
small_model = minimize(model)
-serialize("my_ridge.jls", small_model)
+filename = tempname()
+serialize(filename, small_model)
+```
-recovered_model = deserialize("my_ridge.jls")
+```julia
+recovered_model = deserialize(filename)
@assert LearnAPI.algorithm(recovered_model) == algorithm
-predict(recovered_model, LiteralTarget(), X) == predict(model, LiteralTarget(), X)
+@assert predict(recovered_model, X) == predict(model, X)
```
+## Providing an advanced data interface
+
+```@setup anatomy2
+using LearnAPI
+using LinearAlgebra, Tables
+
+struct Ridge{T<:Real}
+ lambda::T
+end
+
+Ridge(; lambda=0.1) = Ridge(lambda)
+
+struct RidgeFitted{T,F}
+ algorithm::Ridge
+ coefficients::Vector{T}
+ named_coefficients::F
+end
+
+LearnAPI.algorithm(model::RidgeFitted) = model.algorithm
+LearnAPI.coefficients(model::RidgeFitted) = model.named_coefficients
+LearnAPI.minimize(model::RidgeFitted) =
+ RidgeFitted(model.algorithm, model.coefficients, nothing)
+
+@trait(
+ Ridge,
+ constructor = Ridge,
+ kinds_of_proxy=(Point(),),
+ tags = (:regression,),
+ functions = (
+ :(LearnAPI.fit),
+ :(LearnAPI.algorithm),
+ :(LearnAPI.minimize),
+ :(LearnAPI.obs),
+ :(LearnAPI.features),
+ :(LearnAPI.target),
+ :(LearnAPI.predict),
+ :(LearnAPI.coefficients),
+ )
+)
+
+n = 10 # number of observations
+train = 1:6
+test = 7:10
+a, b, c = rand(n), rand(n), rand(n)
+X = (; a, b, c)
+y = 2a - b + 3c + 0.05*rand(n)
+```
+
+An implementation may optionally implement [`obs`](@ref), to expose to the user (or some
+meta-algorithm like cross-validation) the representation of input data internal to `fit`
+or `predict`, such as the matrix version `A` of `X` in the ridge example. Here we
+specifically wrap all the pre-processed data into single object, for which we introduce a
+new type:
+
+```@example anatomy2
+struct RidgeFitObs{T,M<:AbstractMatrix{T}}
+ A::M # `p` x `n` matrix
+ names::Vector{Symbol} # features
+ y::Vector{T} # target
+end
+```
+
+Now we overload `obs` to carry out the data pre-processing previously in `fit`, like this:
+
+```@example anatomy2
+function LearnAPI.obs(::Ridge, data)
+ X, y = data
+ table = Tables.columntable(X)
+ names = Tables.columnnames(table) |> collect
+ return RidgeFitObs(Tables.matrix(table)', names, y)
+end
+```
+
+We informally refer to the output of `obs` as "observations" (see [The `obs`
+contract](@ref) below). The previous core `fit` signature is now replaced with two
+methods - one to handle "regular" input, and one to handle the pre-processed data
+(observations) which appears first below:
+
+```@example anatomy2
+function LearnAPI.fit(algorithm::Ridge, observations::RidgeFitObs; verbosity=1)
+
+ lambda = algorithm.lambda
+
+ A = observations.A
+ names = observations.names
+ y = observations.y
+
+ # apply core algorithm:
+ coefficients = (A*A' + algorithm.lambda*I)\(A*y) # 1 x p matrix
+
+ # determine named coefficients:
+ named_coefficients = [names[j] => coefficients[j] for j in eachindex(names)]
+
+ # make some noise, if allowed:
+ verbosity > 0 && @info "Coefficients: $named_coefficients"
+
+ return RidgeFitted(algorithm, coefficients, named_coefficients)
+
+end
+
+LearnAPI.fit(algorithm::Ridge, data; kwargs...) =
+ fit(algorithm, obs(algorithm, data); kwargs...)
+```
+
+### The `obs` contract
+
+Providing `fit` signatures matching the output of `obs`, is the first part of the `obs`
+contract. The second part is this: *The output of `obs` must implement the interface
+specified by the trait* [`LearnAPI.data_interface(algorithm)`](@ref). Assuming this is
+[`LearnAPI.RandomAccess()`](@ref) (the default) it usually suffices to overload
+`Base.getindex` and `Base.length`:
+
+```@example anatomy2
+Base.getindex(data::RidgeFitObs, I) =
+ RidgeFitObs(data.A[:,I], data.names, y[I])
+Base.length(data::RidgeFitObs, I) = length(data.y)
+```
+
+We can do something similar for `predict`, but there's no need for a new type in this
+case:
+
+```@example anatomy2
+LearnAPI.obs(::RidgeFitted, Xnew) = Tables.matrix(Xnew)'
+
+LearnAPI.predict(model::RidgeFitted, ::Point, observations::AbstractMatrix) =
+ observations'*model.coefficients
+
+LearnAPI.predict(model::RidgeFitted, ::Point, Xnew) =
+ predict(model, Point(), obs(model, Xnew))
+```
+
+### `target` and `features` methods
+
+We provide an additional overloading of [`LearnAPI.target`](@ref) to handle the additional
+supported data argument of `fit`:
+
+```@example anatomy2
+LearnAPI.target(::Ridge, observations::RidgeFitObs) = observations.y
+```
+
+Similarly, we must overload [`LearnAPI.features`](@ref), which extracts features from
+training data (objects that can be passed to `predict`) like this
+
+```@example anatomy2
+LearnAPI.features(::Ridge, observations::RidgeFitObs) = observations.A
+```
+as the fallback mentioned above is no longer adequate.
+
+
+### Important notes:
+
+- The observations to be consumed by `fit` are returned by `obs(algorithm::Ridge, ...)`,
+ while those consumed by `predict` are returned by `obs(model::RidgeFitted, ...)`. We
+ need the different signatures because the form of data consumed by `fit` and `predict`
+ are generally different.
+
+- We need the adjoint operator, `'`, because the last dimension in arrays is the
+ observation dimension, according to the MLUtils.jl convention. Remember, `Xnew` is a
+ table here.
+
+Since LearnAPI.jl provides fallbacks for `obs` that simply return the unadulterated data
+argument, overloading `obs` is optional. This is provided data in publicized
+`fit`/`predict` signatures consists only of objects implement the
+[`LearnAPI.RandomAccess`](@ref) interface (most tables¹, arrays³, and tuples thereof).
+
+To buy out of supporting the MLUtils.jl interface altogether, an implementation must
+overload the trait, [`LearnAPI.data_interface(algorithm)`](@ref).
+
+For more on data interfaces, see [`obs`](@ref) and
+[`LearnAPI.data_interface(algorithm)`](@ref).
+
+
+## Demonstration of an advanced `obs` workflow
+
+We now can train and predict using internal data representations, resampled using the
+generic MLUtils.jl interface:
+
+```@example anatomy2
+import MLUtils
+algorithm = Ridge()
+observations_for_fit = obs(algorithm, (X, y))
+model = fit(algorithm, MLUtils.getobs(observations_for_fit, train))
+observations_for_predict = obs(model, X)
+ẑ = predict(model, MLUtils.getobs(observations_for_predict, test))
+```
+
+```julia
+@assert ẑ == ŷ
+```
+
+For an application of [`obs`](@ref) to efficient cross-validation, see [here](@ref
+obs_workflows).
+
---
-¹ The definition of this and other structs above is not an explicit requirement of
-LearnAPI.jl, whose constructs are purely functional.
+¹ In LearnAPI.jl a *table* is any object `X` implementing the
+[Tables.jl](https://tables.juliadata.org/dev/) interface, additionally satisfying
+`Tables.istable(X) == true` and implementing `DataAPI.nrow` (and whence
+`MLUtils.numobs`). Tables that are also (unnamed) tuples are disallowed.
² An implementation can provide further accessor functions, if necessary, but
like the native ones, they must be included in the [`LearnAPI.functions`](@ref)
declaration.
+
+³ The last index must be the observation index.
diff --git a/docs/src/fit.md b/docs/src/fit.md
deleted file mode 100644
index f2709611..00000000
--- a/docs/src/fit.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# [`fit`](@ref fit)
-
-```julia
-fit(algorithm, data...; verbosity=1) -> model
-fit(model, data...; verbosity=1) -> updated_model
-```
-
-## Typical workflow
-
-```julia
-# Train some supervised `algorithm`:
-model = fit(algorithm, X, y)
-
-# Predict probability distributions:
-ŷ = predict(model, Distribution(), Xnew)
-
-# Inspect some byproducts of training:
-LearnAPI.feature_importances(model)
-```
-
-## Implementation guide
-
-The `fit` method is not implemented directly. Instead, implement [`obsfit`](@ref).
-
-| method | fallback | compulsory? | requires |
-|:-----------------------------|:---------|-------------|-----------------------------|
-| [`obsfit`](@ref)`(alg, ...)` | none | yes | [`obs`](@ref) in some cases |
-| | | | |
-
-
-## Reference
-
-```@docs
-LearnAPI.fit
-LearnAPI.obsfit
-```
diff --git a/docs/src/fit_update.md b/docs/src/fit_update.md
new file mode 100644
index 00000000..c512be9c
--- /dev/null
+++ b/docs/src/fit_update.md
@@ -0,0 +1,87 @@
+# [`fit`, `update`, `update_observations`, and `update_features`](@id fit)
+
+### Training
+
+```julia
+fit(algorithm, data; verbosity=1) -> model
+fit(algorithm; verbosity=1) -> static_model
+```
+
+A "static" algorithm is one that does not generalize to new observations (e.g., some
+clustering algorithms); there is no trainiing data and the algorithm is executed by
+`predict` or `transform` which receive the data. See example below.
+
+When `fit` expects a tuple form of argument, `data = (X1, ..., Xn)`, then the signature
+`fit(algorithm, X1, ..., Xn)` is also provided.
+
+### Updating
+
+```
+update(model, data; verbosity=1, param1=new_value1, param2=new_value2, ...) -> updated_model
+update_observations(model, new_data; verbosity=1, param1=new_value1, ...) -> updated_model
+update_features(model, new_data; verbosity=1, param1=new_value1, ...) -> updated_model
+```
+
+Data slurping forms are similarly provided for updating methods.
+
+## Typical workflows
+
+Supposing `Algorithm` is some supervised classifier type, with an iteration parameter `n`:
+
+```julia
+algorithm = Algorithm(n=100)
+model = fit(algorithm, (X, y)) # or `fit(algorithm, X, y)`
+
+# Predict probability distributions:
+ŷ = predict(model, Distribution(), Xnew)
+
+# Inspect some byproducts of training:
+LearnAPI.feature_importances(model)
+
+# Add 50 iterations and predict again:
+model = update(model; n=150)
+predict(model, Distribution(), X)
+```
+
+### A static algorithm (no "learning")
+
+```julia
+# Apply some clustering algorithm which cannot be generalized to new data:
+model = fit(algorithm) # no training data
+labels = predict(model, LabelAmbiguous(), X) # may mutate `model`
+
+# Or, in one line:
+labels = predict(algorithm, LabelAmbiguous(), X)
+
+# But two-line version exposes byproducts of the clustering algorithm (e.g., outliers):
+LearnAPI.extras(model)
+```
+
+## Implementation guide
+
+### Training
+
+| method | fallback | compulsory? |
+|:-------------------------------------------------------------------------------|:-----------------------------------------------------------------|--------------------|
+| [`fit`](@ref)`(algorithm, data; verbosity=1)` | ignores `data` and applies signature below | yes, unless static |
+| [`fit`](@ref)`(algorithm; verbosity=1)` | none | no, unless static |
+
+### Updating
+
+| method | fallback | compulsory? |
+|:-------------------------------------------------------------------------------------|:---------|-------------|
+| [`update`](@ref)`(model, data; verbosity=1, hyperparameter_updates...)` | none | no |
+| [`update_observations`](@ref)`(model, data; verbosity=1, hyperparameter_updates...)` | none | no |
+| [`update_features`](@ref)`(model, data; verbosity=1, hyperparameter_updates...)` | none | no |
+
+There are some contracts regarding the behaviour of the update methods, as they relate to
+a previous `fit` call. Consult the document strings for details.
+
+## Reference
+
+```@docs
+fit
+update
+update_observations
+update_features
+```
diff --git a/docs/src/index.md b/docs/src/index.md
index f5c793f7..7b638aed 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -9,12 +9,14 @@ A base Julia interface for machine learning and statistics
```
-LearnAPI.jl is a lightweight, functional-style interface, providing a collection of
-[methods](@ref Methods), such as `fit` and `predict`, to be implemented by algorithms from
-machine learning and statistics. Through such implementations, these algorithms buy into
-functionality, such as hyperparameter optimization, as provided by ML/statistics toolboxes
-and other packages. LearnAPI.jl also provides a number of Julia [traits](@ref traits) for
-promising specific behavior.
+LearnAPI.jl is a lightweight, functional-style interface, providing a
+collection of [methods](@ref Methods), such as `fit` and `predict`, to be implemented by
+algorithms from machine learning and statistics. Through such implementations, these
+algorithms buy into functionality, such as hyperparameter optimization and model
+composition, as provided by ML/statistics toolboxes and other packages. LearnAPI.jl also
+provides a number of Julia [traits](@ref traits) for promising specific behavior.
+
+LearnAPI.jl has no package dependencies.
```@raw html
🚧
@@ -22,33 +24,37 @@ promising specific behavior.
!!! warning
- The API described here is under active development and not ready for adoption.
- Join an ongoing design discussion at
- [this](https://discourse.julialang.org/t/ann-learnapi-jl-proposal-for-a-basement-level-machine-learning-api/93048)
+ The API described here is under active development and not ready for adoption.
+ Join an ongoing design discussion at
+ [this](https://discourse.julialang.org/t/ann-learnapi-jl-proposal-for-a-basement-level-machine-learning-api/93048)
Julia Discourse thread.
-
+
## Sample workflow
Suppose `forest` is some object encapsulating the hyperparameters of the [random forest
-algorithm](https://en.wikipedia.org/wiki/Random_forest) (the number of trees,
-etc.). Then, a LearnAPI.jl interface can be implemented, for objects with the type of
-`forest`, to enable the following basic workflow:
+algorithm](https://en.wikipedia.org/wiki/Random_forest) (the number of trees, etc.). Then,
+a LearnAPI.jl interface can be implemented, for objects with the type of `forest`, to
+enable the basic workflow below. In this case data is presented following the
+"scikit-learn" `X, y` pattern, although LearnAPI.jl supports other patterns as well.
```julia
X =
y =
Xnew =
+# List LearnaAPI functions implemented for `forest`:
+LearnAPI.functions(forest)
+
# Train:
model = fit(forest, X, y)
+# Generate point predictions:
+ŷ = predict(model, Xnew) # or `predict(model, Point(), Xnew)`
+
# Predict probability distributions:
predict(model, Distribution(), Xnew)
-# Generate point predictions:
-ŷ = predict(model, LiteralTarget(), Xnew) # or `predict(model, Xnew)`
-
# Apply an "accessor function" to inspect byproducts of training:
LearnAPI.feature_importances(model)
@@ -59,22 +65,31 @@ serialize("my_random_forest.jls", small_model)
# Recover saved model and algorithm configuration:
recovered_model = deserialize("my_random_forest.jls")
@assert LearnAPI.algorithm(recovered_model) == forest
-@assert predict(recovered_model, LiteralTarget(), Xnew) == ŷ
+@assert predict(recovered_model, Point(), Xnew) == ŷ
```
-`Distribution` and `LiteralTarget` are singleton types owned by LearnAPI.jl. They allow
+`Distribution` and `Point` are singleton types owned by LearnAPI.jl. They allow
dispatch based on the [kind of target proxy](@ref proxy), a key LearnAPI.jl concept.
LearnAPI.jl places more emphasis on the notion of target variables and target proxies than
on the usual supervised/unsupervised learning dichotomy. From this point of view, a
supervised algorithm is simply one in which a target variable exists, and happens to
appear as an input to training but not to prediction.
-In LearnAPI.jl, a method called [`obs`](@ref data_interface) gives users access to an
-"internal", algorithm-specific, representation of input data, which is always
-"observation-accessible", in the sense that it can be resampled using
-[MLUtils.jl](https://github.com/JuliaML/MLUtils.jl) `getobs/numobs` interface. The
-implementation can arrange for this resampling to be efficient, and workflows based on
-`obs` can have performance benefits.
+## Data interfaces
+
+Algorithms are free to consume data in any format. However, a method called [`obs`](@ref
+data_interface) (read as "observations") gives users and meta-algorithms access to an
+algorithm-specific representation of input data, which is also guaranteed to implement a
+standard interface for accessing individual observations, unless the algorithm explicitly
+opts out. Moreover, the `fit` and `predict` methods will also be able to consume these
+alternative data representations, for performance benefits in some situations.
+
+The fallback data interface is the [MLUtils.jl](https://github.com/JuliaML/MLUtils.jl)
+`getobs/numobs` interface (here tagged as [`LearnAPI.RandomAccess()`](@ref)) and if the
+input consumed by the algorithm already implements that interface (tables, arrays, etc.)
+then overloading `obs` is completely optional. Plain iteration interfaces, with or without
+knowledge of the number of observations, can also be specified (to support, e.g., data
+loaders reading images from disk).
## Learning more
diff --git a/docs/src/kinds_of_target_proxy.md b/docs/src/kinds_of_target_proxy.md
index 03c7e032..da150f96 100644
--- a/docs/src/kinds_of_target_proxy.md
+++ b/docs/src/kinds_of_target_proxy.md
@@ -1,55 +1,27 @@
# [Kinds of Target Proxy](@id proxy_types)
-The available kinds of [target proxy](@ref proxy) are classified by subtypes of
-`LearnAPI.KindOfProxy`. These types are intended for dispatch only and have no fields.
+The available kinds of [target proxy](@ref proxy) (used for `predict` dispatch) are
+classified by subtypes of `LearnAPI.KindOfProxy`. These types are intended for dispatch
+only and have no fields.
```@docs
LearnAPI.KindOfProxy
```
+
+## Simple target proxies
+
```@docs
LearnAPI.IID
```
-## Simple target proxies (subtypes of `LearnAPI.IID`)
-
-| type | form of an observation |
-|:-------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `LearnAPI.LiteralTarget` | same as target observations |
-| `LearnAPI.Sampleable` | object that can be sampled to obtain object of the same form as target observation |
-| `LearnAPI.Distribution` | explicit probability density/mass function whose sample space is all possible target observations |
-| `LearnAPI.LogDistribution` | explicit log-probability density/mass function whose sample space is possible target observations |
-| † `LearnAPI.Probability` | numerical probability or probability vector |
-| † `LearnAPI.LogProbability` | log-probability or log-probability vector |
-| † `LearnAPI.Parametric` | a list of parameters (e.g., mean and variance) describing some distribution |
-| `LearnAPI.LabelAmbiguous` | collections of labels (in case of multi-class target) but without a known correspondence to the original target labels (and of possibly different number) as in, e.g., clustering |
-| `LearnAPI.LabelAmbiguousSampleable` | sampleable version of `LabelAmbiguous`; see `Sampleable` above |
-| `LearnAPI.LabelAmbiguousDistribution` | pdf/pmf version of `LabelAmbiguous`; see `Distribution` above |
-| `LearnAPI.ConfidenceInterval` | confidence interval |
-| `LearnAPI.Set` | finite but possibly varying number of target observations |
-| `LearnAPI.ProbabilisticSet` | as for `Set` but labeled with probabilities (not necessarily summing to one) |
-| `LearnAPI.SurvivalFunction` | survival function |
-| `LearnAPI.SurvivalDistribution` | probability distribution for survival time |
-| `LearnAPI.OutlierScore` | numerical score reflecting degree of outlierness (not necessarily normalized) |
-| `LearnAPI.Continuous` | real-valued approximation/interpolation of a discrete-valued target, such as a count (e.g., number of phone calls) |
-
-† Provided for completeness but discouraged to avoid [ambiguities in
-representation](https://github.com/alan-turing-institute/MLJ.jl/blob/dev/paper/paper.md#a-unified-approach-to-probabilistic-predictions-and-their-evaluation).
-
-> Table of concrete subtypes of `LearnAPI.IID <: LearnAPI.KindOfProxy`.
-
-
-## When the proxy for the target is a single object
-
-In the following table of subtypes `T <: LearnAPI.KindOfProxy` not falling under the `IID`
-umbrella, it is understood that `predict(model, ::T, ...)` is
-not divided into individual observations, but represents a *single* probability
-distribution for the sample space ``Y^n``, where ``Y`` is the space the target variable
-takes its values, and `n` is the number of observations in `data`.
-
-| type `T` | form of output of `predict(model, ::T, data...)` |
-|:-------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `LearnAPI.JointSampleable` | object that can be sampled to obtain a *vector* whose elements have the form of target observations; the vector length matches the number of observations in `data`. |
-| `LearnAPI.JointDistribution` | explicit probability density/mass function whose sample space is vectors of target observations; the vector length matches the number of observations in `data` |
-| `LearnAPI.JointLogDistribution` | explicit log-probability density/mass function whose sample space is vectors of target observations; the vector length matches the number of observations in `data` |
-
-> Table of `LearnAPI.KindOfProxy` subtypes not subtyping `LearnAPI.IID`
+## Proxies for density estimation lgorithms
+
+```@docs
+LearnAPI.Single
+```
+
+## Joint probability distributions
+
+```@docs
+LearnAPI.Joint
+```
diff --git a/docs/src/minimize.md b/docs/src/minimize.md
index a9423780..03bc028e 100644
--- a/docs/src/minimize.md
+++ b/docs/src/minimize.md
@@ -7,15 +7,15 @@ minimize(model) ->
# Typical workflow
```julia
-model = fit(algorithm, X, y)
-ŷ = predict(model, LiteralTarget(), Xnew)
+model = fit(algorithm, (X, y)) # or `fit(algorithm, X, y)`
+ŷ = predict(model, Point(), Xnew)
LearnAPI.feature_importances(model)
small_model = minimize(model)
serialize("my_model.jls", small_model)
recovered_model = deserialize("my_random_forest.jls")
-@assert predict(recovered_model, LiteralTarget(), Xnew) == ŷ
+@assert predict(recovered_model, Point(), Xnew) == ŷ
# throws MethodError:
LearnAPI.feature_importances(recovered_model)
@@ -23,9 +23,9 @@ LearnAPI.feature_importances(recovered_model)
# Implementation guide
-| method | compulsory? | fallback | requires |
-|:-----------------------------|:-----------:|:--------:|:-------------:|
-| [`minimize`](@ref) | no | identity | [`fit`](@ref) |
+| method | compulsory? | fallback |
+|:-----------------------------|:-----------:|:--------:|
+| [`minimize`](@ref) | no | identity |
# Reference
diff --git a/docs/src/obs.md b/docs/src/obs.md
index bfb35a69..cf794d87 100644
--- a/docs/src/obs.md
+++ b/docs/src/obs.md
@@ -1,97 +1,89 @@
-# [`obs`](@id data_interface)
+# [`obs` and Data Interfaces](@id data_interface)
-The [MLUtils.jl](https://github.com/JuliaML/MLUtils.jl) package provides two methods
-`getobs` and `numobs` for resampling data divided into multiple observations, including
-arrays and tables. The data objects returned below are guaranteed to implement this
-interface and can be passed to the relevant method (`obsfit`, `obspredict` or
-`obstransform`) possibly after resampling using `MLUtils.getobs`. This may provide
-performance advantages over naive workflows.
+The `obs` method takes data intended as input to `fit`, `predict` or `transform`, and
+transforms it to an algorithm-specific form guaranteed to implement a form of observation
+access designated by the algorithm. The transformed data can then be resampled and passed
+on to the relevant method in place of the original input. Using `obs` may provide
+performance advantages over naive workflows in some cases (e.g., cross-validation).
```julia
-obs(fit, algorithm, data...) ->
-obs(predict, algorithm, data...) ->
-obs(transform, algorithm, data...) ->
+obs(algorithm, data) # can be passed to `fit` instead of `data`
+obs(model, data) # can be passed to `predict` or `transform` instead of `data`
```
-## Typical workflows
+## [Typical workflows](@id obs_workflows)
-LearnAPI.jl makes no assumptions about the form of data `X` and `y` in a call like
-`fit(algorithm, X, y)`. The particular `algorithm` is free to articulate it's own
-requirements. However, in this example, the definition
+LearnAPI.jl makes no universal assumptions about the form of `data` in a call
+like `fit(algorithm, data)`. However, if we define
```julia
-obsdata = obs(fit, algorithm, X, y)
+observations = obs(algorithm, data)
```
-combines `X` and `y` in a single object guaranteed to implement the MLUtils.jl
-`getobs`/`numobs` interface, which can be passed to `obsfit` instead of `fit`, as is, or
-after resampling using `MLUtils.getobs`:
+then, assuming the typical case that `LearnAPI.data_interface(algorithm) ==
+LearnAPI.RandomAccess()`, `observations` implements the
+[MLUtils.jl](https://juliaml.github.io/MLUtils.jl/dev/) `getobs`/`numobs` interface, for
+grabbing and counting observations. Moreover, we can pass `observations` to `fit` in place
+of the original data, or first resample it using `MLUtils.getobs`:
```julia
-# equivalent to `mode = fit(algorithm, X, y)`:
-model = obsfit(algorithm, obsdata)
+# equivalent to `model = fit(algorithm, data)`
+model = fit(algorithm, observations)
# with resampling:
-resampled_obsdata = MLUtils.getobs(obsdata, 1:100)
-model = obsfit(algorithm, resampled_obsdata)
+resampled_observations = MLUtils.getobs(observations, 1:10)
+model = fit(algorithm, resampled_observations)
```
In some implementations, the alternative pattern above can be used to avoid repeating
unnecessary internal data preprocessing, or inefficient resampling. For example, here's
-how a user might call `obs` and `MLUtils.getobs` to perform efficient
-cross-validation:
+how a user might call `obs` and `MLUtils.getobs` to perform efficient cross-validation:
```julia
using LearnAPI
import MLUtils
-X =
-y =
-algorithm =
+algorithm =
-test_train_folds = map([1:10, 11:20, 21:30]) do test
- (test, setdiff(1:30, test))
-end
+data =
+X = LearnAPI.features(algorithm, data)
+y = LearnAPI.target(algorithm, data)
-# create fixed model-specific representations of the whole data set:
-fit_data = obs(fit, algorithm, X, y)
-predict_data = obs(predict, algorithm, predict, X)
+train_test_folds = map([1:10, 11:20, 21:30]) do test
+ (setdiff(1:30, test), test)
+end
-scores = map(train_test_folds) do (train_indices, test_indices)
-
- # train using model-specific representation of data:
- train_data = MLUtils.getobs(fit_data, train_indices)
- model = obsfit(algorithm, train_data)
-
- # predict on the fold complement:
- test_data = MLUtils.getobs(predict_data, test_indices)
- ŷ = obspredict(model, LiteralTarget(), test_data)
+fitobs = obs(algorithm, data)
+never_trained = true
- return
-
-end
-```
+scores = map(train_test_folds) do (train, test)
-Note here that the output of `obspredict` will match the representation of `y` , i.e.,
-there is no concept of an algorithm-specific representation of *outputs*, only inputs.
+ # train using model-specific representation of data:
+ fitobs_subset = MLUtils.getobs(fitobs, train)
+ model = fit(algorithm, fitobs_subset)
+ # predict on the fold complement:
+ if never_trained
+ global predictobs = obs(model, X)
+ global never_trained = false
+ end
+ predictobs_subset = MLUtils.getobs(predictobs, test)
+ ŷ = predict(model, Point(), predictobs_subset)
+
+ return
+
+end
+```
## Implementation guide
-| method | compulsory? | fallback |
-|:--------------|:-----------:|:----------------------:|
-| [`obs`](@ref) | depends | slurps `data` argument |
-| | | |
+| method | compulsory? | fallback |
+|:----------------------------------------|:-----------:|:--------------:|
+| [`obs(algorithm_or_model, data)`](@ref) | depends | returns `data` |
+| | | |
-If the `data` consumed by `fit`, `predict` or `transform` consists only of tables and
-arrays (with last dimension the observation dimension) then overloading `obs` is
-optional. However, if an implementation overloads `obs` to return a (thinly wrapped)
-representation of user data that is closer to what the core algorithm actually uses, and
-overloads `MLUtils.getobs` (or, more typically `Base.getindex`) to make resampling of that
-representation efficient, then those optimizations become available to the user, without
-the user concerning herself with the details of the representation.
+A sample implementation is given in [Providing an advanced data interface](@ref).
-A sample implementation is given in the [`obs`](@ref) document-string below.
## Reference
@@ -99,3 +91,20 @@ A sample implementation is given in the [`obs`](@ref) document-string below.
obs
```
+### Data interfaces
+
+New implementations must overload [`LearnAPI.data_interface(algorithm)`](@ref) if the
+output of [`obs`](@ref) does not implement [`LearnAPI.RandomAccess`](@ref). (Arrays, most
+tables, and all tuples thereof, implement `RandomAccess`.)
+
+- [`LearnAPI.RandomAccess`](@ref) (default)
+- [`LearnAPI.FiniteIterable`](@ref)
+- [`LearnAPI.Iterable`](@ref)
+
+
+```@docs
+LearnAPI.RandomAccess
+LearnAPI.FiniteIterable
+LearnAPI.Iterable
+```
+
diff --git a/docs/src/predict_transform.md b/docs/src/predict_transform.md
index 382216b8..df961719 100644
--- a/docs/src/predict_transform.md
+++ b/docs/src/predict_transform.md
@@ -1,81 +1,102 @@
-# [`predict`, `transform`, and relatives](@id operations)
-
-Standard methods:
+# [`predict`, `transform` and `inverse_transform`](@id operations)
```julia
-predict(model, kind_of_proxy, data...) -> prediction
-transform(model, data...) -> transformed_data
-inverse_transform(model, data...) -> inverted_data
+predict(model, kind_of_proxy, data)
+transform(model, data)
+inverse_transform(model, data)
```
-Methods consuming output, `obsdata`, of data-preprocessor [`obs`](@ref):
+When a method expects a tuple form of argument, `data = (X1, ..., Xn)`, then a slurping
+signature is also provided, as in `transform(model, X1, ..., Xn)`.
+
+
+## [Typical worklows](@id predict_workflow)
+
+Train some supervised `algorithm`:
```julia
-obspredict(model, kind_of_proxy, obsdata) -> prediction
-obstransform(model, obsdata) -> transformed_data
+model = fit(algorithm, (X, y)) # or `fit(algorithm, X, y)`
```
-## Typical worklows
+Predict probability distributions:
```julia
-# Train some supervised `algorithm`:
-model = fit(algorithm, X, y)
-
-# Predict probability distributions:
ŷ = predict(model, Distribution(), Xnew)
+```
-# Generate point predictions:
-ŷ = predict(model, LiteralTarget(), Xnew)
+Generate point predictions:
+
+```julia
+ŷ = predict(model, Point(), Xnew)
```
+Train a dimension-reducing `algorithm`:
+
```julia
-# Training a dimension-reducing `algorithm`:
model = fit(algorithm, X)
Xnew_reduced = transform(model, Xnew)
+```
+
+Apply an approximate right inverse:
-# Apply an approximate right inverse:
+```julia
inverse_transform(model, Xnew_reduced)
```
### An advanced workflow
```julia
-fitdata = obs(fit, algorithm, X, y)
-predictdata = obs(predict, algorithm, Xnew)
-model = obsfit(algorithm, obsdata)
-ŷ = obspredict(model, LiteralTarget(), predictdata)
+fitobs = obs(algorithm, (X, y)) # algorithm-specific repr. of data
+model = fit(algorithm, MLUtils.getobs(fitobs, 1:100))
+predictobs = obs(model, MLUtils.getobs(X, 101:150))
+ŷ = predict(model, Point(), predictobs)
```
-## Implementation guide
+## [Implementation guide](@id predict_guide)
-The methods `predict` and `transform` are not directly overloaded. Implement `obspredict`
-and `obstransform` instead:
-
-| method | compulsory? | fallback | requires |
-|:----------------------------|:-----------:|:--------:|:-------------------------------------:|
-| [`obspredict`](@ref) | no | none | [`fit`](@ref) |
-| [`obstransform`](@ref) | no | none | [`fit`](@ref) |
-| [`inverse_transform`](@ref) | no | none | [`fit`](@ref), [`obstransform`](@ref) |
+| method | compulsory? | fallback |
+|:----------------------------|:-----------:|:--------:|
+| [`predict`](@ref) | no | none |
+| [`transform`](@ref) | no | none |
+| [`inverse_transform`](@ref) | no | none |
### Predict or transform?
-If the algorithm has a notion of [target variable](@ref proxy), then arrange for
-[`obspredict`](@ref) to output each supported [kind of target proxy](@ref
-proxy_types) (`LiteralTarget()`, `Distribution()`, etc).
+If the algorithm has a notion of [target variable](@ref proxy), then use
+[`predict`](@ref) to output each supported [kind of target proxy](@ref
+proxy_types) (`Point()`, `Distribution()`, etc).
-For output not associated with a target variable, implement [`obstransform`](@ref)
+For output not associated with a target variable, implement [`transform`](@ref)
instead, which does not dispatch on [`LearnAPI.KindOfProxy`](@ref), but can be optionally
-paired with an implementation of [`inverse_transform`](@ref) for returning (approximate)
+paired with an implementation of [`inverse_transform`](@ref), for returning (approximate)
right inverses to `transform`.
-## Reference
+### [One-liners combining fit and transform/predict](@id one_liners)
+
+Algorithms may optionally overload `transform` to apply `fit` first, using the supplied
+data if required, and then immediately `transform` the same data. The same applies to
+`predict`. In that case the first argument of `transform`/`predict` is an *algorithm*
+instead of the output of `fit`:
+
+```julia
+predict(algorithm, kind_of_proxy, data) # `fit` implied
+transform(algorithm, data) # `fit` implied
+```
+
+For example, if `fit(algorithm, X)` is defined, then `predict(algorithm, X)` will be
+shorthand for
+
+```julia
+model = fit(algorithm, X)
+predict(model, X)
+```
+
+## [Reference](@id predict_ref)
```@docs
predict
-obspredict
transform
-obstransform
inverse_transform
```
diff --git a/docs/src/reference.md b/docs/src/reference.md
index 5a46c6ab..698d0943 100644
--- a/docs/src/reference.md
+++ b/docs/src/reference.md
@@ -21,20 +21,25 @@ undertood that individual objects share the same number of observations, and tha
resampling of one component implies synchronized resampling of the others.
A `DataFrame` instance, from [DataFrames.jl](https://dataframes.juliadata.org/stable/), is
-an example of data, the observations being the rows. LearnAPI.jl makes no assumptions
-about how observations can be accessed, except in the case of the output of [`obs`](@ref
-data_interface), which must implement the MLUtils.jl `getobs`/`numobs` interface. For
-example, it is generally ambiguous whether the rows or columns of a matrix are considered
-observations, but if a matrix is returned by [`obs`](@ref data_interface) the observations
-must be the columns.
+an example of data, the observations being the rows. Typically, data provided to
+LearnAPI.jl algorithms, will implement the
+[MLUtils.jl](https://juliaml.github.io/MLUtils.jl/stable) `getobs/numobs` interface for
+accessing individual observations, but implementations can opt out of this requirement;
+see [`obs`](@ref) and [`LearnAPI.data_interface`](@ref) for details.
+
+!!! note
+
+ In the MLUtils.jl
+ convention, observations in tables are the rows but observations in a matrix are the
+ columns.
### [Hyperparameters](@id hyperparameters)
Besides the data it consumes, a machine learning algorithm's behavior is governed by a
number of user-specified *hyperparameters*, such as the number of trees in a random
-forest. In LearnAPI.jl, one is allowed to have hyperparematers that are not data-generic.
-For example, a class weight dictionary will only make sense for a target taking values in
-the set of dictionary keys.
+forest. In LearnAPI.jl, one is allowed to have hyperparameters that are not data-generic.
+For example, a class weight dictionary, which will only make sense for a target taking
+values in the set of dictionary keys, can be specified as a hyperparameter.
### [Targets and target proxies](@id proxy)
@@ -49,58 +54,60 @@ detection, "outlier"/"inlier" predictions, or probability-like scores, are simil
compared with ground truth labels. In clustering, integer labels assigned to observations
by the clustering algorithm can can be paired with human labels using, say, the Rand
index. In survival analysis, predicted survival functions or probability distributions are
-compared with censored ground truth survival times.
+compared with censored ground truth survival times. And so on ...
#### Definitions
-More generally, whenever we have a variable (e.g., a class label) that can (in principle)
-can be paired with a predicted value, or some predicted "proxy" for that variable (such as
-a class probability), then we call the variable a *target* variable, and the predicted
-output a *target proxy*. In this definition, it is immaterial whether or not the target
-appears in training (is supervised) or whether or not the model generalizes to new
+More generally, whenever we have a variable (e.g., a class label) that can, at least in
+principle, be paired with a predicted value, or some predicted "proxy" for that variable
+(such as a class probability), then we call the variable a *target* variable, and the
+predicted output a *target proxy*. In this definition, it is immaterial whether or not the
+target appears in training (is supervised) or whether or not the model generalizes to new
observations ("learns").
LearnAPI.jl provides singleton [target proxy types](@ref proxy_types) for prediction
-dispatch in LearnAPI.jl. These are also used to distinguish performance metrics provided
-by the package
+dispatch. These are also used to distinguish performance metrics provided by the package
[StatisticalMeasures.jl](https://juliaai.github.io/StatisticalMeasures.jl/dev/).
### [Algorithms](@id algorithms)
An object implementing the LearnAPI.jl interface is called an *algorithm*, although it is
-more accurately "the configuration of some algorithm".¹ It will have a type name
-reflecting the name of some ML/statistics algorithm (e.g., `RandomForestRegressor`) and it
-will encapsulate a particular set of user-specified [hyperparameters](@ref).
+more accurately "the configuration of some algorithm".¹ An algorithm encapsulates a
+particular set of user-specified [hyperparameters](@ref) as the object's *properties*
+(which conceivably differ from its fields). It does not store learned parameters.
-Additionally, for `alg::Alg` to be a LearnAPI algorithm, we require:
+Informally, we will sometimes use the word "model" to refer to the output of
+`fit(algorithm, ...)` (see below), something which typically does store learned
+parameters.
-- `Base.propertynames(alg)` returns the hyperparameter names; values can be accessed using
- `Base.getproperty`
+For `algorithm` to be a valid LearnAPI.jl algorithm,
+[`LearnAPI.constructor(algorithm)`](@ref) must be defined and return a keyword constructor
+enabling recovery of `algorithm` from its properties:
-- If `alg` is an algorithm, then so are all instances of the same type.
-
-- If `_alg` is another algorithm, then `alg == _alg` if and only if `typeof(alg) ==
- typeof(_alg)` and corresponding properties are `==`. This includes properties that are
- random number generators (which should be copied in training to avoid mutation).
+```julia
+properties = propertynames(algorithm)
+named_properties = NamedTuple{properties}(getproperty.(Ref(algorithm), properties))
+@assert algorithm == LearnAPI.constructor(algorithm)(; named_properties...)
+```
-- If an algorithm has other algorithms as hyperparameters, then
- [`LearnAPI.is_composite`](@ref)`(alg)` must be `true` (fallback is `false`).
+Note that if if `algorithm` is an instance of a *mutable* struct, this requirement
+generally requires overloading `Base.==` for the struct.
-- A keyword constructor for `Alg` exists, providing default values for *all* non-algorithm
- hyperparameters.
-
-- At least one non-trait LearnAPI.jl function must be overloaded for instances of `Alg`,
- and accordingly `LearnAPI.functions(algorithm)` must be non-empty.
+#### Composite algorithms (wrappers)
-Any object `alg` for which [`LearnAPI.functions`](@ref)`(alg)` is non-empty is understood
-have a valid implementation of the LearnAPI.jl interface.
+A *composite algorithm* is one with at least one property that can take other algorithms
+as values; for such algorithms [`LearnAPI.is_composite`](@ref)`(algorithm)` must be `true`
+(fallback is `false`). Generally, the keyword constructor provided by
+[`LearnAPI.constructor`](@ref) must provide default values for all fields that are not
+algorithm-valued.
+Any object `algorithm` for which [`LearnAPI.functions`](@ref)`(algorithm)` is non-empty is
+understood to have a valid implementation of the LearnAPI.jl interface.
-### Example
+#### Example
-Any instance of `GradientRidgeRegressor` defined below meets all but the last criterion
-above:
+Any instance of `GradientRidgeRegressor` defined below is a valid algorithm.
```julia
struct GradientRidgeRegressor{T<:Real}
@@ -109,28 +116,45 @@ struct GradientRidgeRegressor{T<:Real}
l2_regularization::T
end
GradientRidgeRegressor(; learning_rate=0.01, epochs=10, l2_regularization=0.01) =
- GradientRidgeRegressor(learning_rate, epochs, l2_regularization)
+ GradientRidgeRegressor(learning_rate, epochs, l2_regularization)
+LearnAPI.constructor(::GradientRidgeRegressor) = GradientRidgeRegressor
```
-The same is not true if we make this a `mutable struct`. In that case we will need to
-appropriately overload `Base.==` for `GradientRidgeRegressor`.
+## Documentation
+Attach public LearnAPI.jl-related documentation for an algorithm to it's *constructor*,
+rather than to the struct defining its type. In this way, an algorithm can implement
+multiple interfaces, in addition to the LearnAPI interface, with separate document strings
+for each.
## Methods
-Only these method names are exported: `fit`, `obsfit`, `predict`, `obspredict`,
-`transform`, `obstransform`, `inverse_transform`, `minimize`, and `obs`. All new
-implementations must implement [`obsfit`](@ref), the accessor function
-[`LearnAPI.algorithm`](@ref algorithm_minimize) and the trait
-[`LearnAPI.functions`](@ref).
+!!! note "Compulsory methods"
+
+ All new algorithm types must implement [`fit`](@ref),
+ [`LearnAPI.algorithm`](@ref algorithm_minimize), [`LearnAPI.constructor`](@ref) and
+ [`LearnAPI.functions`](@ref).
-- [`fit`](@ref fit)/[`obsfit`](@ref): for training algorithms that generalize to new data
+Most algorithms will also implement [`predict`](@ref) and/or [`transform`](@ref).
-- [`predict`](@ref operations)/[`obspredict`](@ref): for outputting [targets](@ref proxy)
- or [target proxies](@ref proxy) (such as probability density functions)
+### List of methods
-- [`transform`](@ref operations)/[`obstransform`](@ref): similar to `predict`, but for
- arbitrary kinds of output, and which can be paired with an `inverse_transform` method
+- [`fit`](@ref fit): for training or updating algorithms that generalize to new data. Or,
+ for non-generalizing algorithms (see [Static Algorithms](@ref)), for wrapping
+ `algorithm` in a mutable struct that can be mutated by `predict`/`transform` to record
+ byproducts of those operations.
+
+- [`update`](@ref fit): for updating learning outcomes after hyperparameter changes, such
+ as increasing an iteration parameter.
+
+- [`update_observations`](@ref fit), [`update_features`](@ref fit): update learning
+ outcomes by presenting additional training data.
+
+- [`predict`](@ref operations): for outputting [targets](@ref proxy) or [target
+ proxies](@ref proxy) (such as probability density functions)
+
+- [`transform`](@ref operations): similar to `predict`, but for arbitrary kinds of output,
+ and which can be paired with an `inverse_transform` method
- [`inverse_transform`](@ref operations): for inverting the output of
`transform` ("inverting" broadly understood)
@@ -138,21 +162,23 @@ implementations must implement [`obsfit`](@ref), the accessor function
- [`minimize`](@ref algorithm_minimize): for stripping the `model` output by `fit` of
inessential content, for purposes of serialization.
-- [`obs`](@ref data_interface): a method for exposing to the user "optimized",
- algorithm-specific representations of data, which can be passed to `obsfit`,
- `obspredict` or `obstransform`, but which can also be efficiently resampled using the
- `getobs`/`numobs` interface provided by
- [MLUtils.jl](https://github.com/JuliaML/MLUtils.jl).
+- [`LearnAPI.target`](@ref input), [`LearnAPI.weights`](@ref input),
+ [`LearnAPI.features`](@ref): for extracting relevant parts of training data, where
+ defined.
-- [Accessor functions](@ref accessor_functions): include things like `feature_importances`
- and `training_losses`, for extracting, from training outcomes, information common to
- many algorithms.
+- [`obs`](@ref data_interface): method for exposing to the user
+ algorithm-specific representations of data, which are additionally guaranteed to
+ implement the observation access API specified by
+ [`LearnAPI.data_interface(algorithm)`](@ref).
+
+- [Accessor functions](@ref accessor_functions): these include functions like
+ `feature_importances` and `training_losses`, for extracting, from training outcomes,
+ information common to many algorithms.
+
+- [Algorithm traits](@ref traits): methods that promise specific algorithm behavior or
+ record general information about the algorithm. Only [`LearnAPI.constructor`](@ref) and
+ [`LearnAPI.functions`](@ref) are universally compulsory.
-- [Algorithm traits](@ref traits): special methods that promise specific algorithm
- behavior or for recording general information about the algorithm. The only universally
- compulsory trait is `LearnAPI.functions(algorithm)`, which returns a list of the
- explicitly overloaded non-trait methods.
-
---
¹ We acknowledge users may not like this terminology, and may know "algorithm" by some
diff --git a/docs/src/target_weights_features.md b/docs/src/target_weights_features.md
new file mode 100644
index 00000000..df4f76b7
--- /dev/null
+++ b/docs/src/target_weights_features.md
@@ -0,0 +1,46 @@
+# [`target`, `weights`, and `features`](@id input)
+
+Methods for extracting parts of training data:
+
+```julia
+LearnAPI.target(algorithm, data) ->
+LearnAPI.weights(algorithm, data) ->
+LearnAPI.features(algorithm, data) ->
+```
+
+Here `data` is something supported in a call of the form `fit(algorithm, data)`.
+
+# Typical workflow
+
+Not typically appearing in a general user's workflow but useful in meta-alagorithms, such
+as cross-validation (see the example in [`obs` and Data Interfaces](@ref data_interface)).
+
+Supposing `algorithm` is a supervised classifier predicting a one-dimensional vector
+target:
+
+```julia
+model = fit(algorithm, data)
+X = LearnAPI.features(algorithm, data)
+y = LearnAPI.target(algorithm, data)
+ŷ = predict(model, Point(), X)
+training_loss = sum(ŷ .!= y)
+```
+
+# Implementation guide
+
+The fallback returns `first(data)`, assuming `data` is a tuple, and `data` otherwise.
+
+| method | fallback | compulsory? |
+|:----------------------------|:-----------------:|------------------------|
+| [`LearnAPI.target`](@ref) | returns `nothing` | no |
+| [`LearnAPI.weights`](@ref) | returns `nothing` | no |
+| [`LearnAPI.features`](@ref) | see docstring | only if fallback fails |
+
+
+# Reference
+
+```@docs
+LearnAPI.target
+LearnAPI.weights
+LearnAPI.features
+```
diff --git a/docs/src/traits.md b/docs/src/traits.md
index 3a263595..25edaa1c 100644
--- a/docs/src/traits.md
+++ b/docs/src/traits.md
@@ -1,17 +1,10 @@
# [Algorithm Traits](@id traits)
-Traits generally promise specific algorithm behavior, such as: *This algorithm supports
-per-observation weights, which must appear as the third argument of `fit`*, or *This
-algorithm's `transform` method predicts `Real` vectors*. They also record more mundane
-information, such as a package license.
+Algorithm traits are simply functions whose sole argument is an algorithm.
-Algorithm traits are functions whose first (and usually only) argument is an algorithm.
-
-### Special two-argument traits
-
-The two-argument version of [`LearnAPI.predict_output_scitype`](@ref) and
-[`LearnAPI.predict_output_scitype`](@ref) are the only overloadable traits with more than
-one argument.
+Traits promise specific algorithm behavior, such as: *This algorithm can make point or
+probabilistic predictions* or *This algorithm is supervised* (sees a target in
+training). They may also record more mundane information, such as a package license.
## [Trait summary](@id trait_summary)
@@ -20,57 +13,35 @@ one argument.
In the examples column of the table below, `Table`, `Continuous`, `Sampleable` are names owned by the
package [ScientificTypesBase.jl](https://github.com/JuliaAI/ScientificTypesBase.jl/).
-| trait | return value | fallback value | example |
-|:----------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------|:---------------------------------------------------------|
-| [`LearnAPI.functions`](@ref)`(algorithm)` | functions you can apply to `algorithm` or associated model (traits excluded) | `()` | `(LearnAPI.fit, LearnAPI.predict, LearnAPI.algorithm)` |
-| [`LearnAPI.kinds_of_proxy`](@ref)`(algorithm)` | instances `kop` of `KindOfProxy` for which an implementation of `LearnAPI.predict(algorithm, kop, ...)` is guaranteed. | `()` | `(Distribution(), Interval())` |
-| [`LearnAPI.position_of_target`](@ref)`(algorithm)` | the positional index¹ of the **target** in `data` in `fit(algorithm, data...)` calls | `0` | 2 |
-| [`LearnAPI.position_of_weights`](@ref)`(algorithm)` | the positional index¹ of **per-observation weights** in `data` in `fit(algorithm, data...)` | `0` | 3 |
-| [`LearnAPI.descriptors`](@ref)`(algorithm)` | lists one or more suggestive algorithm descriptors from `LearnAPI.descriptors()` | `()` | (:regression, :probabilistic) |
-| [`LearnAPI.is_pure_julia`](@ref)`(algorithm)` | `true` if implementation is 100% Julia code | `false` | `true` |
-| [`LearnAPI.pkg_name`](@ref)`(algorithm)` | name of package providing core code (may be different from package providing LearnAPI.jl implementation) | `"unknown"` | `"DecisionTree"` |
-| [`LearnAPI.pkg_license`](@ref)`(algorithm)` | name of license of package providing core code | `"unknown"` | `"MIT"` |
-| [`LearnAPI.doc_url`](@ref)`(algorithm)` | url providing documentation of the core code | `"unknown"` | `"https://en.wikipedia.org/wiki/Decision_tree_learning"` |
-| [`LearnAPI.load_path`](@ref)`(algorithm)` | a string indicating where the struct for `typeof(algorithm)` is defined, beginning with name of package providing implementation | `"unknown"` | `FastTrees.LearnAPI.DecisionTreeClassifier` |
-| [`LearnAPI.is_composite`](@ref)`(algorithm)` | `true` if one or more properties (fields) of `algorithm` may be an algorithm | `false` | `true` |
-| [`LearnAPI.human_name`](@ref)`(algorithm)` | human name for the algorithm; should be a noun | type name with spaces | "elastic net regressor" |
-| [`LearnAPI.iteration_parameter`](@ref)`(algorithm)` | symbolic name of an iteration parameter | `nothing` | :epochs |
-| [`LearnAPI.fit_scitype`](@ref)`(algorithm)` | upper bound on `scitype(data)` ensuring `fit(algorithm, data...)` works | `Union{}` | `Tuple{Table(Continuous), AbstractVector{Continuous}}` |
-| [`LearnAPI.fit_observation_scitype`](@ref)`(algorithm)` | upper bound on `scitype(observation)` for `observation` in `data` ensuring `fit(algorithm, data...)` works | `Union{}` | `Tuple{AbstractVector{Continuous}, Continuous}` |
-| [`LearnAPI.fit_type`](@ref)`(algorithm)` | upper bound on `typeof(data)` ensuring `fit(algorithm, data...)` works | `Union{}` | `Tuple{AbstractMatrix{<:Real}, AbstractVector{<:Real}}` |
-| [`LearnAPI.fit_observation_type`](@ref)`(algorithm)` | upper bound on `typeof(observation)` for `observation` in `data` ensuring `fit(algorithm, data...)` works | `Union{}` | `Tuple{AbstractVector{<:Real}, Real}` |
-| [`LearnAPI.predict_input_scitype`](@ref)`(algorithm)` | upper bound on `scitype(data)` ensuring `predict(model, kop, data...)` works | `Union{}` | `Table(Continuous)` |
-| [`LearnAPI.predict_input_observation_scitype`](@ref)`(algorithm)` | upper bound on `scitype(observation)` for `observation` in `data` ensuring `predict(model, kop, data...)` works | `Union{}` | `Vector{Continuous}` |
-| [`LearnAPI.predict_input_type`](@ref)`(algorithm)` | upper bound on `typeof(data)` ensuring `predict(model, kop, data...)` works | `Union{}` | `AbstractMatrix{<:Real}` |
-| [`LearnAPI.predict_input_observation_type`](@ref)`(algorithm)` | upper bound on `typeof(observation)` for `observation` in `data` ensuring `predict(model, kop, data...)` works | `Union{}` | `Vector{<:Real}` |
-| [`LearnAPI.predict_output_scitype`](@ref)`(algorithm, kind_of_proxy)` | upper bound on `scitype(predict(model, ...))` | `Any` | `AbstractVector{Continuous}` |
-| [`LearnAPI.predict_output_type`](@ref)`(algorithm, kind_of_proxy)` | upper bound on `typeof(predict(model, ...))` | `Any` | `AbstractVector{<:Real}` |
-| [`LearnAPI.transform_input_scitype`](@ref)`(algorithm)` | upper bound on `scitype(data)` ensuring `transform(model, data...)` works | `Union{}` | `Table(Continuous)` |
-| [`LearnAPI.transform_input_observation_scitype`](@ref)`(algorithm)` | upper bound on `scitype(observation)` for `observation` in `data` ensuring `transform(model, data...)` works | `Union{}` | `Vector{Continuous}` |
-| [`LearnAPI.transform_input_type`](@ref)`(algorithm)` | upper bound on `typeof(data)`ensuring `transform(model, data...)` works | `Union{}` | `AbstractMatrix{<:Real}}` |
-| [`LearnAPI.transform_input_observation_type`](@ref)`(algorithm)` | upper bound on `typeof(observation)` for `observation` in `data` ensuring `transform(model, data...)` works | `Union{}` | `Vector{Continuous}` |
-| [`LearnAPI.transform_output_scitype`](@ref)`(algorithm)` | upper bound on `scitype(transform(model, ...))` | `Any` | `Table(Continuous)` |
-| [`LearnAPI.transform_output_type`](@ref)`(algorithm)` | upper bound on `typeof(transform(model, ...))` | `Any` | `AbstractMatrix{<:Real}` |
-| [`LearnAPI.predict_or_transform_mutates`](@ref)`(algorithm)` | `true` if `predict` or `transform` mutates first argument | `false` | `true` |
-
-¹ If the value is `0`, then the variable in boldface type is not supported and not
-expected to appear in `data`. If `length(data)` is less than the trait value, then `data`
-is understood to exclude the variable, but note that `fit` can have multiple signatures of
-varying lengths, as in `fit(algorithm, X, y)` and `fit(algorithm, X, y, w)`. A non-zero
-value is a promise that `fit` includes a signature of sufficient length to include the
-variable.
-
+| trait | return value | fallback value | example |
+|:-------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------|:-----------------------------------------------------------|
+| [`LearnAPI.constructor`](@ref)`(algorithm)` | constructor for generating new or modified versions of `algorithm` | (no fallback) | `RidgeRegressor` |
+| [`LearnAPI.functions`](@ref)`(algorithm)` | functions you can apply to `algorithm` or associated model (traits excluded) | `()` | `(:fit, :predict, :minimize, :(LearnAPI.algorithm), :obs)` |
+| [`LearnAPI.kinds_of_proxy`](@ref)`(algorithm)` | instances `kind` of `KindOfProxy` for which an implementation of `LearnAPI.predict(algorithm, kind, ...)` is guaranteed. | `()` | `(Distribution(), Interval())` |
+| [`LearnAPI.tags`](@ref)`(algorithm)` | lists one or more suggestive algorithm tags from `LearnAPI.tags()` | `()` | (:regression, :probabilistic) |
+| [`LearnAPI.is_pure_julia`](@ref)`(algorithm)` | `true` if implementation is 100% Julia code | `false` | `true` |
+| [`LearnAPI.pkg_name`](@ref)`(algorithm)` | name of package providing core code (may be different from package providing LearnAPI.jl implementation) | `"unknown"` | `"DecisionTree"` |
+| [`LearnAPI.pkg_license`](@ref)`(algorithm)` | name of license of package providing core code | `"unknown"` | `"MIT"` |
+| [`LearnAPI.doc_url`](@ref)`(algorithm)` | url providing documentation of the core code | `"unknown"` | `"https://en.wikipedia.org/wiki/Decision_tree_learning"` |
+| [`LearnAPI.load_path`](@ref)`(algorithm)` | string locating name returned by `LearnAPI.constructor(algorithm)`, beginning with a package name | "unknown"` | `FastTrees.LearnAPI.DecisionTreeClassifier` |
+| [`LearnAPI.is_composite`](@ref)`(algorithm)` | `true` if one or more properties of `algorithm` may be an algorithm | `false` | `true` |
+| [`LearnAPI.human_name`](@ref)`(algorithm)` | human name for the algorithm; should be a noun | type name with spaces | "elastic net regressor" |
+| [`LearnAPI.data_interface`](@ref)`(algorithm)` | Interface implemented by objects returned by [`obs`](@ref) | `Base.HasLength()` (supports `MLUtils.getobs/numobs`) | `Base.SizeUnknown()` (supports `iterate`) |
+| [`LearnAPI.iteration_parameter`](@ref)`(algorithm)` | symbolic name of an iteration parameter | `nothing` | :epochs |
+| [`LearnAPI.fit_observation_scitype`](@ref)`(algorithm)` | upper bound on `scitype(observation)` for `observation` in `data` ensuring `fit(algorithm, data)` works | `Union{}` | `Tuple{AbstractVector{Continuous}, Continuous}` |
+| [`LearnAPI.target_observation_scitype`](@ref)`(algorithm)` | upper bound on the scitype of each observation of the targget | `Any` | `Continuous` |
+| [`LearnAPI.predict_or_transform_mutates`](@ref)`(algorithm)` | `true` if `predict` or `transform` mutates first argument | `false` | `true` |
### Derived Traits
-The following convenience methods are provided but not overloadable by new implementations.
+The following are provided for convenience but should not be overloaded by new algorithms:
-| trait | return value | example |
-|:-----------------------------------------------------|:--------------------------------------------------------------------------------------------------------------|:--------|
-| `LearnAPI.name(algorithm)` | algorithm type name as string | "PCA" |
-| `LearnAPI.is_algorithm(algorithm)` | `true` if `LearnAPI.functions(algorithm)` is not empty | `true` |
-| [`LearnAPI.predict_output_scitype(algorithm)`](@ref) | dictionary of upper bounds on the scitype of predictions, keyed on subtypes of [`LearnAPI.KindOfProxy`](@ref) | |
-| [`LearnAPI.predict_output_type(algorithm)`](@ref) | dictionary of upper bounds on the type of predictions, keyed on subtypes of [`LearnAPI.KindOfProxy`](@ref) | |
+| trait | return value | example |
+|:-----------------------------------|:---------------------------------------------------------------------|:--------|
+| `LearnAPI.name(algorithm)` | algorithm type name as string | "PCA" |
+| `LearnAPI.is_algorithm(algorithm)` | `true` if `algorithm` is LearnAPI.jl-compliant | `true` |
+| `LearnAPI.target(algorithm)` | `true` if [`LearnAPI.target(algorithm, data)`](@ref) is implemented | `false` |
+| `LearnAPI.weights(algorithm)` | `true` if [`LearnAPI.weights(algorithm, data)`](@ref) is implemented | `false` |
## Implementation guide
@@ -97,31 +68,31 @@ Multiple traits can be declared like this:
)
```
-### The global trait contracts
+### [The global trait contract](@id trait_contract)
To ensure that trait metadata can be stored in an external algorithm registry, LearnAPI.jl
requires:
-1. *Finiteness:* The value of a trait is the same for all algorithms with same
- underlying `UnionAll` type. That is, even if the type parameters are different, the
- trait should be the same. There is an exception if `is_composite(algorithm) = true`.
+1. *Finiteness:* The value of a trait is the same for all `algorithm`s with same value of
+ [`LearnAPI.constructor(algorithm)`](@ref). This typically means trait values do not
+ depend on type parameters! If `is_composite(algorithm) = true`, this requirement is
+ dropped.
-2. *Serializability:* The value of any trait can be evaluated without installing any
- third party package; `using LearnAPI` should suffice.
+2. *Low level deserializability:* It should be possible to evaluate the trait *value* when
+ `LearnAPI` is the only imported module.
Because of 1, combining a lot of functionality into one algorithm (e.g. the algorithm can
perform both classification or regression) can mean traits are necessarily less
-informative (as in `LearnAPI.predict_type(algorithm) = Any`).
+informative (as in `LearnAPI.target_observation_scitype(algorithm) = Any`).
## Reference
```@docs
+LearnAPI.constructor
LearnAPI.functions
LearnAPI.kinds_of_proxy
-LearnAPI.position_of_target
-LearnAPI.position_of_weights
-LearnAPI.descriptors
+LearnAPI.tags
LearnAPI.is_pure_julia
LearnAPI.pkg_name
LearnAPI.pkg_license
@@ -129,22 +100,10 @@ LearnAPI.doc_url
LearnAPI.load_path
LearnAPI.is_composite
LearnAPI.human_name
+LearnAPI.data_interface
LearnAPI.iteration_parameter
-LearnAPI.fit_scitype
-LearnAPI.fit_type
LearnAPI.fit_observation_scitype
-LearnAPI.fit_observation_type
-LearnAPI.predict_input_scitype
-LearnAPI.predict_input_observation_scitype
-LearnAPI.predict_input_type
-LearnAPI.predict_input_observation_type
-LearnAPI.predict_output_scitype
-LearnAPI.predict_output_type
-LearnAPI.transform_input_scitype
-LearnAPI.transform_input_observation_scitype
-LearnAPI.transform_input_type
-LearnAPI.transform_input_observation_type
+LearnAPI.target_observation_scitype
LearnAPI.predict_or_transform_mutates
-LearnAPI.transform_output_scitype
-LearnAPI.transform_output_type
+LearnAPI.@trait
```
diff --git a/src/LearnAPI.jl b/src/LearnAPI.jl
index 24626bcd..ffab0130 100644
--- a/src/LearnAPI.jl
+++ b/src/LearnAPI.jl
@@ -5,15 +5,16 @@ import InteractiveUtils.subtypes
include("tools.jl")
include("types.jl")
include("predict_transform.jl")
-include("fit.jl")
+include("fit_update.jl")
include("minimize.jl")
+include("target_weights_features.jl")
include("obs.jl")
include("accessor_functions.jl")
include("traits.jl")
export @trait
-export fit, predict, transform, inverse_transform, fit_transform, minimize
-export obs, obsfit, obspredict, obstransform
+export fit, update, update_observations, update_features
+export predict, transform, inverse_transform, minimize, obs
for name in Symbol.(CONCRETE_TARGET_PROXY_TYPES_SYMBOLS)
@eval export $name
diff --git a/src/accessor_functions.jl b/src/accessor_functions.jl
index d20f1da2..854bfdb7 100644
--- a/src/accessor_functions.jl
+++ b/src/accessor_functions.jl
@@ -31,7 +31,7 @@ is `true`.
# New implementations
Implementation is compulsory for new algorithm types. The behaviour described above is the
-only contract. $(DOC_IMPLEMENTED_METHODS(:algorithm))
+only contract. $(DOC_IMPLEMENTED_METHODS(":(LearnAPI.algorithm)"))
"""
function algorithm end
@@ -44,7 +44,7 @@ Return the algorithm-specific feature importances of a `model` output by
an abstract vector of `feature::Symbol => importance::Real` pairs (e.g `[:gender => 0.23,
:height => 0.7, :weight => 0.1]`).
-The `algorithm` supports feature importances if `LearnAPI.feature_importances in
+The `algorithm` supports feature importances if `:(LearnAPI.feature_importances) in
LearnAPI.functions(algorithm)`.
If an algorithm is sometimes unable to report feature importances then
@@ -55,7 +55,7 @@ If an algorithm is sometimes unable to report feature importances then
Implementation is optional.
-$(DOC_IMPLEMENTED_METHODS(:feature_importances)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.feature_importances)")).
"""
function feature_importances end
@@ -68,7 +68,7 @@ an abstract vector of `feature_or_class::Symbol => coefficient::Real` pairs (e.g
=> 0.23, :height => 0.7, :weight => 0.1]`) or, in the case of multi-targets,
`feature::Symbol => coefficients::AbstractVector{<:Real}` pairs.
-The `model` reports coefficients if `LearnAPI.coefficients in
+The `model` reports coefficients if `:(LearnAPI.coefficients) in
LearnAPI.functions(Learn.algorithm(model))`.
See also [`LearnAPI.intercept`](@ref).
@@ -77,7 +77,7 @@ See also [`LearnAPI.intercept`](@ref).
Implementation is optional.
-$(DOC_IMPLEMENTED_METHODS(:coefficients)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.coefficients)")).
"""
function coefficients end
@@ -88,7 +88,7 @@ function coefficients end
For a linear model, return the learned intercept. The value returned is `Real` (single
target) or an `AbstractVector{<:Real}` (multi-target).
-The `model` reports intercept if `LearnAPI.intercept in
+The `model` reports intercept if `:(LearnAPI.intercept) in
LearnAPI.functions(Learn.algorithm(model))`.
See also [`LearnAPI.coefficients`](@ref).
@@ -97,7 +97,7 @@ See also [`LearnAPI.coefficients`](@ref).
Implementation is optional.
-$(DOC_IMPLEMENTED_METHODS(:intercept)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.intercept)")).
"""
function intercept end
@@ -120,7 +120,7 @@ See also [`LearnAPI.trees`](@ref).
Implementation is optional.
-$(DOC_IMPLEMENTED_METHODS(:tree)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.tree)")).
"""
function tree end
@@ -137,7 +137,7 @@ See also [`LearnAPI.tree`](@ref).
Implementation is optional.
-$(DOC_IMPLEMENTED_METHODS(:trees)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.trees)")).
"""
function trees end
@@ -155,11 +155,29 @@ See also [`fit`](@ref).
Implement for iterative algorithms that compute and record training losses as part of
training (e.g. neural networks).
-$(DOC_IMPLEMENTED_METHODS(:training_losses)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.training_losses)")).
"""
function training_losses end
+"""
+ LearnAPI.training_predictions(model)
+
+Return internally computed training predictions when running `model = fit(algorithm, ...)`
+for some `algorithm`.
+
+See also [`fit`](@ref).
+
+# New implementations
+
+Implement for iterative algorithms that compute and record training losses as part of
+training (e.g. neural networks).
+
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.training_predictions)")).
+
+"""
+function training_predictions end
+
"""
LearnAPI.training_scores(model)
@@ -174,7 +192,7 @@ Implement for algorithms, such as outlier detection algorithms, which associate
with each observation during training, where these scores are of interest in later
processes (e.g, in defining normalized scores for new data).
-$(DOC_IMPLEMENTED_METHODS(:training_scores)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.training_scores)")).
"""
function training_scores end
@@ -194,9 +212,9 @@ See also [`is_composite`](@ref).
# New implementations
-Implementent if and only if `model` is a composite model.
+Implementent if and only if `model` is a composite model.
-$(DOC_IMPLEMENTED_METHODS(:components)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.components)")).
"""
function components end
@@ -211,7 +229,7 @@ See also [`fit`](@ref).
# New implementations
-$(DOC_IMPLEMENTED_METHODS(:training_labels)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.training_labels)")).
"""
function training_labels end
@@ -227,6 +245,7 @@ const ACCESSOR_FUNCTIONS_WITHOUT_EXTRAS = (
feature_importances,
training_labels,
training_losses,
+ training_predictions,
training_scores,
components,
)
@@ -254,7 +273,7 @@ See also [`fit`](@ref).
Implementation is discouraged for byproducts already covered by other LearnAPI.jl accessor
functions: $ACCESSOR_FUNCTIONS_WITHOUT_EXTRAS_LIST.
-$(DOC_IMPLEMENTED_METHODS(:training_labels)).
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.training_labels)")).
"""
function extras end
@@ -268,4 +287,3 @@ const ACCESSOR_FUNCTIONS_LIST = join(
", ",
" and ",
)
-
diff --git a/src/fit.jl b/src/fit.jl
deleted file mode 100644
index 010e53e0..00000000
--- a/src/fit.jl
+++ /dev/null
@@ -1,184 +0,0 @@
-# # DOC STRING HELPERS
-
-const TRAINING_FUNCTIONS = (:fit,)
-
-
-# # FIT
-
-"""
- LearnAPI.fit(algorithm, data...; verbosity=1)
-
-Execute the algorithm with configuration `algorithm` using the provided training `data`,
-returning an object, `model`, on which other methods, such as [`predict`](@ref) or
-[`transform`](@ref), can be dispatched. [`LearnAPI.functions(algorithm)`](@ref) returns a
-list of methods that can be applied to either `algorithm` or `model`.
-
-# Arguments
-
-- `algorithm`: property-accessible object whose properties are the hyperparameters of
- some ML/statistical algorithm
-
-$(DOC_ARGUMENTS(:fit))
-
-- `verbosity=1`: logging level; set to `0` for warnings only, and `-1` for silent training
-
-See also [`obsfit`](@ref), [`predict`](@ref), [`transform`](@ref),
-[`inverse_transform`](@ref), [`LearnAPI.functions`](@ref), [`obs`](@ref).
-
-# Extended help
-
-# New implementations
-
-LearnAPI.jl provides the following definition of `fit`, which is never directly overloaded:
-
-```julia
-fit(algorithm, data...; verbosity=1) =
- obsfit(algorithm, Obs(), obs(fit, algorithm, data...); verbosity)
-```
-
-Rather, new algorithms should overload [`obsfit`](@ref). See also [`obs`](@ref).
-
-"""
-fit(algorithm, data...; verbosity=1) =
- obsfit(algorithm, obs(fit, algorithm, data...), verbosity)
-
-"""
- obsfit(algorithm, obsdata; verbosity=1)
-
-A lower-level alternative to [`fit`](@ref), this method consumes a pre-processed form of
-user data. Specifically, the following two code snippets are equivalent:
-
-```julia
-model = fit(algorithm, data...)
-```
-and
-
-```julia
-obsdata = obs(fit, algorithm, data...)
-model = obsfit(algorithm, obsdata)
-```
-
-Here `obsdata` is algorithm-specific, "observation-accessible" data, meaning it implements
-the MLUtils.jl `getobs`/`numobs` interface for observation resampling (even if `data` does
-not). Moreover, resampled versions of `obsdata` may be passed to `obsfit` in its place.
-
-The use of `obsfit` may offer performance advantages. See more at [`obs`](@ref).
-
-See also [`fit`](@ref), [`obs`](@ref).
-
-# Extended help
-
-# New implementations
-
-Implementation of the following method signature is compulsory for all new algorithms:
-
-```julia
-LearnAPI.obsfit(algorithm, obsdata, verbosity)
-```
-
-Here `obsdata` has the form explained above. If [`obs`](@ref)`(fit, ...)` is not being
-overloaded, then a fallback gives `obsdata = data` (always a tuple!). Note that
-`verbosity` is a positional argument, not a keyword argument in the overloaded signature.
-
-New implementations must also implement [`LearnAPI.algorithm`](@ref).
-
-If overloaded, then the functions `LearnAPI.obsfit` and `LearnAPI.fit` must be included in
-the tuple returned by the [`LearnAPI.functions(algorithm)`](@ref) trait.
-
-## Non-generalizing algorithms
-
-If the algorithm does not generalize to new data (e.g, DBSCAN clustering) then `data = ()`
-and `obsfit` carries out no computation, as this happen entirely in a `transform` and/or
-`predict` call. In such cases, `obsfit(algorithm, ...)` may return `algorithm`, but
-another possibility is allowed: To provide a mechanism for `transform`/`predict` to report
-byproducts of the computation (e.g., a list of boundary points in DBSCAN clustering) they
-are allowed to *mutate* the `model` object returned by `obsfit`, which is then arranged to
-be a mutable struct wrapping `algorithm` and fields to store the byproducts. In that case,
-[`LearnAPI.predict_or_transform_mutates(algorithm)`](@ref) must be overloaded to return
-`true`.
-
-"""
-obsfit(algorithm, obsdata; verbosity=1) =
- obsfit(algorithm, obsdata, verbosity)
-
-
-# # UPDATE
-
-"""
- LearnAPI.update!(algorithm, verbosity, fitted_params, state, data...)
-
-Based on the values of `state`, and `fitted_params` returned by a preceding call to
-[`LearnAPI.fit`](@ref), [`LearnAPI.ingest!`](@ref), or [`LearnAPI.update!`](@ref), update a
-algorithm's fitted parameters, returning new (or mutated) `state` and `fitted_params`.
-
-Intended for retraining when the training data has not changed, but `algorithm`
-properties (hyperparameters) may have changed, e.g., when increasing an iteration
-parameter. Specifically, the assumption is that `data` have the same values
-seen in the most recent call to `fit/update!/ingest!`.
-
-For incremental training (same algorithm, new data) see instead [`LearnAPI.ingest!`](@ref).
-
-# Return value
-
-Same as [`LearnAPI.fit`](@ref), namely a tuple (`fitted_params`, `state`, `report`). See
-[`LearnAPI.fit`](@ref) for details.
-
-
-# New implementations
-
-Overloading this method is optional. A fallback calls `LearnAPI.fit`:
-
-```julia
-LearnAPI.update!(algorithm, verbosity, fitted_params, state, data...) =
- fit(algorithm, verbosity, data)
-```
-$(DOC_IMPLEMENTED_METHODS(:fit))
-
-The most common use case is continuing training of an iterative algorithm: `state` is
-simply a copy of the algorithm used in the last training call (`fit`, `update!` or
-`ingest!`) and this will include the current number of iterations as a property. If
-`algorithm` and `state` differ only in the number of iterations (e.g., epochs in a neural
-network), which has increased, then the fitted parameters (network weights and biases) are
-updated, rather than computed from scratch. Otherwise, `update!` simply calls `fit`, to
-force retraining from scratch.
-
-It is permitted to return mutated versions of `state` and `fitted_params`.
-
-See also [`LearnAPI.fit`](@ref), [`LearnAPI.ingest!`](@ref).
-
-"""
-
-
-# # INGEST
-
-"""
- LearnAPI.ingest!(algorithm, verbosity, fitted_params, state, data...)
-
-For an algorithm that supports incremental learning, update the fitted parameters using
-`data`, which has typically not been seen before. The arguments `state` and
-`fitted_params` are the output of a preceding call to [`LearnAPI.fit`](@ref),
-[`LearnAPI.ingest!`](@ref), or [`LearnAPI.update!`](@ref), of which mutated or new
-versions are returned.
-
-For updating fitted parameters using the *same* data but new hyperparameters, see instead
-[`LearnAPI.update!`](@ref).
-
-For training an algorithm with new hyperparameters but *unchanged* data, see instead
-[`LearnAPI.update!`](@ref).
-
-
-# Return value
-
-Same as [`LearnAPI.fit`](@ref), namely a tuple (`fitted_params`, `state`, `report`). See
-[`LearnAPI.fit`](@ref) for details.
-
-
-# New implementations
-
-Implementing this method is optional. It has no fallback.
-
-$(DOC_IMPLEMENTED_METHODS(:fit))
-
-See also [`LearnAPI.fit`](@ref), [`LearnAPI.update!`](@ref).
-
-"""
diff --git a/src/fit_update.jl b/src/fit_update.jl
new file mode 100644
index 00000000..44b427b2
--- /dev/null
+++ b/src/fit_update.jl
@@ -0,0 +1,149 @@
+# # FIT
+
+"""
+ fit(algorithm, data; verbosity=1)
+ fit(algorithm; verbosity=1)
+
+Execute the algorithm with configuration `algorithm` using the provided training `data`,
+returning an object, `model`, on which other methods, such as [`predict`](@ref) or
+[`transform`](@ref), can be dispatched. [`LearnAPI.functions(algorithm)`](@ref) returns a
+list of methods that can be applied to either `algorithm` or `model`.
+
+The second signature is provided by algorithms that do not generalize to new observations
+("static" algorithms). In that case, `transform(model, data)` or `predict(model, ...,
+data)` carries out the actual algorithm execution, writing any byproducts of that
+operation to the mutable object `model` returned by `fit`.
+
+Whenever `fit` expects a tuple form of argument, `data = (X1, ..., Xn)`, then the
+signature `fit(algorithm, X1, ..., Xn)` is also provided.
+
+For example, a supervised classifier will typically admit this workflow:
+
+```julia
+model = fit(algorithm, (X, y)) # or `fit(algorithm, X, y)`
+ŷ = predict(model, Xnew)
+```
+
+Use `verbosity=0` for warnings only, and `-1` for silent training.
+
+See also [`predict`](@ref), [`transform`](@ref), [`inverse_transform`](@ref),
+[`LearnAPI.functions`](@ref), [`obs`](@ref).
+
+# Extended help
+
+# New implementations
+
+Implementation is compulsory. The signature must include `verbosity`. A fallback for the
+first signature calls the second, ignoring `data`:
+
+```julia
+fit(algorithm, data; kwargs...) = fit(algorithm; kwargs...)
+```
+
+Fallbacks also provide the data slurping versions.
+
+$(DOC_DATA_INTERFACE(:fit))
+
+"""
+fit(algorithm, data; kwargs...) =
+ fit(algorithm; kwargs...)
+fit(algorithm, data1, datas...; kwargs...) =
+ fit(algorithm, (data1, datas...); kwargs...)
+
+# # UPDATE AND COUSINS
+
+"""
+ update(model, data; verbosity=1, hyperparam_replacements...)
+
+Return an updated version of the `model` object returned by a previous [`fit`](@ref) or
+`update` call, but with the specified hyperparameter replacements, in the form `p1=value1,
+p2=value2, ...`.
+
+Provided that `data` is identical with the data presented in a preceding `fit` call, as in
+the example below, execution is semantically equivalent to the call `fit(algorithm,
+data)`, where `algorithm` is `LearnAPI.algorithm(model)` with the specified
+replacements. In some cases (typically, when changing an iteration parameter) there may be
+a performance benefit to using `update` instead of retraining ab initio.
+
+If `data` differs from that in the preceding `fit` or `update` call, then behaviour is
+algorithm-specific.
+
+```julia
+algorithm = MyForest(ntrees=100)
+
+# train with 100 trees:
+model = fit(algorithm, data)
+
+# add 50 more trees:
+model = update(model, data; ntrees=150)
+```
+
+See also [`fit`](@ref), [`update_observations`](@ref), [`update_features`](@ref).
+
+# New implementations
+
+Implementation is optional. The signature must include
+`verbosity`. $(DOC_IMPLEMENTED_METHODS(":(LearnAPI.update)"))
+
+"""
+update(model, data1, datas...; kwargs...) = update(model, (data1, datas...); kwargs...)
+
+"""
+ update_observations(model, new_data; verbosity=1, parameter_replacements...)
+
+Return an updated version of the `model` object returned by a previous [`fit`](@ref) or
+`update` call given the new observations present in `new_data`. One may additionally
+specify hyperparameter replacements in the form `p1=value1, p2=value2, ...`.
+
+When following the call `fit(algorithm, data)`, the `update` call is semantically
+equivalent to retraining ab initio using a concatenation of `data` and `new_data`,
+*provided there are no hyperparameter replacements.* Behaviour is otherwise
+algorithm-specific.
+
+```julia-repl
+algorithm = MyNeuralNetwork(epochs=10, learning_rate=0.01)
+
+# train for ten epochs:
+model = fit(algorithm, data)
+
+# train for two more epochs using new data and new learning rate:
+model = update_observations(model, new_data; epochs=2, learning_rate=0.1)
+```
+
+See also [`fit`](@ref), [`update`](@ref), [`update_features`](@ref).
+
+# Extended help
+
+# New implementations
+
+Implementation is optional. The signature must include
+`verbosity`. $(DOC_IMPLEMENTED_METHODS(":(LearnAPI.update_observations)"))
+
+"""
+update_observations(algorithm, data1, datas...; kwargs...) =
+ update_observations(algorithm, (data1, datas...); kwargs...)
+
+"""
+ update_features(model, new_data; verbosity=1, parameter_replacements...)
+
+Return an updated version of the `model` object returned by a previous [`fit`](@ref) or
+`update` call given the new features encapsulated in `new_data`. One may additionally
+specify hyperparameter replacements in the form `p1=value1, p2=value2, ...`.
+
+When following the call `fit(algorithm, data)`, the `update` call is semantically
+equivalent to retraining ab initio using a concatenation of `data` and `new_data`,
+*provided there are no hyperparameter replacements.* Behaviour is otherwise
+algorithm-specific.
+
+See also [`fit`](@ref), [`update`](@ref), [`update_features`](@ref).
+
+# Extended help
+
+# New implementations
+
+Implementation is optional. The signature must include
+`verbosity`. $(DOC_IMPLEMENTED_METHODS(":(LearnAPI.update_features)"))
+
+"""
+update_features(algorithm, data1, datas...; kwargs...) =
+ update_features(algorithm, (data1, datas...); kwargs...)
diff --git a/src/minimize.jl b/src/minimize.jl
index 173ee24f..653d3fdf 100644
--- a/src/minimize.jl
+++ b/src/minimize.jl
@@ -5,7 +5,7 @@ Return a version of `model` that will generally have a smaller memory allocation
`model`, suitable for serialization. Here `model` is any object returned by
[`fit`](@ref). Accessor functions that can be called on `model` may not work on
`minimize(model)`, but [`predict`](@ref), [`transform`](@ref) and
-[`inverse_transform`](@ref) will work, if implemented for `model`. Check
+[`inverse_transform`](@ref) will work, if implemented. Check
`LearnAPI.functions(LearnAPI.algorithm(model))` to view see what the original `model`
implements.
@@ -17,7 +17,7 @@ functionality is preserved by `minimize`.
# New implementations
Overloading `minimize` for new algorithms is optional. The fallback is the
-identity. $(DOC_IMPLEMENTED_METHODS(:minimize, overloaded=true))
+identity. $(DOC_IMPLEMENTED_METHODS(":minimize", overloaded=true))
New implementations must enforce the following identities, whenever the right-hand side is
defined:
diff --git a/src/obs.jl b/src/obs.jl
index 75da42f4..47fd8b79 100644
--- a/src/obs.jl
+++ b/src/obs.jl
@@ -1,122 +1,81 @@
"""
- obs(func, algorithm, data...)
+ obs(algorithm, data)
+ obs(model, data)
-Where `func` is `fit`, `predict` or `transform`, return a combined, algorithm-specific,
-representation of `data...`, which can be passed directly to `obsfit`, `obspredict` or
-`obstransform`, as shown in the example below.
+Return an algorithm-specific representation of `data`, suitable for passing to `fit`
+(first signature) or to `predict` and `transform` (second signature), in place of
+`data`. Here `model` is the return value of `fit(algorithm, ...)` for some LearnAPI.jl
+algorithm, `algorithm`.
-The returned object implements the `getobs`/`numobs` observation-resampling interface
-provided by MLUtils.jl, even if `data` does not.
+The returned object is guaranteed to implement observation access as indicated by
+[`LearnAPI.data_interface(algorithm)`](@ref) (typically
+[`LearnAPI.RandomAccess()`](@ref)).
-Calling `func` on the returned object may be cheaper than calling `func` directly on
-`data...`. And resampling the returned object using `MLUtils.getobs` may be cheaper than
-directly resampling the components of `data` (an operation not provided by the LearnAPI.jl
-interface).
+Calling `fit`/`predict`/`transform` on the returned objects may have performance
+advantages over calling directly on `data` in some contexts. And resampling the returned
+object using `MLUtils.getobs` may be cheaper than directly resampling the components of
+`data`.
# Example
Usual workflow, using data-specific resampling methods:
```julia
-X =
-y =
-
-Xtrain = Tables.select(X, 1:100)
-ytrain = y[1:100]
-model = fit(algorithm, Xtrain, ytrain)
-ŷ = predict(model, LiteralTarget(), y[101:150])
+data = (X, y) # a DataFrame and a vector
+data_train = (Tables.select(X, 1:100), y[1:100])
+model = fit(algorithm, data_train)
+ŷ = predict(model, Point(), X[101:150])
```
-Alternative workflow using `obs`:
+Alternative workflow using `obs` and the MLUtils.jl method `getobs` (assumes
+`LearnAPI.data_interface(algorithm) == RandomAccess()`):
```julia
import MLUtils
-fitdata = obs(fit, algorithm, X, y)
-predictdata = obs(predict, algorithm, X)
+fit_observations = obs(algorithm, data)
+model = fit(algorithm, MLUtils.getobs(fit_observations, 1:100))
-model = obsfit(algorithm, MLUtils.getobs(fitdata, 1:100))
-ẑ = obspredict(model, LiteralTarget(), MLUtils.getobs(predictdata, 101:150))
+predict_observations = obs(model, X)
+ẑ = predict(model, Point(), MLUtils.getobs(predict_observations, 101:150))
@assert ẑ == ŷ
```
-See also [`obsfit`](@ref), [`obspredict`](@ref), [`obstransform`](@ref).
+See also [`LearnAPI.data_interface`](@ref).
# Extended help
# New implementations
-If the `data` to be consumed in standard user calls to `fit`, `predict` or `transform`
-consists only of tables and arrays (with last dimension the observation dimension) then
-overloading `obs` is optional, but the user will get no performance benefits by using
-it. The implementation of `obs` is optional under more general circumstances stated at the
-end.
+Implementation is typically optional.
-The fallback for `obs` just slurps the provided data:
+For each supported form of `data` in `fit(algorithm, data)`, it must be true that `model =
+fit(algorithm, observations)` is equivalent to `model = fit(algorithm, data)`, whenever
+`observations = obs(algorithm, data)`. For each supported form of `data` in calls
+`predict(model, ..., data)` and `transform(model, data)`, where implemented, the calls
+`predict(model, ..., observations)` and `transform(model, observations)` are supported
+alternatives, whenever `observations = obs(model, data)`.
-```julia
-obs(func, alg, data...) = data
-```
+The fallback for `obs` is `obs(model_or_algorithm, data) = data`, and the fallback for
+`LearnAPI.data_interface(algorithm)` is `LearnAPI.RandomAccess()`. For details refer to
+the [`LearnAPI.data_interface`](@ref) document string.
-The only contractual obligation of `obs` is to return an object implementing the
-`getobs`/`numobs` interface. Generally it suffices to overload `Base.getindex` and
-`Base.length`. However, note that implementations of [`obsfit`](@ref),
-[`obspredict`](@ref), and [`obstransform`](@ref) depend on the form of output of `obs`.
+In particular, if the `data` to be consumed by `fit`, `predict` or `transform` consists
+only of suitable tables and arrays, then `obs` and `LearnAPI.data_interface` do not need
+to be overloaded. However, the user will get no performance benefits by using `obs` in
+that case.
-$(DOC_IMPLEMENTED_METHODS(:(obs), overloaded=true))
+When overloading `obs(algorithm, data)` to output new model-specific representations of
+data, it may be necessary to also overload [`LearnAPI.features`](@ref),
+[`LearnAPI.target`](@ref) (supervised algorithms), and/or [`LearnAPI.weights`](@ref) (if
+weights are supported), for extracting relevant parts of the representation.
## Sample implementation
-Suppose that `fit`, for an algorithm of type `Alg`, is to have the primary signature
-
-```julia
-fit(algorithm::Alg, X, y)
-```
-
-where `X` is a table, `y` a vector. Internally, the algorithm is to call a lower level
-function
-
-`train(A, names, y)`
-
-where `A = Tables.matrix(X)'` and `names` are the column names of `X`. Then relevant parts
-of an implementation might look like this:
-
-```julia
-# thin wrapper for algorithm-specific representation of data:
-struct ObsData{T}
- A::Matrix{T}
- names::Vector{Symbol}
- y::Vector{T}
-end
-
-# (indirect) implementation of `getobs/numobs`:
-Base.getindex(data::ObsData, I) =
- ObsData(data.A[:,I], data.names, y[I])
-Base.length(data::ObsData, I) = length(data.y)
-
-# implementation of `obs`:
-function LearnAPI.obs(::typeof(fit), ::Alg, X, y)
- table = Tables.columntable(X)
- names = Tables.columnnames(table) |> collect
- return ObsData(Tables.matrix(table)', names, y)
-end
-
-# implementation of `obsfit`:
-function LearnAPI.obsfit(algorithm::Alg, data::ObsData; verbosity=1)
- coremodel = train(data.A, data.names, data.y)
- data.verbosity > 0 && @info "Training using these features: $names."
-
- return model
-end
-```
-
-## When is overloading `obs` optional?
+Refer to the "Anatomy of an Implementation" section of the LearnAPI.jl
+[manual](https://juliaai.github.io/LearnAPI.jl/dev/).
-Overloading `obs` is optional, for a given `typeof(algorithm)` and `typeof(fun)`, if the
-components of `data` in the standard call `func(algorithm_or_model, data...)` are already
-expected to separately implement the `getobs`/`numbobs` interface. This is true for arrays
-whose last dimension is the observation dimension, and for suitable tables.
"""
-obs(func, alg, data...) = data
+obs(algorithm_or_model, data) = data
diff --git a/src/predict_transform.jl b/src/predict_transform.jl
index 71e4e730..a87cf07b 100644
--- a/src/predict_transform.jl
+++ b/src/predict_transform.jl
@@ -1,4 +1,4 @@
- function DOC_IMPLEMENTED_METHODS(name; overloaded=false)
+function DOC_IMPLEMENTED_METHODS(name; overloaded=false)
word = overloaded ? "overloaded" : "implemented"
"If $word, you must include `$name` in the tuple returned by the "*
"[`LearnAPI.functions`](@ref) trait. "
@@ -8,20 +8,12 @@ const OPERATIONS = (:predict, :transform, :inverse_transform)
const DOC_OPERATIONS_LIST_SYMBOL = join(map(op -> "`:$op`", OPERATIONS), ", ")
const DOC_OPERATIONS_LIST_FUNCTION = join(map(op -> "`LearnAPI.$op`", OPERATIONS), ", ")
-DOC_ARGUMENTS(func) =
-"""
-- `data`: tuple of data objects with a common number of observations, for example,
- `data = (X, y, w)` where `X` is a table of features, `y` is a target vector with the
- same number of rows, and `w` a vector of per-observation weights.
-
-"""
-
DOC_MUTATION(op) =
"""
If [`LearnAPI.predict_or_transform_mutates(algorithm)`](@ref) is overloaded to return
`true`, then `$op` may mutate it's first argument, but not in a way that alters the
- result of a subsequent call to `obspredict`, `obstransform` or
+ result of a subsequent call to `predict`, `transform` or
`inverse_transform`. This is necessary for some non-generalizing algorithms but is
otherwise discouraged. See more at [`fit`](@ref).
@@ -40,75 +32,41 @@ DOC_MINIMIZE(func) =
"""
-# # METHOD STUBS/FALLBACKS
-
-"""
- predict(model, kind_of_proxy::LearnAPI.KindOfProxy, data...)
- predict(model, data...)
-
-The first signature returns target or target proxy predictions for input features `data`,
-according to some `model` returned by [`fit`](@ref) or [`obsfit`](@ref). Where supported,
-these are literally target predictions if `kind_of_proxy = LiteralTarget()`, and
-probability density/mass functions if `kind_of_proxy = Distribution()`. List all options
-with [`LearnAPI.kinds_of_proxy(algorithm)`](@ref), where `algorithm =
-LearnAPI.algorithm(model)`.
-
-The shortcut `predict(model, data...) = predict(model, LiteralTarget(), data...)` is also
-provided.
-
-# Arguments
-
-- `model` is anything returned by a call of the form `fit(algorithm, ...)`, for some
- LearnAPI-complaint `algorithm`.
-
-$(DOC_ARGUMENTS(:predict))
-
-# Example
-
-In the following, `algorithm` is some supervised learning algorithm with
-training features `X`, training target `y`, and test features `Xnew`:
-
-```julia
-model = fit(algorithm, X, y; verbosity=0)
-predict(model, LiteralTarget(), Xnew)
-```
-
-Note `predict ` does not mutate any argument, except in the special case
-`LearnAPI.predict_or_transform_mutates(algorithm) = true`.
-
-See also [`obspredict`](@ref), [`fit`](@ref), [`transform`](@ref),
-[`inverse_transform`](@ref).
-
-# Extended help
+DOC_DATA_INTERFACE(method) =
+ """
-# New implementations
+ ## Assumptions about data
-LearnAPI.jl provides the following definition of `predict` which is never to be directly
-overloaded:
+ By default, it is assumed that `data` supports the [`LearnAPI.RandomAccess`](@ref)
+ interface; this includes all matrices, with observations-as-columns, most tables, and
+ tuples thereof). See [`LearnAPI.RandomAccess`](@ref) for details. If this is not the
+ case then an implementation must either: (i) overload [`obs`](@ref) to articulate how
+ provided data can be transformed into a form that does support
+ [`LearnAPI.RandomAccess`](@ref); or (ii) overload the trait
+ [`LearnAPI.data_interface`](@ref) to specify a more relaxed data API. Refer to
+ document strings for details.
-```julia
-predict(model, kop::LearnAPI.KindOfProxy, data...) =
- obspredict(model, kop, obs(predict, LearnAPI.algorithm(model), data...))
-```
+ """
-Rather, new algorithms overload [`obspredict`](@ref).
-"""
-predict(model, kind_of_proxy::KindOfProxy, data...) =
- obspredict(model, kind_of_proxy, obs(predict, algorithm(model), data...))
-predict(model, data...) = predict(model, LiteralTarget(), data...)
+# # METHOD STUBS/FALLBACKS
"""
- obspredict(model, kind_of_proxy::LearnAPI.KindOfProxy, obsdata)
+ predict(model, kind_of_proxy::LearnAPI.KindOfProxy, data)
+ predict(model, data)
+
+The first signature returns target predictions, or proxies for target predictions, for
+input features `data`, according to some `model` returned by [`fit`](@ref). Where
+supported, these are literally target predictions if `kind_of_proxy = Point()`,
+and probability density/mass functions if `kind_of_proxy = Distribution()`. List all
+options with [`LearnAPI.kinds_of_proxy(algorithm)`](@ref), where `algorithm =
+LearnAPI.algorithm(model)`.
-Similar to `predict` but consumes algorithm-specific representations of input data,
-`obsdata`, as returned by `obs(predict, algorithm, data...)`. Here `data...` is the form of
-data expected in the main [`predict`](@ref) method. Alternatively, such `obsdata` may be
-replaced by a resampled version, where resampling is performed using `MLUtils.getobs`
-(always supported).
+The shortcut `predict(model, data)` calls the first method with an algorithm-specific
+`kind_of_proxy`, namely the first element of [`LearnAPI.kinds_of_proxy(algorithm)`](@ref),
+which lists all supported target proxies.
-For some algorithms and workflows, `obspredict` will have a performance benefit over
-[`predict`](@ref). See more at [`obs`](@ref).
+The argument `model` is anything returned by a call of the form `fit(algorithm, ...)`.
# Example
@@ -116,141 +74,103 @@ In the following, `algorithm` is some supervised learning algorithm with
training features `X`, training target `y`, and test features `Xnew`:
```julia
-model = fit(algorithm, X, y)
-obsdata = obs(predict, algorithm, Xnew)
-ŷ = obspredict(model, LiteralTarget(), obsdata)
-@assert ŷ == predict(model, LiteralTarget(), Xnew)
+model = fit(algorithm, (X, y)) # or `fit(algorithm, X, y)`
+predict(model, Point(), Xnew)
```
-See also [`predict`](@ref), [`fit`](@ref), [`transform`](@ref),
-[`inverse_transform`](@ref), [`obs`](@ref).
+See also [`fit`](@ref), [`transform`](@ref), [`inverse_transform`](@ref).
# Extended help
+If `predict` supports data in the form of a tuple `data = (X1, ..., Xn)`, then a slurping
+signature is also provided, as in `predict(model, X1, ..., Xn)`.
+
+Note `predict ` does not mutate any argument, except in the special case
+`LearnAPI.predict_or_transform_mutates(algorithm) = true`.
+
# New implementations
-Implementation of `obspredict` is optional, but required to enable `predict`. The method
-must also handle `obsdata` in the case it is replaced by `MLUtils.getobs(obsdata, I)` for
-some collection `I` of indices. If [`obs`](@ref) is not overloaded, then `obsdata = data`,
-where `data...` is what the standard [`predict`](@ref) call expects, as in the call
-`predict(model, kind_of_proxy, data...)`. Note `data` is always a tuple, even if `predict`
-has only one data argument. See more at [`obs`](@ref).
+If there is no notion of a "target" variable in the LearnAPI.jl sense, or you need an
+operation with an inverse, implement [`transform`](@ref) instead.
+Implementation is optional. Only the first signature is implemented, but each
+`kind_of_proxy` that gets an implementation must be added to the list returned by
+[`LearnAPI.kinds_of_proxy`](@ref).
-$(DOC_MUTATION(:obspredict))
+$(DOC_IMPLEMENTED_METHODS(":predict"))
-If overloaded, you must include both `LearnAPI.obspredict` and `LearnAPI.predict` in the
-list of methods returned by the [`LearnAPI.functions`](@ref) trait.
+$(DOC_MINIMIZE(:predict))
-An implementation is provided for each kind of target proxy you wish to support. See the
-LearnAPI.jl documentation for options. Each supported `kind_of_proxy` instance should be
-listed in the return value of the [`LearnAPI.kinds_of_proxy(algorithm)`](@ref) trait.
+$(DOC_MUTATION(:predict))
-$(DOC_MINIMIZE(:obspredict))
+$(DOC_DATA_INTERFACE(:predict))
"""
-function obspredict end
+predict(model, data) = predict(model, kinds_of_proxy(algorithm(model)) |> first, data)
-"""
- transform(model, data...)
+# automatic slurping of multiple data arguments:
+predict(model, k::KindOfProxy, data1, data2, datas...; kwargs...) =
+ predict(model, k, (data1, data2, datas...); kwargs...)
+predict(model, data1, data2, datas...; kwargs...) =
+ predict(model, (data1, data2, datas...); kwargs...)
-Return a transformation of some `data`, using some `model`, as returned by [`fit`](@ref).
-# Arguments
-- `model` is anything returned by a call of the form `fit(algorithm, ...)`, for some
- LearnAPI-complaint `algorithm`.
+"""
+ transform(model, data)
-$(DOC_ARGUMENTS(:transform))
+Return a transformation of some `data`, using some `model`, as returned by
+[`fit`](@ref).
+
+For `data` that consists of a tuple, a slurping version is also provided, i.e., you can do
+`transform(model, X1, X2, X3)` in place of `transform(model, (X1, X2, X3))`.
# Example
-Here `X` and `Xnew` are data of the same form:
+Below, `X` and `Xnew` are data of the same form.
+
+For an `algorithm` that generalizes to new data ("learns"):
```julia
-# For an algorithm that generalizes to new data ("learns"):
model = fit(algorithm, X; verbosity=0)
transform(model, Xnew)
-
-# For a static (non-generalizing) transformer:
-model = fit(algorithm)
-transform(model, X)
```
-Note `transform` does not mutate any argument, except in the special case
-`LearnAPI.predict_or_transform_mutates(algorithm) = true`.
-
-See also [`obstransform`](@ref), [`fit`](@ref), [`predict`](@ref),
-[`inverse_transform`](@ref).
-
-# Extended help
-
-# New implementations
-
-LearnAPI.jl provides the following definition of `transform` which is never to be directly
-overloaded:
-
+For a static (non-generalizing) transformer:
```julia
-transform(model, data...) =
- obstransform(model, obs(predict, LearnAPI.algorithm(model), data...))
+model = fit(algorithm)
+W = transform(model, X)
```
-Rather, new algorithms overload [`obstransform`](@ref).
-
-"""
-transform(model, data...) =
- obstransform(model, obs(transform, LearnAPI.algorithm(model), data...))
-
-"""
- obstransform(model, kind_of_proxy::LearnAPI.KindOfProxy, obsdata)
-
-Similar to `transform` but consumes algorithm-specific representations of input data,
-`obsdata`, as returned by `obs(transform, algorithm, data...)`. Here `data...` is the
-form of data expected in the main [`transform`](@ref) method. Alternatively, such
-`obsdata` may be replaced by a resampled version, where resampling is performed using
-`MLUtils.getobs` (always supported).
-
-For some algorithms and workflows, `obstransform` will have a performance benefit over
-[`transform`](@ref). See more at [`obs`](@ref).
-
-# Example
-
-In the following, `algorithm` is some unsupervised learning algorithm with
-training features `X`, and test features `Xnew`:
+or, in one step (where supported):
```julia
-model = fit(algorithm, X, y)
-obsdata = obs(transform, algorithm, Xnew)
-W = obstransform(model, obsdata)
-@assert W == transform(model, Xnew)
+W = transform(algorithm, X)
```
-See also [`transform`](@ref), [`fit`](@ref), [`predict`](@ref),
-[`inverse_transform`](@ref), [`obs`](@ref).
+Note `transform` does not mutate any argument, except in the special case
+`LearnAPI.predict_or_transform_mutates(algorithm) = true`.
+
+See also [`fit`](@ref), [`predict`](@ref),
+[`inverse_transform`](@ref).
# Extended help
# New implementations
-Implementation of `obstransform` is optional, but required to enable `transform`. The
-method must also handle `obsdata` in the case it is replaced by `MLUtils.getobs(obsdata,
-I)` for some collection `I` of indices. If [`obs`](@ref) is not overloaded, then `obsdata
-= data`, where `data...` is what the standard [`transform`](@ref) call expects, as in the
-call `transform(model, data...)`. Note `data` is always a tuple, even if `transform` has
-only one data argument. See more at [`obs`](@ref).
+Implementation for new LearnAPI.jl algorithms is optional. A fallback provides the
+slurping version. $(DOC_IMPLEMENTED_METHODS(":transform"))
-$(DOC_MUTATION(:obstransform))
+$(DOC_MINIMIZE(:transform))
-If overloaded, you must include both `LearnAPI.obstransform` and `LearnAPI.transform` in
-the list of methods returned by the [`LearnAPI.functions`](@ref) trait.
+$(DOC_MUTATION(:transform))
-Each supported `kind_of_proxy` should be listed in the return value of the
-[`LearnAPI.kinds_of_proxy(algorithm)`](@ref) trait.
+$(DOC_DATA_INTERFACE(:transform))
-$(DOC_MINIMIZE(:obstransform))
"""
-function obstransform end
+transform(model, data1, data2...; kwargs...) =
+ transform(model, (data1, datas...); kwargs...) # automatic slurping
"""
inverse_transform(model, data)
@@ -259,20 +179,13 @@ Inverse transform `data` according to some `model` returned by [`fit`](@ref). He
"inverse" is to be understood broadly, e.g, an approximate
right inverse for [`transform`](@ref).
-# Arguments
-
-- `model`: anything returned by a call of the form `fit(algorithm, ...)`, for some
- LearnAPI-complaint `algorithm`.
-
-- `data`: something having the same form as the output of `transform(model, inputs...)`
-
# Example
In the following, `algorithm` is some dimension-reducing algorithm that generalizes to new
data (such as PCA); `Xtrain` is the training input and `Xnew` the input to be reduced:
```julia
-model = fit(algorithm, Xtrain; verbosity=0)
+model = fit(algorithm, Xtrain)
W = transform(model, Xnew) # reduced version of `Xnew`
Ŵ = inverse_transform(model, W) # embedding of `W` in original space
```
@@ -283,7 +196,7 @@ See also [`fit`](@ref), [`transform`](@ref), [`predict`](@ref).
# New implementations
-Implementation is optional. $(DOC_IMPLEMENTED_METHODS(:inverse_transform, ))
+Implementation is optional. $(DOC_IMPLEMENTED_METHODS(":inverse_transform"))
$(DOC_MINIMIZE(:inverse_transform))
diff --git a/src/target_weights_features.jl b/src/target_weights_features.jl
new file mode 100644
index 00000000..7df72646
--- /dev/null
+++ b/src/target_weights_features.jl
@@ -0,0 +1,76 @@
+"""
+ LearnAPI.target(algorithm, data) -> target
+
+Return, for each form of `data` supported in a call of the form [`fit(algorithm,
+data)`](@ref), the target variable part of `data`. If `nothing` is returned, the
+`algorithm` does not see a target variable in training (is unsupervised).
+
+Refer to LearnAPI.jl documentation for the precise meaning of "target".
+
+# New implementations
+
+A fallback returns `nothing`. Must be implemented if `fit` consumes data including a
+target variable.
+
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.target)"; overloaded=true))
+
+"""
+target(::Any, data) = nothing
+
+"""
+ LearnAPI.weights(algorithm, data) -> weights
+
+Return, for each form of `data` supported in a call of the form `[`fit(algorithm,
+data)`](@ref), the per-observation weights part of `data`. Where `nothing` is returned, no
+weights are part of `data`, which is to be interpreted as uniform weighting.
+
+# New implementations
+
+Overloading is optional. A fallback returns `nothing`.
+
+$(DOC_IMPLEMENTED_METHODS(":(LearnAPI.weights)"; overloaded=true))
+
+"""
+weights(::Any, data) = nothing
+
+"""
+ LearnAPI.features(algorithm, data)
+
+Return, for each form of `data` supported in a call of the form `[`fit(algorithm,
+data)`](@ref), the "features" part of `data` (as opposed to the target
+variable, for example).
+
+The returned object `X` may always be passed to `predict` or `transform`, where
+implemented, as in the following sample workflow:
+
+```julia
+model = fit(algorithm, data)
+X = features(data)
+ŷ = predict(algorithm, kind_of_proxy, X) # eg, `kind_of_proxy = Point()`
+```
+
+The return value has the same number of observations as `data` does. For supervised models
+(i.e., where `:(LearnAPI.target) in LearnAPI.functions(algorithm)`) `ŷ` above is generally
+intended to be an approximate proxy for `LearnAPI.target(algorithm, data)`, the training
+target.
+
+
+# New implementations
+
+The only contract `features` must satisfy is the one about passability of the output to
+`predict` or `transform`, for each supported input `data`. The following fallbacks
+typically make overloading `LearnAPI.features` unnecessary:
+
+```julia
+LearnAPI.features(algorithm, data) = data
+LearnAPI.features(algorithm, data::Tuple) = first(data)
+```
+
+Overloading may be necessary if [`obs(algorithm, data)`](@ref) is overloaded to return
+some algorithm-specific representation of training `data`. For density estimators, whose
+`fit` typically consumes *only* a target variable, you should overload this method to
+return `nothing`.
+
+"""
+features(algorithm, data) = data
+features(algorithm, data::Tuple) = first(data)
diff --git a/src/tools.jl b/src/tools.jl
index 7a211729..1b033f05 100644
--- a/src/tools.jl
+++ b/src/tools.jl
@@ -8,6 +8,27 @@ function name_value_pair(ex)
return (ex.args[1], ex.args[2])
end
+"""
+ @trait(TypeEx, trait1=value1, trait2=value2, ...)
+
+Overload a number of traits for algorithms of type `TypeEx`. For example, the code
+
+```julia
+@trait(
+ RidgeRegressor,
+ tags = ("regression", ),
+ doc_url = "https://some.cool.documentation",
+)
+```
+
+is equivalent to
+
+```julia
+LearnAPI.tags(::RidgeRegressor) = ("regression", ),
+LearnAPI.doc_url(::RidgeRegressor) = "https://some.cool.documentation",
+```
+
+"""
macro trait(algorithm_ex, exs...)
program = quote end
for ex in exs
@@ -20,28 +41,6 @@ macro trait(algorithm_ex, exs...)
return esc(program)
end
-# """
-# typename(x)
-
-# Return a symbolic representation of the name of `type(x)`, stripped of any type-parameters
-# and module qualifications. For example, if
-
-# typeof(x) = MLJBase.Machine{MLJAlgorithms.ConstantRegressor,true}
-
-# Then `typename(x)` returns `:Machine`.
-
-# """
-function typename(x)
- M = typeof(x)
- if isdefined(M, :name)
- return M.name.name
- elseif isdefined(M, :body)
- return typename(M.body)
- else
- return Symbol(string(M))
- end
-end
-
function is_uppercase(char::Char)
i = Int(char)
i > 64 && i < 91
diff --git a/src/traits.jl b/src/traits.jl
index 73c3b03a..93938007 100644
--- a/src/traits.jl
+++ b/src/traits.jl
@@ -3,7 +3,7 @@
const DOC_UNKNOWN =
"Returns `\"unknown\"` if the algorithm implementation has "*
- "failed to overload the trait. "
+ "not overloaded the trait. "
const DOC_ON_TYPE = "The value of the trait must depend only on the type of `algorithm`. "
DOC_ONLY_ONE(func) =
@@ -13,13 +13,21 @@ DOC_ONLY_ONE(func) =
"`LearnAPI.$(func)_observation_scitype`, "*
"`LearnAPI.$(func)_observation_type`."
+const DOC_EXPLAIN_EACHOBS =
+ """
+
+ Here, "for each `o` in `observations`" is understood in the sense of
+ [`LearnAPI.data_interface(algorithm)`](@ref). For example, if
+ `LearnAPI.data_interface(algorithm) == Base.HasLength()`, then this means "for `o` in
+ `MLUtils.eachobs(observations)`".
+
+ """
const TRAITS = [
+ :constructor,
:functions,
:kinds_of_proxy,
- :position_of_target,
- :position_of_weights,
- :descriptors,
+ :tags,
:is_pure_julia,
:pkg_name,
:pkg_license,
@@ -28,38 +36,67 @@ const TRAITS = [
:is_composite,
:human_name,
:iteration_parameter,
+ :data_interface,
:predict_or_transform_mutates,
- :fit_scitype,
:fit_observation_scitype,
- :fit_type,
- :fit_observation_type,
- :predict_input_scitype,
- :predict_output_scitype,
- :predict_input_type,
- :predict_output_type,
- :transform_input_scitype,
- :transform_output_scitype,
- :transform_input_type,
- :transform_output_type,
+ :target_observation_scitype,
:name,
:is_algorithm,
+ :target,
]
# # OVERLOADABLE TRAITS
+"""
+ Learn.API.constructor(algorithm)
+
+Return a keyword constructor that can be used to clone `algorithm`:
+
+```julia-repl
+julia> algorithm.lambda
+0.1
+julia> C = LearnAPI.constructor(algorithm)
+julia> algorithm2 = C(lambda=0.2)
+julia> algorithm2.lambda
+0.2
+```
+
+# New implementations
+
+All new implementations must overload this trait.
+
+Attach public LearnAPI.jl-related documentation for an algorithm to the constructor, not
+the algorithm struct.
+
+It must be possible to recover an algorithm from the constructor returned as follows:
+
+```julia
+properties = propertynames(algorithm)
+named_properties = NamedTuple{properties}(getproperty.(Ref(algorithm), properties))
+@assert algorithm == LearnAPI.constructor(algorithm)(; named_properties...)
+```
+
+The keyword constructor provided by `LearnAPI.constructor` must provide default values for
+all properties, with the exception of those that can take other LearnAPI.jl algorithms as
+values.
+
+"""
+function constructor end
+
"""
LearnAPI.functions(algorithm)
-Return a tuple of functions that can be sensibly applied to `algorithm`, or to objects
-having the same type as `algorithm`, or to associated models (objects returned by
-`fit(algorithm, ...)`. Algorithm traits are excluded.
+Return a tuple of expressions representing functions that can be meaningfully applied
+with `algorithm`, or an associated model (object returned by `fit(algorithm, ...)`, as the
+first argument. Algorithm traits (methods for which `algorithm` is the *only* argument)
+are excluded.
-In addition to functions, the returned tuple may include expressions, like
-`:(DecisionTree.print_tree)`, which reference functions not owned by LearnAPI.jl.
+The returned tuple may include expressions like `:(DecisionTree.print_tree)`, which
+reference functions not owned by LearnAPI.jl.
-The understanding is that `algorithm` is a LearnAPI-compliant object whenever this is
-non-empty.
+The understanding is that `algorithm` is a LearnAPI-compliant object whenever the return
+value is non-empty.
# Extended help
@@ -68,21 +105,25 @@ non-empty.
All new implementations must overload this trait. Here's a checklist for elements in the
return value:
-| function | needs explicit implementation? | include in returned tuple? |
-|----------------------|---------------------------------|----------------------------------|
-| `fit` | no | yes |
-| `obsfit` | yes | yes |
-| `minimize` | optional | yes |
-| `predict` | no | if `obspredict` is implemented |
-| `obspredict` | optional | if implemented |
-| `transform` | no | if `obstransform` is implemented |
-| `obstransform` | optional | if implemented |
-| `obs` | optional | yes |
-| `inverse_transform` | optional | if implemented |
-| `LearnAPI.algorithm` | yes | yes |
-
-Also include any implemented accessor functions. The LearnAPI.jl accessor functions are:
-$ACCESSOR_FUNCTIONS_LIST.
+| symbol | implementation/overloading compulsory? | include in returned tuple? |
+|-----------------------------------|----------------------------------------|------------------------------------|
+| `:(LearnAPI.fit)` | yes | yes |
+| `:(LearnAPI.algorithm)` | yes | yes |
+| `:(LearnAPI.minimize)` | no | yes |
+| `:(LearnAPI.obs)` | no | yes |
+| `:(LearnAPI.features)` | no | yes, unless `fit` consumes no data |
+| `:(LearnAPI.update)` | no | only if implemented |
+| `:(LearnAPI.update_observations)` | no | only if implemented |
+| `:(LearnAPI.update_features)` | no | only if implemented |
+| `:(LearnAPI.target)` | no | only if implemented |
+| `:(LearnAPI.weights)` | no | only if implemented |
+| `:(LearnAPI.predict)` | no | only if implemented |
+| `:(LearnAPI.transform)` | no | only if implemented |
+| `:(LearnAPI.inverse_transform)` | no | only if implemented |
+| | no | only if implemented |
+
+Also include any implemented accessor functions, both those owned by LearnaAPI.jl, and any
+algorithm-specific ones. The LearnAPI.jl accessor functions are: $ACCESSOR_FUNCTIONS_LIST.
"""
functions(::Any) = ()
@@ -91,20 +132,24 @@ functions(::Any) = ()
"""
LearnAPI.kinds_of_proxy(algorithm)
-Returns an tuple of all instances, `kind`, for which for which `predict(algorithm, kind,
+Returns a tuple of all instances, `kind`, for which for which `predict(algorithm, kind,
data...)` has a guaranteed implementation. Each such `kind` subtypes
-[`LearnAPI.KindOfProxy`](@ref). Examples are `LiteralTarget()` (for predicting actual
+[`LearnAPI.KindOfProxy`](@ref). Examples are `Point()` (for predicting actual
target values) and `Distributions()` (for predicting probability mass/density functions).
+The call `predict(model, data)` always returns `predict(model, kind, data)`, where `kind`
+is the first element of the trait's return value.
+
See also [`LearnAPI.predict`](@ref), [`LearnAPI.KindOfProxy`](@ref).
# Extended help
# New implementations
-Implementation is optional but recommended whenever `predict` is overloaded.
+Must be overloaded whenever `predict` is implemented.
-Elements of the returned tuple must be one of these: $CONCRETE_TARGET_PROXY_TYPES_LIST.
+Elements of the returned tuple must be one of the following, described further in
+LearnAPI.jl documentation: $CONCRETE_TARGET_PROXY_TYPES_LIST.
Suppose, for example, we have the following implementation of a supervised learner
returning only probabilistic predictions:
@@ -119,69 +164,47 @@ Then we can declare
@trait MyNewAlgorithmType kinds_of_proxy = (LearnaAPI.Distribution(),)
```
+LearnAPI.jl provides the fallback for `predict(model, data)`.
+
For more on target variables and target proxies, refer to the LearnAPI documentation.
"""
kinds_of_proxy(::Any) = ()
-"""
- LearnAPI.position_of_target(algorithm)
-
-Return the expected position of the target variable within `data` in calls of the form
-[`LearnAPI.fit`](@ref)`(algorithm, verbosity, data...)`.
-
-If this number is `0`, then no target is expected. If this number exceeds `length(data)`,
-then `data` is understood to exclude the target variable.
-
-"""
-position_of_target(::Any) = 0
-
-"""
- LearnAPI.position_of_weights(algorithm)
-
-Return the expected position of per-observation weights within `data` in
-calls of the form [`LearnAPI.fit`](@ref)`(algorithm, data...)`.
-
-If this number is `0`, then no weights are expected. If this number exceeds
-`length(data)`, then `data` is understood to exclude weights, which are assumed to be
-uniform.
-
-"""
-position_of_weights(::Any) = 0
-
-descriptors() = [
- :regression,
- :classification,
- :clustering,
- :gradient_descent,
- :iterative_algorithms,
- :incremental_algorithms,
- :dimension_reduction,
- :encoders,
- :static_algorithms,
- :missing_value_imputation,
- :ensemble_algorithms,
- :wrappers,
- :time_series_forecasting,
- :time_series_classification,
- :survival_analysis,
- :distribution_fitters,
- :Bayesian_algorithms,
- :outlier_detection,
- :collaborative_filtering,
- :text_analysis,
- :audio_analysis,
- :natural_language_processing,
- :image_processing,
+tags() = [
+ "regression",
+ "classification",
+ "clustering",
+ "gradient descent",
+ "iterative algorithms",
+ "incremental algorithms",
+ "dimension reduction",
+ "encoders",
+ "feature engineering",
+ "static algorithms",
+ "missing value imputation",
+ "ensemble algorithms",
+ "wrappers",
+ "time series forecasting",
+ "time series classification",
+ "survival analysis",
+ "density estimation",
+ "Bayesian algorithms",
+ "outlier detection",
+ "collaborative filtering",
+ "text analysis",
+ "audio analysis",
+ "natural language processing",
+ "image processing",
]
-const DOC_DESCRIPTORS_LIST = join(map(d -> "`:$d`", descriptors()), ", ")
+const DOC_TAGS_LIST = join(map(d -> "`\"$d\"`", tags()), ", ")
"""
- LearnAPI.descriptors(algorithm)
+ LearnAPI.tags(algorithm)
-Lists one or more suggestive algorithm descriptors from this list: $DOC_DESCRIPTORS_LIST (do
-`LearnAPI.descriptors()` to reproduce).
+Lists one or more suggestive algorithm tags. Do `LearnAPI.tags()` to list
+all possible.
!!! warning
The value of this trait guarantees no particular behavior. The trait is
@@ -189,10 +212,10 @@ Lists one or more suggestive algorithm descriptors from this list: $DOC_DESCRIPT
# New implementations
-This trait should return a tuple of symbols, as in `(:classifier, :text_analysis)`.
+This trait should return a tuple of strings, as in `("classifier", "text analysis")`.
"""
-descriptors(::Any) = ()
+tags(::Any) = ()
"""
LearnAPI.is_pure_julia(algorithm)
@@ -248,14 +271,19 @@ doc_url(::Any) = "unknown"
"""
LearnAPI.load_path(algorithm)
-Return a string indicating where the `struct` for `typeof(algorithm)` can be found, beginning
-with the name of the package module defining it. For example, a return value of
-`"FastTrees.LearnAPI.DecisionTreeClassifier"` means the following julia code will return the
-algorithm type:
+Return a string indicating where in code the definition of the algorithm's constructor can
+be found, beginning with the name of the package module defining it. By "constructor" we
+mean the return value of [`LearnAPI.constructor(algorithm)`](@ref).
+
+# Implementation
+
+For example, a return value of `"FastTrees.LearnAPI.DecisionTreeClassifier"` means the
+following julia code will not error:
```julia
import FastTrees
-FastTrees.LearnAPI.DecisionTreeClassifier
+import LearnAPI
+@assert FastTrees.LearnAPI.DecisionTreeClassifier == LearnAPI.constructor(algorithm)
```
$DOC_UNKNOWN
@@ -271,7 +299,7 @@ load_path(::Any) = "unknown"
Returns `true` if one or more properties (fields) of `algorithm` may themselves be
algorithms, and `false` otherwise.
-See also `[LearnAPI.components]`(@ref).
+See also [`LearnAPI.components`](@ref).
# New implementations
@@ -289,8 +317,8 @@ is_composite(::Any) = false
"""
LearnAPI.human_name(algorithm)
-A human-readable string representation of `typeof(algorithm)`. Primarily intended for
-auto-generation of documentation.
+Return a human-readable string representation of `typeof(algorithm)`. Primarily intended
+for auto-generation of documentation.
# New implementations
@@ -300,7 +328,28 @@ to return `"K-nearest neighbors regressor"`. Ideally, this is a "concrete" noun
`"ridge regressor"` rather than an "abstract" noun like `"ridge regression"`.
"""
-human_name(M) = snakecase(name(M), delim=' ') # `name` defined below
+human_name(algorithm) = snakecase(name(algorithm), delim=' ') # `name` defined below
+
+"""
+ LearnAPI.data_interface(algorithm)
+
+Return the data interface supported by `algorithm` for accessing individual observations
+in representations of input data returned by [`obs(algorithm, data)`](@ref) or
+[`obs(model, data)`](@ref), whenever `algorithm == LearnAPI.algorithm(model)`. Here `data`
+is `fit`, `predict`, or `transform`-consumable data.
+
+Possible return values are [`LearnAPI.RandomAccess`](@ref),
+[`LearnAPI.FiniteIterable`](@ref), and [`LearnAPI.Iterable`](@ref).
+
+See also [`obs`](@ref).
+
+# New implementations
+
+The fallback returns [`LearnAPI.RandomAccess`](@ref), which applies to arrays, most
+tables, and tuples of these. See the doc-string for details.
+
+"""
+data_interface(::Any) = LearnAPI.RandomAccess()
"""
LearnAPI.predict_or_transform_mutates(algorithm)
@@ -331,58 +380,17 @@ Implement if algorithm is iterative. Returns a symbol or `nothing`.
iteration_parameter(::Any) = nothing
-"""
- LearnAPI.fit_scitype(algorithm)
-
-Return an upper bound on the scitype of `data` guaranteed to work when calling
-`fit(algorithm, data...)`.
-
-Specifically, if the return value is `S` and `ScientificTypes.scitype(data) <: S`, then
-all the following calls are guaranteed to work:
-
-```julia
-fit(algorithm, data...)
-obsdata = obs(fit, algorithm, data...)
-fit(algorithm, Obs(), obsdata)
-```
-
-See also [`LearnAPI.fit_type`](@ref), [`LearnAPI.fit_observation_scitype`](@ref),
-[`LearnAPI.fit_observation_type`](@ref).
-
-# New implementations
-
-Optional. The fallback return value is `Union{}`. $(DOC_ONLY_ONE(:fit))
-
-"""
-fit_scitype(::Any) = Union{}
-
"""
LearnAPI.fit_observation_scitype(algorithm)
-Return an upper bound on the scitype of observations guaranteed to work when calling
-`fit(algorithm, data...)`, independent of the type/scitype of the data container
-itself. Here "observations" is in the sense of MLUtils.jl. Assuming this trait has
-value different from `Union{}` the understanding is that `data` implements the MLUtils.jl
-`getobs`/`numobs` interface.
-
-Specifically, denoting the type returned above by `S`, supposing `S != Union{}`, and that
-user supplies `data` satisfying
-
-```julia
-ScientificTypes.scitype(MLUtils.getobs(data, i)) <: S
-```
+Return an upper bound `S` on the scitype of individual observations guaranteed to work
+when calling `fit`: if `observations = obs(algorithm, data)` and
+`ScientificTypes.scitype(o) <:S` for each `o` in `observations`, then the call
+`fit(algorithm, data)` is supported.
-for any valid index `i`, then all the following are guaranteed to work:
+$DOC_EXPLAIN_EACHOBS
-
-```julia
-fit(algorithm, data....)
-obsdata = obs(fit, algorithm, data...)
-fit(algorithm, Obs(), obsdata)
-```
-
-See also See also [`LearnAPI.fit_type`](@ref), [`LearnAPI.fit_scitype`](@ref),
-[`LearnAPI.fit_observation_type`](@ref).
+See also [`LearnAPI.target_observation_scitype`](@ref).
# New implementations
@@ -392,340 +400,42 @@ Optional. The fallback return value is `Union{}`. $(DOC_ONLY_ONE(:fit))
fit_observation_scitype(::Any) = Union{}
"""
- LearnAPI.fit_type(algorithm)
-
-Return an upper bound on the type of `data` guaranteed to work when calling
-`fit(algorithm, data...)`.
-
-Specifically, if the return value is `T` and `typeof(data) <: T`, then
-all the following calls are guaranteed to work:
+ LearnAPI.target_observation_scitype(algorithm)
-```julia
-fit(algorithm, data...)
-obsdata = obs(fit, algorithm, data...)
-fit(algorithm, Obs(), obsdata)
-```
-
-See also [`LearnAPI.fit_scitype`](@ref), [`LearnAPI.fit_observation_type`](@ref).
-[`LearnAPI.fit_observation_scitype`](@ref)
-
-# New implementations
-
-Optional. The fallback return value is `Union{}`. $(DOC_ONLY_ONE(:fit))
-
-"""
-fit_type(::Any) = Union{}
+Return an upper bound `S` on the scitype of each observation of an applicable target
+variable. Specifically:
-"""
- LearnAPI.fit_observation_type(algorithm)
+- If `:(LearnAPI.target) in LearnAPI.functions(algorithm)` (i.e., `fit` consumes target
+ variables) then "target" means anything returned by `LearnAPI.target(algorithm, data)`,
+ where `data` is an admissible argument in the call `fit(algorithm, data)`.
-Return an upper bound on the type of observations guaranteed to work when calling
-`fit(algorithm, data...)`, independent of the type/scitype of the data container
-itself. Here "observations" is in the sense of MLUtils.jl. Assuming this trait has value
-different from `Union{}` the understanding is that `data` implements the MLUtils.jl
-`getobs`/`numobs` interface.
+- `S` will always be an upper bound on the scitype of observations that could be
+ conceivably extracted from the output of [`predict`](@ref).
-Specifically, denoting the type returned above by `T`, supposing `T != Union{}`, and that
-user supplies `data` satisfying
+To illustate the second case, suppose we have
```julia
-typeof(MLUtils.getobs(data, i)) <: T
+model = fit(algorithm, data)
+ŷ = predict(model, Sampleable(), data_new)
```
-for any valid index `i`, then the following is guaranteed to work:
+Then each individual sample generated by each "observation" of `ŷ` (a vector of sampleable
+objects, say) will be bound in scitype by `S`.
-```julia
-fit(algorithm, data....)
-obsdata = obs(fit, algorithm, data...)
-fit(algorithm, Obs(), obsdata)
-```
-
-See also See also [`LearnAPI.fit_type`](@ref), [`LearnAPI.fit_scitype`](@ref),
-[`LearnAPI.fit_observation_scitype`](@ref).
+See also See also [`LearnAPI.fit_observation_scitype`](@ref).
# New implementations
-Optional. The fallback return value is `Union{}`. $(DOC_ONLY_ONE(:fit))
+Optional. The fallback return value is `Any`.
"""
-fit_observation_type(::Any) = Union{}
-
-function DOC_INPUT_SCITYPE(op)
- extra = op == :predict ? " kind_of_proxy," : ""
- ONLY = DOC_ONLY_ONE(op)
- """
- LearnAPI.$(op)_input_scitype(algorithm)
-
- Return an upper bound on the scitype of `data` guaranteed to work in the call
- `$op(algorithm,$extra data...)`.
-
- Specifically, if `S` is the value returned and `ScientificTypes.scitype(data) <: S`,
- then the following is guaranteed to work:
-
- ```julia
- $op(model,$extra data...)
- obsdata = obs($op, algorithm, data...)
- $op(model,$extra Obs(), obsdata)
- ```
- whenever `algorithm = LearnAPI.algorithm(model)`.
-
- See also [`LearnAPI.$(op)_input_type`](@ref).
-
- # New implementations
-
- Implementation is optional. The fallback return value is `Union{}`. $ONLY
-
- """
-end
-
-function DOC_INPUT_OBSERVATION_SCITYPE(op)
- extra = op == :predict ? " kind_of_proxy," : ""
- ONLY = DOC_ONLY_ONE(op)
- """
- LearnAPI.$(op)_observation_scitype(algorithm)
-
- Return an upper bound on the scitype of observations guaranteed to work when calling
- `$op(model,$extra data...)`, independent of the type/scitype of the data container
- itself. Here "observations" is in the sense of MLUtils.jl. Assuming this trait has
- value different from `Union{}` the understanding is that `data` implements the
- MLUtils.jl `getobs`/`numobs` interface.
-
- Specifically, denoting the type returned above by `S`, supposing `S != Union{}`, and
- that user supplies `data` satisfying
-
- ```julia
- ScientificTypes.scitype(MLUtils.getobs(data, i)) <: S
- ```
-
- for any valid index `i`, then all the following are guaranteed to work:
-
- ```julia
- $op(model,$extra data...)
- obsdata = obs($op, algorithm, data...)
- $op(model,$extra Obs(), obsdata)
- ```
- whenever `algorithm = LearnAPI.algorithm(model)`.
-
- See also See also [`LearnAPI.fit_type`](@ref), [`LearnAPI.fit_scitype`](@ref),
- [`LearnAPI.fit_observation_type`](@ref).
-
- # New implementations
-
- Optional. The fallback return value is `Union{}`. $ONLY
-
- """
-end
-
-function DOC_INPUT_TYPE(op)
- extra = op == :predict ? " kind_of_proxy," : ""
- ONLY = DOC_ONLY_ONE(op)
- """
- LearnAPI.$(op)_input_type(algorithm)
-
- Return an upper bound on the type of `data` guaranteed to work in the call
- `$op(algorithm,$extra data...)`.
-
- Specifically, if `T` is the value returned and `typeof(data) <: T`, then the following
- is guaranteed to work:
-
- ```julia
- $op(model,$extra data...)
- obsdata = obs($op, model, data...)
- $op(model,$extra Obs(), obsdata)
- ```
-
- See also [`LearnAPI.$(op)_input_scitype`](@ref).
-
- # New implementations
-
- Implementation is optional. The fallback return value is `Union{}`. Should not be
- overloaded if `LearnAPI.$(op)_input_scitype` is overloaded.
-
- """
-end
-
-function DOC_INPUT_OBSERVATION_TYPE(op)
- extra = op == :predict ? " kind_of_proxy," : ""
- ONLY = DOC_ONLY_ONE(op)
- """
- LearnAPI.$(op)_observation_type(algorithm)
-
- Return an upper bound on the type of observations guaranteed to work when calling
- `$op(model,$extra data...)`, independent of the type/scitype of the data container
- itself. Here "observations" is in the sense of MLUtils.jl. Assuming this trait has
- value different from `Union{}` the understanding is that `data` implements the
- MLUtils.jl `getobs`/`numobs` interface.
-
- Specifically, denoting the type returned above by `T`, supposing `T != Union{}`, and
- that user supplies `data` satisfying
-
- ```julia
- typeof(MLUtils.getobs(data, i)) <: T
- ```
-
- for any valid index `i`, then all the following are guaranteed to work:
-
- ```julia
- $op(model,$extra data...)
- obsdata = obs($op, algorithm, data...)
- $op(model,$extra Obs(), obsdata)
- ```
- whenever `algorithm = LearnAPI.algorithm(model)`.
-
- See also See also [`LearnAPI.fit_type`](@ref), [`LearnAPI.fit_scitype`](@ref),
- [`LearnAPI.fit_observation_type`](@ref).
-
- # New implementations
-
- Optional. The fallback return value is `Union{}`. $ONLY
-
- """
-end
-
-DOC_OUTPUT_SCITYPE(op) =
- """
- LearnAPI.$(op)_output_scitype(algorithm)
-
- Return an upper bound on the scitype of the output of the `$op` operation.
-
- See also [`LearnAPI.$(op)_input_scitype`](@ref).
-
- # New implementations
-
- Implementation is optional. The fallback return value is `Any`.
-
- """
-
-DOC_OUTPUT_TYPE(op) =
- """
- LearnAPI.$(op)_output_type(algorithm)
-
- Return an upper bound on the type of the output of the `$op` operation.
-
- # New implementations
-
- Implementation is optional. The fallback return value is `Any`.
-
- """
-
-"$(DOC_INPUT_SCITYPE(:predict))"
-predict_input_scitype(::Any) = Union{}
-
-"$(DOC_INPUT_OBSERVATION_SCITYPE(:predict))"
-predict_input_observation_scitype(::Any) = Union{}
-
-"$(DOC_INPUT_TYPE(:predict))"
-predict_input_type(::Any) = Union{}
-
-"$(DOC_INPUT_OBSERVATION_TYPE(:predict))"
-predict_input_observation_type(::Any) = Union{}
-
-"$(DOC_INPUT_SCITYPE(:transform))"
-transform_input_scitype(::Any) = Union{}
-
-"$(DOC_INPUT_OBSERVATION_SCITYPE(:transform))"
-transform_input_observation_scitype(::Any) = Union{}
-
-"$(DOC_INPUT_TYPE(:transform))"
-transform_input_type(::Any) = Union{}
-
-"$(DOC_INPUT_OBSERVATION_TYPE(:transform))"
-transform_input_observation_type(::Any) = Union{}
-
-"$(DOC_OUTPUT_SCITYPE(:transform))"
-transform_output_scitype(::Any) = Any
-
-"$(DOC_OUTPUT_TYPE(:transform))"
-transform_output_type(::Any) = Any
-
-
-# # TWO-ARGUMENT TRAITS
-
-# Here `s` is `:type` or `:scitype`:
-const DOC_PREDICT_OUTPUT(s) =
- """
- LearnAPI.predict_output_$s(algorithm, kind_of_proxy::KindOfProxy)
-
- Return an upper bound for the $(s)s of predictions of the specified form where
- supported, and otherwise return `Any`. For example, if
-
- ŷ = LearnAPI.predict(model, LearnAPI.Distribution(), data...)
-
- successfully returns (i.e., `algorithm` supports predictions of target probability
- distributions) then the following is guaranteed to hold:
-
- $(s)(ŷ) <: LearnAPI.predict_output_$(s)(algorithm, LearnAPI.Distribution())
-
- **Note.** This trait has a single-argument "convenience" version
- `LearnAPI.predict_output_$(s)(algorithm)` derived from this one, which returns a
- dictionary keyed on target proxy types.
-
- See also [`LearnAPI.KindOfProxy`](@ref), [`LearnAPI.predict`](@ref),
- [`LearnAPI.predict_input_$(s)`](@ref).
-
- # New implementations
-
- Overloading the trait is optional. Here's a sample implementation for a supervised
- regressor type `MyRgs` that only predicts actual values of the target:
-
- ```julia
- @trait MyRgs predict_output_$(s) = AbstractVector{ScientificTypesBase.Continuous}
- ```
-
- The fallback method returns `Any`.
-
- """
-
-"$(DOC_PREDICT_OUTPUT(:scitype))"
-predict_output_scitype(algorithm, kind_of_proxy) = Any
-
-"$(DOC_PREDICT_OUTPUT(:type))"
-predict_output_type(algorithm, kind_of_proxy) = Any
+target_observation_scitype(::Any) = Any
# # DERIVED TRAITS
-name(A) = string(typename(A))
-
-is_algorithm(A) = !isempty(functions(A))
-
+name(algorithm) = split(string(constructor(algorithm)), ".") |> last
+is_algorithm(algorithm) = !isempty(functions(algorithm))
preferred_kind_of_proxy(algorithm) = first(kinds_of_proxy(algorithm))
-
-const DOC_PREDICT_OUTPUT2(s) =
- """
- LearnAPI.predict_output_$(s)(algorithm)
-
- Return a dictionary of upper bounds on the $(s) of predictions, keyed on concrete
- subtypes of [`LearnAPI.KindOfProxy`](@ref). Each of these subtypes represents a
- different form of target prediction (`LiteralTarget`, `Distribution`,
- `SurvivalFunction`, etc) possibly supported by `algorithm`, but the existence of a key
- does not guarantee that form is supported.
-
- As an example, if
-
- ŷ = LearnAPI.predict(model, LearnAPI.Distribution(), data...)
-
- successfully returns (i.e., `algorithm` supports predictions of target probability
- distributions) then the following is guaranteed to hold:
-
- $(s)(ŷ) <: LearnAPI.predict_output_$(s)s(algorithm)[LearnAPI.Distribution]
-
- See also [`LearnAPI.KindOfProxy`](@ref), [`LearnAPI.predict`](@ref),
- [`LearnAPI.predict_input_$(s)`](@ref).
-
- # New implementations
-
- This single argument trait should not be overloaded. Instead, overload
- [`LearnAPI.predict_output_$(s)`](@ref)(algorithm, kind_of_proxy).
-
- """
-
-"$(DOC_PREDICT_OUTPUT2(:scitype))"
-predict_output_scitype(algorithm) =
- Dict(T => predict_output_scitype(algorithm, T())
- for T in CONCRETE_TARGET_PROXY_TYPES)
-
-"$(DOC_PREDICT_OUTPUT2(:type))"
-predict_output_type(algorithm) =
- Dict(T => predict_output_type(algorithm, T())
- for T in CONCRETE_TARGET_PROXY_TYPES)
+target(algorithm) = :(LearnAPI.target) in functions(algorithm)
+weights(algorithm) = :(LearnAPI.weights) in functions(algorithm)
diff --git a/src/types.jl b/src/types.jl
index e72c159e..e046384d 100644
--- a/src/types.jl
+++ b/src/types.jl
@@ -1,28 +1,6 @@
# # TARGET PROXIES
-const DOC_HOW_TO_LIST_PROXIES =
- "Run `LearnAPI.CONCRETE_TARGET_PROXY_TYPES` "*
- " to list all options. "
-
-
-"""
-
- LearnAPI.KindOfProxy
-
-Abstract type whose concrete subtypes `T` each represent a different kind of proxy for
-some target variable, associated with some algorithm. Instances `T()` are used to request
-the form of target predictions in [`predict`](@ref) calls.
-
-See LearnAPI.jl documentation for an explanation of "targets" and "target proxies".
-
-For example, `Distribution` is a concrete subtype of `LearnAPI.KindOfProxy` and a call
-like `predict(model, Distribution(), Xnew)` returns a data object whose observations are
-probability density/mass functions, assuming `algorithm` supports predictions of that
-form.
-
-$DOC_HOW_TO_LIST_PROXIES
-
-"""
+# see later for doc string:
abstract type KindOfProxy end
"""
@@ -32,7 +10,7 @@ Abstract subtype of [`LearnAPI.KindOfProxy`](@ref). If `kind_of_proxy` is an ins
`LearnAPI.IID` then, given `data` constisting of ``n`` observations, the
following must hold:
-- `ŷ = LearnAPI.predict(model, kind_of_proxy, data...)` is
+- `ŷ = LearnAPI.predict(model, kind_of_proxy, data)` is
data also consisting of ``n`` observations.
- The ``j``th observation of `ŷ`, for any ``j``, depends only on the ``j``th
@@ -40,10 +18,42 @@ following must hold:
See also [`LearnAPI.KindOfProxy`](@ref).
+# Extended help
+
+| type | form of an observation |
+|:-------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `LearnAPI.Point` | same as target observations; may have the interpretation of a 50% quantile, 50% expectile or mode |
+| `LearnAPI.Sampleable` | object that can be sampled to obtain object of the same form as target observation |
+| `LearnAPI.Distribution` | explicit probability density/mass function whose sample space is all possible target observations |
+| `LearnAPI.LogDistribution` | explicit log-probability density/mass function whose sample space is possible target observations |
+| `LearnAPI.Probability`¹ | numerical probability or probability vector |
+| `LearnAPI.LogProbability`¹ | log-probability or log-probability vector |
+| `LearnAPI.Parametric`¹ | a list of parameters (e.g., mean and variance) describing some distribution |
+| `LearnAPI.LabelAmbiguous` | collections of labels (in case of multi-class target) but without a known correspondence to the original target labels (and of possibly different number) as in, e.g., clustering |
+| `LearnAPI.LabelAmbiguousSampleable` | sampleable version of `LabelAmbiguous`; see `Sampleable` above |
+| `LearnAPI.LabelAmbiguousDistribution` | pdf/pmf version of `LabelAmbiguous`; see `Distribution` above |
+| `LearnAPI.LabelAmbiguousFuzzy` | same as `LabelAmbiguous` but with multiple values of indeterminant number |
+| `LearnAPI.Quantile`² | same as target but with quantile interpretation |
+| `LearnAPI.Expectile`² | same as target but with expectile interpretation |
+| `LearnAPI.ConfidenceInterval`² | confidence interval |
+| `LearnAPI.Fuzzy` | finite but possibly varying number of target observations |
+| `LearnAPI.ProbabilisticFuzzy` | as for `Fuzzy` but labeled with probabilities (not necessarily summing to one) |
+| `LearnAPI.SurvivalFunction` | survival function |
+| `LearnAPI.SurvivalDistribution` | probability distribution for survival time |
+| `LearnAPI.SurvivalHazardFunction` | hazard function for survival time |
+| `LearnAPI.OutlierScore` | numerical score reflecting degree of outlierness (not necessarily normalized) |
+| `LearnAPI.Continuous` | real-valued approximation/interpolation of a discrete-valued target, such as a count (e.g., number of phone calls) |
+
+¹Provided for completeness but discouraged to avoid [ambiguities in
+representation](https://github.com/alan-turing-institute/MLJ.jl/blob/dev/paper/paper.md#a-unified-approach-to-probabilistic-predictions-and-their-evaluation).
+
+²The level will be controlled by a hyper-parameter; models providing only quantiles or
+expectiles at 50% will provide `Point` instead.
+
"""
abstract type IID <: KindOfProxy end
-struct LiteralTarget <: IID end
+struct Point <: IID end
struct Sampleable <: IID end
struct Distribution <: IID end
struct LogDistribution <: IID end
@@ -53,22 +63,65 @@ struct Parametric <: IID end
struct LabelAmbiguous <: IID end
struct LabelAmbiguousSampleable <: IID end
struct LabelAmbiguousDistribution <: IID end
+struct LabelAmbiguousFuzzy <: IID end
struct ConfidenceInterval <: IID end
-struct Set <: IID end
-struct ProbabilisticSet <: IID end
+struct Fuzzy <: IID end
+struct ProbabilisticFuzzy <: IID end
struct SurvivalFunction <: IID end
struct SurvivalDistribution <: IID end
+struct HazardFunction <: IID end
struct OutlierScore <: IID end
struct Continuous <: IID end
+struct Quantile <: IID end
+struct Expectile <: IID end
-# struct None <: KindOfProxy end
-struct JointSampleable <: KindOfProxy end
-struct JointDistribution <: KindOfProxy end
-struct JointLogDistribution <: KindOfProxy end
+
+"""
+ Joint <: KindOfProxy
+
+Abstract subtype of [`LearnAPI.KindOfProxy`](@ref). If `kind_of_proxy` is an instance of
+`LearnAPI.Joint` then, given `data` consisting of ``n`` observations, `predict(model,
+kind_of_proxy, data)` represents a *single* probability distribution for the sample
+space ``Y^n``, where ``Y`` is the space from which the target variable takes its values.
+
+| type `T` | form of output of `predict(model, ::T, data)` |
+|:-------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `LearnAPI.JointSampleable` | object that can be sampled to obtain a *vector* whose elements have the form of target observations; the vector length matches the number of observations in `data`. |
+| `LearnAPI.JointDistribution` | explicit probability density/mass function whose sample space is vectors of target observations; the vector length matches the number of observations in `data` |
+| `LearnAPI.JointLogDistribution` | explicit log-probability density/mass function whose sample space is vectors of target observations; the vector length matches the number of observations in `data` |
+
+"""
+abstract type Joint <: KindOfProxy end
+struct JointSampleable <: Joint end
+struct JointDistribution <: Joint end
+struct JointLogDistribution <: Joint end
+
+"""
+ Single <: KindOfProxy
+
+Abstract subtype of [`LearnAPI.KindOfProxy`](@ref). It applies only to algorithms for
+which `predict` has no data argument, i.e., is of the form `predict(model,
+kind_of_proxy)`. An example is an algorithm learning a probability distribution from
+samples, and we regard the samples as drawn from the "target" variable. If in this case,
+`kind_of_proxy` is an instance of `LearnAPI.Single` then, `predict(algorithm)` returns a
+single object representing a probability distribution.
+
+| type `T` | form of output of `predict(model, ::T)` |
+|:--------------------------------:|:-----------------------------------------------------------------------|
+| `LearnAPI.SingleSampleable` | object that can be sampled to obtain a single target observation |
+| `LearnAPI.SingleDistribution` | explicit probability density/mass function for sampling the target |
+| `LearnAPI.SingleLogDistribution` | explicit log-probability density/mass function for sampling the target |
+
+"""
+abstract type Single <: KindOfProxy end
+struct SingleSampeable <: Single end
+struct SingleDistribution <: Single end
+struct SingleLogDistribution <: Single end
const CONCRETE_TARGET_PROXY_TYPES = [
subtypes(IID)...,
- setdiff(subtypes(KindOfProxy), subtypes(IID))...,
+ subtypes(Single)...,
+ subtypes(Joint)...,
]
const CONCRETE_TARGET_PROXY_TYPES_SYMBOLS = map(CONCRETE_TARGET_PROXY_TYPES) do T
@@ -77,8 +130,109 @@ end
const CONCRETE_TARGET_PROXY_TYPES_LIST = join(
map(CONCRETE_TARGET_PROXY_TYPES_SYMBOLS) do s
- "`$s`"
+ "`$s()`"
end,
", ",
" and ",
)
+
+const DOC_HOW_TO_LIST_PROXIES =
+ "The instances of [`LearnAPI.KindOfProxy`](@ref) are: "*
+ "$(LearnAPI.CONCRETE_TARGET_PROXY_TYPES_LIST). "
+
+
+"""
+
+ LearnAPI.KindOfProxy
+
+Abstract type whose concrete subtypes `T` each represent a different kind of proxy for
+some target variable, associated with some algorithm. Instances `T()` are used to request
+the form of target predictions in [`predict`](@ref) calls.
+
+See LearnAPI.jl documentation for an explanation of "targets" and "target proxies".
+
+For example, `Distribution` is a concrete subtype of `LearnAPI.KindOfProxy` and a call
+like `predict(model, Distribution(), Xnew)` returns a data object whose observations are
+probability density/mass functions, assuming `algorithm` supports predictions of that
+form.
+
+$DOC_HOW_TO_LIST_PROXIES
+
+"""
+KindOfProxy
+
+
+# # DATA INTERFACES
+
+abstract type DataInterface end
+abstract type Finite <: DataInterface end
+
+"""
+ LearnAPI.RandomAccess
+
+A data interface type. We say that `data` implements the `RandomAccess` interface if
+`data` implements the methods `getobs` and `numobs` from MLUtils.jl. The first method
+allows one to grab observations specified by an arbitrary index set, as in
+`MLUtils.getobs(data, [2, 3, 5])`, while the second method returns the total number of
+available observations, which is assumed to be known and finite.
+
+All arrays implement `RandomAccess`, with the last index being the observation index
+(observations-as-columns in matrices).
+
+A Tables.jl compatible table `data` implements `RandomAccess` if `Tables.istable(data)` is
+true and if `data` implements `DataAPI.nrows`. This includes many tables, and in
+particular, `DataFrame`s. Tables that are also tuples are excluded.
+
+Any tuple of objects implementing `RandomAccess` also implements `RandomAccess`.
+
+If [`LearnAPI.data_interface(algorithm)`](@ref) takes the value `RandomAccess()`, then
+[`obs`](@ref)`(algorithm, ...)` is guaranteed to return objects implementing the
+`RandomAccess` interface, and the same holds for `obs(model, ...)`, whenever
+`LearnAPI.algorithm(model) == algorithm`.
+
+# Implementing `RandomAccess` for new data types
+
+Typically, to implement `RandomAccess` for a new data type requires only implementing
+`Base.getindex` and `Base.length`, which are the fallbacks for `MLUtils.getobs` and
+`MLUtils.numobs`, and this avoids making MLUtils.jl a package dependency.
+
+See also [`LearnAPI.FiniteIterable`](@ref), [`LearnAPI.Iterable`](@ref).
+"""
+struct RandomAccess <: Finite end
+
+"""
+ LearnAPI.FiniteIterable
+
+A data interface type. We say that `data` implements the `FiniteIterable` interface if
+it implements Julia's `iterate` interface, including `Base.length`, and if
+`Base.IteratorSize(typeof(data)) == Base.HasLength()`. For example, this is true if:
+
+- `data` implements the [`LearnAPI.RandomAccess`](@ref) interface (arrays and most tables)
+
+- `data isa MLUtils.DataLoader`, which includes output from `MLUtils.eachobs`.
+
+If [`LearnAPI.data_interface(algorithm)`](@ref) takes the value `FiniteIterable()`, then
+[`obs`](@ref)`(algorithm, ...)` is guaranteed to return objects implementing the
+`FiniteIterable` interface, and the same holds for `obs(model, ...)`, whenever
+`LearnAPI.algorithm(model) == algorithm`.
+
+See also [`LearnAPI.RandomAccess`](@ref), [`LearnAPI.Iterable`](@ref).
+"""
+struct FiniteIterable <: Finite end
+
+"""
+ LearnAPI.Iterable
+
+A data interface type. We say that `data` implements the `Iterable` interface if it
+implements Julia's basic `iterate` interface. (Such objects may not implement
+`MLUtils.numobs` or `Base.length`.)
+
+If [`LearnAPI.data_interface(algorithm)`](@ref) takes the value `Iterable()`, then
+[`obs`](@ref)`(algorithm, ...)` is guaranteed to return objects implementing `Iterable`,
+and the same holds for `obs(model, ...)`, whenever `LearnAPI.algorithm(model) ==
+algorithm`.
+
+See also [`LearnAPI.FiniteIterable`](@ref), [`LearnAPI.RandomAccess`](@ref).
+
+"""
+struct Iterable <: DataInterface end
diff --git a/test/integration/regression.jl b/test/integration/regression.jl
index 2c5d9d70..ba68cef1 100644
--- a/test/integration/regression.jl
+++ b/test/integration/regression.jl
@@ -7,16 +7,24 @@ import DataFrames
# # NAIVE RIDGE REGRESSION WITH NO INTERCEPTS
-# We overload `obs` to expose internal representation of input data. See later for a
-# simpler variation using the `obs` fallback.
+# We overload `obs` to expose internal representation of data. See later for a simpler
+# variation using the `obs` fallback.
+# no docstring here - that goes with the constructor
struct Ridge
lambda::Float64
end
-Ridge(; lambda=0.1) = Ridge(lambda)
-struct RidgeFitObs{T}
- A::Matrix{T} # p x n
+"""
+ Ridge(; lambda=0.1)
+
+Instantiate a ridge regression algorithm, with regularization of `lambda`.
+
+"""
+Ridge(; lambda=0.1) = Ridge(lambda) # LearnAPI.constructor defined later
+
+struct RidgeFitObs{T,M<:AbstractMatrix{T}}
+ A::M # p x n
names::Vector{Symbol}
y::Vector{T}
end
@@ -27,23 +35,28 @@ struct RidgeFitted{T,F}
feature_importances::F
end
+LearnAPI.algorithm(model::RidgeFitted) = model.algorithm
+
Base.getindex(data::RidgeFitObs, I) =
RidgeFitObs(data.A[:,I], data.names, data.y[I])
Base.length(data::RidgeFitObs, I) = length(data.y)
-function LearnAPI.obs(::typeof(fit), ::Ridge, X, y)
+# observations for consumption by `fit`:
+function LearnAPI.obs(::Ridge, data)
+ X, y = data
table = Tables.columntable(X)
names = Tables.columnnames(table) |> collect
- RidgeFitObs(Tables.matrix(table, transpose=true), names, y)
+ RidgeFitObs(Tables.matrix(table)', names, y)
end
-function LearnAPI.obsfit(algorithm::Ridge, fitdata::RidgeFitObs, verbosity)
+# for observations:
+function LearnAPI.fit(algorithm::Ridge, observations::RidgeFitObs; verbosity=1)
# unpack hyperparameters and data:
lambda = algorithm.lambda
- A = fitdata.A
- names = fitdata.names
- y = fitdata.y
+ A = observations.A
+ names = observations.names
+ y = observations.y
# apply core algorithm:
coefficients = (A*A' + algorithm.lambda*I)\(A*y) # 1 x p matrix
@@ -61,13 +74,27 @@ function LearnAPI.obsfit(algorithm::Ridge, fitdata::RidgeFitObs, verbosity)
end
-LearnAPI.algorithm(model::RidgeFitted) = model.algorithm
+# for unprocessed `data = (X, y)`:
+LearnAPI.fit(algorithm::Ridge, data; kwargs...) =
+ fit(algorithm, obs(algorithm, data); kwargs...)
+
+# extracting stuff from training data:
+LearnAPI.target(::Ridge, data) = last(data)
+LearnAPI.target(::Ridge, observations::RidgeFitObs) = observations.y
+LearnAPI.features(::Ridge, observations::RidgeFitObs) = observations.A
+
+# observations for consumption by `predict`:
+LearnAPI.obs(::RidgeFitted, X) = Tables.matrix(X)'
-LearnAPI.obspredict(model::RidgeFitted, ::LiteralTarget, Anew::Matrix) =
- ((model.coefficients)'*Anew)'
+# matrix input:
+LearnAPI.predict(model::RidgeFitted, ::Point, observations::AbstractMatrix) =
+ observations'*model.coefficients
-LearnAPI.obs(::typeof(predict), ::Ridge, X) = Tables.matrix(X, transpose=true)
+# tabular input:
+LearnAPI.predict(model::RidgeFitted, ::Point, Xnew) =
+ predict(model, Point(), obs(model, Xnew))
+# accessor function:
LearnAPI.feature_importances(model::RidgeFitted) = model.feature_importances
LearnAPI.minimize(model::RidgeFitted) =
@@ -75,31 +102,37 @@ LearnAPI.minimize(model::RidgeFitted) =
@trait(
Ridge,
- position_of_target=2,
- kinds_of_proxy = (LiteralTarget(),),
+ constructor = Ridge,
+ kinds_of_proxy = (Point(),),
+ tags = ("regression",),
functions = (
- fit,
- obsfit,
- minimize,
- predict,
- obspredict,
- obs,
- LearnAPI.algorithm,
- LearnAPI.feature_importances,
- )
+ :(LearnAPI.fit),
+ :(LearnAPI.algorithm),
+ :(LearnAPI.minimize),
+ :(LearnAPI.obs),
+ :(LearnAPI.features),
+ :(LearnAPI.target),
+ :(LearnAPI.predict),
+ :(LearnAPI.feature_importances),
+ )
)
-n = 10 # number of observations
+# synthetic test data:
+n = 30 # number of observations
train = 1:6
test = 7:10
a, b, c = rand(n), rand(n), rand(n)
X = (; a, b, c)
X = DataFrames.DataFrame(X)
y = 2a - b + 3c + 0.05*rand(n)
+data = (X, y)
@testset "test an implementation of ridge regression" begin
algorithm = Ridge(lambda=0.5)
- @test LearnAPI.obs in LearnAPI.functions(algorithm)
+ @test :(LearnAPI.obs) in LearnAPI.functions(algorithm)
+
+ @test LearnAPI.target(algorithm, data) == y
+ @test LearnAPI.features(algorithm, data) == X
# verbose fitting:
@test_logs(
@@ -112,7 +145,7 @@ y = 2a - b + 3c + 0.05*rand(n)
),
)
- # quite fitting:
+ # quiet fitting:
model = @test_logs(
fit(
algorithm,
@@ -122,14 +155,16 @@ y = 2a - b + 3c + 0.05*rand(n)
),
)
- ŷ = predict(model, LiteralTarget(), Tables.subset(X, test))
+ ŷ = predict(model, Point(), Tables.subset(X, test))
@test ŷ isa Vector{Float64}
@test predict(model, Tables.subset(X, test)) == ŷ
- fitdata = LearnAPI.obs(fit, algorithm, X, y)
- predictdata = LearnAPI.obs(predict, algorithm, X)
- model = obsfit(algorithm, MLUtils.getobs(fitdata, train); verbosity=1)
- @test obspredict(model, LiteralTarget(), MLUtils.getobs(predictdata, test)) == ŷ
+ fitobs = LearnAPI.obs(algorithm, data)
+ predictobs = LearnAPI.obs(model, X)
+ model = fit(algorithm, MLUtils.getobs(fitobs, train); verbosity=0)
+ @test LearnAPI.target(algorithm, fitobs) == y
+ @test predict(model, Point(), MLUtils.getobs(predictobs, test)) ≈ ŷ
+ @test predict(model, LearnAPI.features(algorithm, fitobs)) ≈ predict(model, X)
@test LearnAPI.feature_importances(model) isa Vector{<:Pair{Symbol}}
@@ -140,19 +175,28 @@ y = 2a - b + 3c + 0.05*rand(n)
recovered_model = deserialize(filename)
@test LearnAPI.algorithm(recovered_model) == algorithm
- @test obspredict(
+ @test predict(
recovered_model,
- LiteralTarget(),
- MLUtils.getobs(predictdata, test)
- ) == ŷ
+ Point(),
+ MLUtils.getobs(predictobs, test)
+ ) ≈ ŷ
+
end
# # VARIATION OF RIDGE REGRESSION THAT USES FALLBACK OF LearnAPI.obs
+# no docstring here - that goes with the constructor
struct BabyRidge
lambda::Float64
end
-BabyRidge(; lambda=0.1) = BabyRidge(lambda)
+
+"""
+ BabyRidge(; lambda=0.1)
+
+Instantiate a ridge regression algorithm, with regularization of `lambda`.
+
+"""
+BabyRidge(; lambda=0.1) = BabyRidge(lambda) # LearnAPI.constructor defined later
struct BabyRidgeFitted{T,F}
algorithm::BabyRidge
@@ -160,18 +204,17 @@ struct BabyRidgeFitted{T,F}
feature_importances::F
end
-function LearnAPI.obsfit(algorithm::BabyRidge, data, verbosity)
+function LearnAPI.fit(algorithm::BabyRidge, data; verbosity=1)
X, y = data
lambda = algorithm.lambda
-
table = Tables.columntable(X)
names = Tables.columnnames(table) |> collect
- A = Tables.matrix(table, transpose=true)
+ A = Tables.matrix(table)'
# apply core algorithm:
- coefficients = (A*A' + algorithm.lambda*I)\(A*y) # 1 x p matrix
+ coefficients = (A*A' + algorithm.lambda*I)\(A*y) # vector
feature_importances = nothing
@@ -179,41 +222,49 @@ function LearnAPI.obsfit(algorithm::BabyRidge, data, verbosity)
end
+# extracting stuff from training data:
+LearnAPI.target(::BabyRidge, data) = last(data)
+
LearnAPI.algorithm(model::BabyRidgeFitted) = model.algorithm
-function LearnAPI.obspredict(model::BabyRidgeFitted, ::LiteralTarget, data)
- X = only(data)
- Anew = Tables.matrix(X, transpose=true)
- return ((model.coefficients)'*Anew)'
-end
+LearnAPI.predict(model::BabyRidgeFitted, ::Point, Xnew) =
+ Tables.matrix(Xnew)*model.coefficients
+
+LearnAPI.minimize(model::BabyRidgeFitted) =
+ BabyRidgeFitted(model.algorithm, model.coefficients, nothing)
@trait(
BabyRidge,
- position_of_target=2,
- kinds_of_proxy = (LiteralTarget(),),
+ constructor = BabyRidge,
+ kinds_of_proxy = (Point(),),
+ tags = ("regression",),
functions = (
- fit,
- obsfit,
- minimize,
- predict,
- obspredict,
- obs,
- LearnAPI.algorithm,
- LearnAPI.feature_importances,
- )
+ :(LearnAPI.fit),
+ :(LearnAPI.algorithm),
+ :(LearnAPI.minimize),
+ :(LearnAPI.obs),
+ :(LearnAPI.features),
+ :(LearnAPI.target),
+ :(LearnAPI.predict),
+ :(LearnAPI.feature_importances),
+ )
)
@testset "test a variation which does not overload LearnAPI.obs" begin
algorithm = BabyRidge(lambda=0.5)
model = fit(algorithm, Tables.subset(X, train), y[train]; verbosity=0)
- ŷ = predict(model, LiteralTarget(), Tables.subset(X, test))
+ ŷ = predict(model, Point(), Tables.subset(X, test))
@test ŷ isa Vector{Float64}
- fitdata = obs(fit, algorithm, X, y)
- predictdata = LearnAPI.obs(predict, algorithm, X)
- model = obsfit(algorithm, MLUtils.getobs(fitdata, train); verbosity=0)
- @test obspredict(model, LiteralTarget(), MLUtils.getobs(predictdata, test)) == ŷ
+ fitobs = obs(algorithm, data)
+ predictobs = LearnAPI.obs(model, X)
+ model = fit(algorithm, MLUtils.getobs(fitobs, train); verbosity=0)
+ @test predict(model, Point(), MLUtils.getobs(predictobs, test)) == ŷ ==
+ predict(model, MLUtils.getobs(predictobs, test))
+ @test LearnAPI.target(algorithm, data) == y
+ @test LearnAPI.predict(model, X) ≈
+ LearnAPI.predict(model, LearnAPI.features(algorithm, data))
end
true
diff --git a/test/integration/static_algorithms.jl b/test/integration/static_algorithms.jl
index e5295ddc..3812fbc6 100644
--- a/test/integration/static_algorithms.jl
+++ b/test/integration/static_algorithms.jl
@@ -13,13 +13,15 @@ import DataFrames
struct Selector
names::Vector{Symbol}
end
-Selector(; names=Symbol[]) = Selector(names)
+Selector(; names=Symbol[]) = Selector(names) # LearnAPI.constructor defined later
-LearnAPI.obsfit(algorithm::Selector, obsdata, verbosity) = algorithm
-LearnAPI.algorithm(model) = model # i.e., the algorithm
+# `fit` consumes no observational data, does no "learning", and just returns a thinly
+# wrapped `algorithm` (to distinguish it from the algorithm in dispatch):
+LearnAPI.fit(algorithm::Selector; verbosity=1) = Ref(algorithm)
+LearnAPI.algorithm(model) = model[]
-function LearnAPI.obstransform(algorithm::Selector, obsdata)
- X = only(obsdata)
+function LearnAPI.transform(model::Base.RefValue{Selector}, X)
+ algorithm = LearnAPI.algorithm(model)
table = Tables.columntable(X)
names = Tables.columnnames(table)
filtered_names = filter(in(algorithm.names), names)
@@ -28,35 +30,47 @@ function LearnAPI.obstransform(algorithm::Selector, obsdata)
return Tables.materializer(X)(filtered_table)
end
-@trait Selector functions = (
- fit,
- obsfit,
- minimize,
- transform,
- obstransform,
- obs,
- Learn.algorithm,
+# fit and transform in one go:
+function LearnAPI.transform(algorithm::Selector, X)
+ model = fit(algorithm)
+ transform(model, X)
+end
+
+@trait(
+ Selector,
+ constructor = Selector,
+ tags = ("feature engineering",),
+ functions = (
+ :(LearnAPI.fit),
+ :(LearnAPI.algorithm),
+ :(LearnAPI.minimize),
+ :(LearnAPI.obs),
+ :(LearnAPI.transform),
+ ),
)
@testset "test a static transformer" begin
algorithm = Selector(names=[:x, :w])
X = DataFrames.DataFrame(rand(3, 4), [:x, :y, :z, :w])
model = fit(algorithm) # no data arguments!
- @test model == algorithm
- @test transform(model, X) ==
- DataFrames.DataFrame(Tables.matrix(X)[:,[1,4]], [:x, :w])
+ @test LearnAPI.algorithm(model) == algorithm
+ W = transform(model, X)
+ @test W == DataFrames.DataFrame(Tables.matrix(X)[:,[1,4]], [:x, :w])
+ @test W == transform(algorithm, X)
end
# # FEATURE SELECTOR THAT REPORTS BYPRODUCTS OF SELECTION PROCESS
# This a variation of `Selector` above that stores the names of rejected features in the
-# model object, for inspection by an accessor function called `rejected`.
+# model object, for inspection by an accessor function called `rejected`. Since
+# `transform(model, X)` mutates `model` in this case, we must overload the
+# `predict_or_transform_mutates` trait.
struct Selector2
names::Vector{Symbol}
end
-Selector2(; names=Symbol[]) = Selector2(names)
+Selector2(; names=Symbol[]) = Selector2(names) # LearnAPI.constructor defined later
mutable struct Selector2Fit
algorithm::Selector2
@@ -66,13 +80,11 @@ end
LearnAPI.algorithm(model::Selector2Fit) = model.algorithm
rejected(model::Selector2Fit) = model.rejected
-# Here `obsdata=()` and we are just wrapping `algorithm` with a place-holder for
-# the `rejected` feature names.
-LearnAPI.obsfit(algorithm::Selector2, obsdata, verbosity) = Selector2Fit(algorithm)
+# Here we are wrapping `algorithm` with a place-holder for the `rejected` feature names.
+LearnAPI.fit(algorithm::Selector2; verbosity=1) = Selector2Fit(algorithm)
-# output the filtered table and add `rejected` field to model (mutatated)
-function LearnAPI.obstransform(model::Selector2Fit, obsdata)
- X = only(obsdata)
+# output the filtered table and add `rejected` field to model (mutatated!)
+function LearnAPI.transform(model::Selector2Fit, X)
table = Tables.columntable(X)
names = Tables.columnnames(table)
keep = LearnAPI.algorithm(model).names
@@ -83,18 +95,24 @@ function LearnAPI.obstransform(model::Selector2Fit, obsdata)
return Tables.materializer(X)(filtered_table)
end
+# fit and transform in one step:
+function LearnAPI.transform(algorithm::Selector2, X)
+ model = fit(algorithm)
+ transform(model, X)
+end
+
@trait(
Selector2,
+ constructor = Selector2,
predict_or_transform_mutates = true,
+ tags = ("feature engineering",),
functions = (
- fit,
- obsfit,
- minimize,
- transform,
- obstransform,
- obs,
- Learn.algorithm,
- :(MyPkg.rejected), # accessor function not owned by LearnAPI.jl
+ :(LearnAPI.fit),
+ :(LearnAPI.algorithm),
+ :(LearnAPI.minimize),
+ :(LearnAPI.obs),
+ :(LearnAPI.transform),
+ :(MyPkg.rejected), # accessor function not owned by LearnAPI.jl,
)
)
@@ -106,6 +124,7 @@ end
@test LearnAPI.algorithm(model) == algorithm
filtered = DataFrames.DataFrame(Tables.matrix(X)[:,[1,4]], [:x, :w])
@test transform(model, X) == filtered
+ @test transform(algorithm, X) == filtered
@test rejected(model) == [:y, :z]
end
diff --git a/test/runtests.jl b/test/runtests.jl
index 8697a248..93788bc4 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -4,6 +4,10 @@ using Test
include("tools.jl")
end
+@testset "traits.jl" begin
+ include("traits.jl")
+end
+
# # INTEGRATION TESTS
@testset "regression" begin
diff --git a/test/tools.jl b/test/tools.jl
index 1b2e942f..523f40e1 100644
--- a/test/tools.jl
+++ b/test/tools.jl
@@ -1,6 +1,5 @@
using LearnAPI
using Test
-using SparseArrays
module Fruit
using LearnAPI
@@ -22,13 +21,6 @@ import .Fruit
## HELPERS
-@testset "typename" begin
- @test LearnAPI.typename(Fruit.RedApple(1)) == :RedApple
- @test LearnAPI.typename(nothing) == :Nothing
- m = SparseArrays.sparse([1,2], [1,3], [0.5, 0.6])
- @test LearnAPI.typename(m) == :SparseMatrixCSC
-end
-
@testset "snakecase" begin
@test LearnAPI.snakecase("AnthonyBlaomsPetElk") ==
"anthony_blaoms_pet_elk"
diff --git a/test/traits.jl b/test/traits.jl
new file mode 100644
index 00000000..3000d016
--- /dev/null
+++ b/test/traits.jl
@@ -0,0 +1,16 @@
+module FruitSalad
+using LearnAPI
+
+struct RedApple{T}
+ x::T
+end
+
+LearnAPI.constructor(::RedApple) = RedApple
+
+end
+
+import .FruitSalad
+
+@testset "name" begin
+ @test LearnAPI.name(FruitSalad.RedApple(1)) == "RedApple"
+end
diff --git a/typos.toml b/typos.toml
new file mode 100644
index 00000000..8f5d6f5a
--- /dev/null
+++ b/typos.toml
@@ -0,0 +1,6 @@
+[default.extend-words]
+# Don't correct "mape" to "map"
+mape = "mape"
+yhat = "yhat"
+LSO ="LSO"
+datas = "datas"