Extending KFC Procedure
kfc_procedure is designed around small abstract interfaces and
registry-based factories.
That makes the package extensible in several places without changing the high-level KFC or COBRA orchestration code.
The current source exposes extension points for:
KFC layer
Bregman divergences
F-Step local models
C-Step combiners
COBRA layer
estimators
distances
kernel adapters
kernels
aggregators
losses
normalizers
data splitters
cross-validation strategies
optimizers
Most of these components follow the same pattern:
1. implement the expected interface
2. register the class with the appropriate factory
3. refer to the implementation by its registered string name
Extension architecture
flowchart TD
F["BaseFactory"]
D["BregmanDivergenceFactory"]
LM["LocalModelFactory"]
C["CombinerFactory"]
E["EstimatorFactory"]
DS["DistanceFactory"]
KA["KernelAdapterFactory"]
K["KernelFactory"]
A["AggregatorFactory"]
L["LossFactory"]
N["NormalizerFactory"]
S["SplitterFactory"]
CV["CVFactory"]
O["OptimizerFactory"]
F --> D
F --> LM
F --> C
F --> E
F --> DS
F --> KA
F --> K
F --> A
F --> L
F --> N
F --> S
F --> CV
F --> O
Each factory subclass receives its own independent registry.
The common factory system
The central registry implementation is:
from:
Registration uses a decorator:
Creation uses:
Independent registries
BaseFactory.__init_subclass__() performs:
for every factory subclass.
Therefore:
do not share one global name table.
The same symbolic name can exist in different factories without conflict.
For example, a distance and a loss can both use:
in their own registries.
Registration names are normalized
BaseFactory normalizes names by:
So:
is stored as:
Invalid names
Registration names must be strings and cannot become empty after stripping.
These are invalid:
and non-string names raise a TypeError.
Multiple aliases
One class can be registered under several names:
Every alias points to the same RegistryEntry.
Duplicate names
Within one registration call:
the normalized names collide and the source raises:
because both normalize to:
Existing registry conflicts
If a normalized name is already registered, register() raises:
before the decorator registers the new class.
The source does not silently overwrite an existing implementation.
Categories
Factories can attach categories:
@CombinerFactory.register(
"my_regression_combiner",
categories={
"regression",
},
)
class MyRegressionCombiner(
BaseCombiner
):
...
Categories are normalized in the same way as names.
Category queries
Useful methods include:
The current KFC combiner and local-model layers use categories to distinguish:
The optimizer factory also uses categories such as:
Metadata
register() accepts arbitrary keyword metadata:
The metadata are stored in the registration entry.
Inspect them with:
Registry inspection
The common factory API provides:
Removing registrations
The source also provides:
and:
unregister() removes only one alias.
If one class is registered as:
then:
leaves:
registered.
clear() caution
Factory.clear() removes every registration from that factory subclass.
This affects built-ins already imported into that registry.
It is primarily a low-level registry operation rather than a normal user-guide configuration step.
Inspecting one registration
from kfc_procedure.cobra.core.distances import (
DistanceFactory,
)
print(
DistanceFactory.info(
"euclidean"
)
)
The returned dictionary contains:
Extending Bregman divergences
KFC clustering divergences inherit:
and register with:
A concrete divergence is expected to provide:
while the base class supplies the generic Bregman distance machinery.
Required mathematical interface
For a convex generator:
the base class computes:
A custom divergence therefore implements:
Minimal custom divergence
import numpy as np
from kfc_procedure.core.clustering.divergences.base import (
BaseBregmanDivergence,
BregmanDivergenceFactory,
)
@BregmanDivergenceFactory.register(
"scaled_euclidean"
)
class ScaledEuclideanDivergence(
BaseBregmanDivergence
):
name = "scaled_euclidean"
family = "custom"
def __init__(
self,
scale=1.0,
**kwargs,
):
super().__init__(
**kwargs,
)
self.scale = scale
def in_domain(
self,
X,
):
return True
def phi(
self,
X,
):
X = np.asarray(
X,
dtype=float,
)
return (
self.scale
*
np.sum(
X ** 2,
axis=1,
)
)
def grad_phi(
self,
X,
):
X = np.asarray(
X,
dtype=float,
)
return (
2.0
*
self.scale
*
X
)
Then:
Base divergence functionality you inherit
BaseBregmanDivergence already implements methods such as:
using the custom generator interface.
The distance() implementation also caches quantities associated with the
reference matrix Y for repeated evaluations.
So a custom divergence normally should not need to reimplement the entire clustering distance engine.
Domain validation
The base constructor accepts:
and concrete divergences define:
However, the generic current distance() implementation directly checks:
rather than conditionally checking only when validate_domain is true.
So disabling validate_domain is not a universal way to bypass domain checks
through all current base paths.
Use custom divergence in KFC
model = KFCRegressor(
divergences=[
"scaled_euclidean",
],
divergences_params={
"scaled_euclidean": {
"scale": 2.0,
},
},
...
)
The exact divergences_params lookup should match the string key used by
K-Step.
Divergence object path
K-Step also accepts a:
instance directly.
In that case, factory parameter lookup is bypassed and the object itself is used.
Extending F-Step local models
Custom KFC local models inherit:
and register with:
The required methods are:
and:
The base class also defines a default:
that raises NotImplementedError.
Custom regression local model
import numpy as np
from kfc_procedure.core.ml.base import (
BaseLocalModel,
LocalModelFactory,
)
@LocalModelFactory.register(
"median_regressor",
categories={
"regression",
},
)
class MedianRegressor(
BaseLocalModel
):
def fit(
self,
X,
y,
):
self.value_ = float(
np.median(
y
)
)
return self
def predict(
self,
X,
):
return np.full(
len(
X
),
self.value_,
dtype=float,
)
Then:
can resolve it through the factory.
Custom classification local model
@LocalModelFactory.register(
"my_classifier",
categories={
"classification",
},
)
class MyClassifier(
BaseLocalModel
):
def fit(
self,
X,
y,
):
...
return self
def predict(
self,
X,
):
...
def predict_proba(
self,
X,
):
...
The category is important because F-Step checks task support for string-based models.
random_state constructor compatibility
For string local models, F-Step injects:
into the factory parameters when that key is not already present.
Built-in scikit-learn adapters filter unsupported constructor keywords.
A directly registered custom local model does not automatically receive that filtering layer.
Therefore a custom string model should either:
or:
if it will be used with the current F-Step construction path.
Current F-Step behavior
A registered custom class with:
can fail if F-Step calls it with:
and the class does not accept that keyword.
Scikit-learn models are auto-registered
The package already provides:
which discovers scikit-learn classifiers and regressors using:
and registers snake_case names in:
So you often do not need a custom wrapper just to use a normal scikit-learn estimator in F-Step.
Custom scikit-learn-style behavior
If you do write a local model, inheriting from:
also gives you a BaseEstimator ancestry through the source class hierarchy.
Following scikit-learn-style constructor conventions makes the object easier to inspect and compose.
Extending C-Step combiners
A KFC combiner inherits:
and implements:
and:
The base class supplies:
as an alias for:
Regression combiner example
import numpy as np
from kfc_procedure.core.combiner.base import (
BaseCombiner,
CombinerFactory,
)
@CombinerFactory.register(
"trimmed_mean",
categories={
"regression",
},
)
class TrimmedMeanCombiner(
BaseCombiner
):
def __init__(
self,
trim=1,
random_state=None,
):
self.trim = trim
self.random_state = random_state
def fit(
self,
X,
y=None,
):
return self
def combine(
self,
X,
):
X = np.asarray(
X,
dtype=float,
)
X_sorted = np.sort(
X,
axis=1,
)
if self.trim == 0:
kept = X_sorted
else:
kept = X_sorted[
:,
self.trim:
-self.trim,
]
return np.mean(
kept,
axis=1,
)
The explicit:
parameter makes this example compatible with the current C-Step string construction behavior.
Classification combiner example
from collections import Counter
@CombinerFactory.register(
"last_tie_vote",
categories={
"classification",
},
)
class LastTieVoteCombiner(
BaseCombiner
):
def __init__(
self,
random_state=None,
):
self.random_state = random_state
def fit(
self,
X,
y=None,
):
return self
def combine(
self,
X,
):
outputs = []
for row in X:
counts = Counter(
row
)
max_count = max(
counts.values()
)
tied = [
value
for value, count
in counts.items()
if count == max_count
]
outputs.append(
tied[
-1
]
)
return np.asarray(
outputs,
dtype=object,
)
Combiner category checks
C-Step checks:
for string-based combiners.
So a custom regression combiner should register category:
and a classifier combiner should register:
C-Step random_state caveat
Like F-Step, C-Step injects:
when constructing a string-based combiner.
Unlike SklearnLocalModel, CombinerFactory.create() forwards constructor
kwargs directly.
Therefore custom string combiners should be designed to accept the injected keyword if they use the current C-Step pathway.
Pre-built combiner objects
If you pass a combiner object instead of a string:
C-Step returns that object directly from _build_combiner().
No factory lookup or random_state injection occurs.
This can be useful when your custom constructor does not match the current string-factory path.
Extending COBRA estimators
The COBRA core defines:
with required:
methods and a registry:
This is separate from KFC's:
Minimal COBRA estimator
import numpy as np
from kfc_procedure.cobra.core.estimators.base import (
BaseEstimator,
EstimatorFactory,
)
@EstimatorFactory.register(
"constant_regressor"
)
class ConstantRegressor(
BaseEstimator
):
def __init__(
self,
value=None,
):
self.value = value
def fit(
self,
x,
y,
**kwargs,
):
self.value_ = (
float(
np.mean(
y
)
)
if self.value is None
else float(
self.value
)
)
return self
def predict(
self,
x,
**kwargs,
):
return np.full(
len(
x
),
self.value_,
)
COBRA estimator integration
The shared COBRA helper:
supports:
So a registered estimator can be included in an estimator pool by name.
Estimator categories
The current COBRA EstimatorFactory itself does not require task categories
for the high-level helper in the same way KFC LocalModelFactory does.
If you add categories, they remain available through the generic factory query methods, but the current shared estimator helper does not use them for regression/classification validation.
Extending distances
Custom COBRA distances inherit:
and implement:
The result should have shape:
Custom squared L2 distance
import numpy as np
from kfc_procedure.cobra.core.distances import (
BaseDistance,
DistanceFactory,
)
@DistanceFactory.register(
"squared_l2"
)
class SquaredL2Distance(
BaseDistance
):
def matrix(
self,
x,
y,
):
x = np.asarray(
x,
dtype=float,
)
y = np.asarray(
y,
dtype=float,
)
diff = (
x[
:,
None,
:
]
-
y[
None,
:,
:
]
)
return np.sum(
diff ** 2,
axis=2,
)
Use it with:
Distance parameters
BaseDistance stores arbitrary constructor kwargs through its own
params/attribute system.
A custom distance can therefore expose configuration and receive it through:
Example:
Extending kernel adapters
Kernel adapters sit between:
They inherit:
and must implement:
The base class already provides:
Custom adapter example
import numpy as np
from kfc_procedure.cobra.core.adapters import (
BaseKernelAdapter,
KernelAdapterFactory,
)
@KernelAdapterFactory.register(
"power_scale"
)
class PowerScaleAdapter(
BaseKernelAdapter
):
def __init__(
self,
scale=1.0,
power=1.0,
):
super().__init__(
scale=scale,
power=power,
)
def transform(
self,
*distances,
):
if len(
distances
) != 1:
raise ValueError(
"power_scale expects one distance matrix"
)
D = np.asarray(
distances[0],
dtype=float,
)
return (
self.scale
*
D ** self.power
)
Adapter parameter vector
The inherited:
returns:
So parameter order follows insertion order in the adapter's internal parameter dictionary.
If an external optimizer depends on that vector order, construct parameters in a stable and intentional order.
Adapter integration limitation
The current main:
construct:
directly through the factory, while MixCOBRA chooses the one- or two-parameter built-in adapter based on:
They do not currently expose:
as user-facing constructor arguments.
Therefore registering a custom adapter does not automatically make it selectable from those high-level estimators.
This is a lower-level extension point unless the estimator source is extended.
Extending kernels
Custom kernels inherit:
and implement:
The base class also provides:
Kernel metadata
Two important class attributes are:
The current high-level regression COBRA estimators inspect:
when deciding whether gradient optimization can be used.
Custom kernel example
import numpy as np
from kfc_procedure.cobra.core.kernels import (
BaseKernel,
KernelFactory,
)
@KernelFactory.register(
"inverse_square"
)
class InverseSquareKernel(
BaseKernel
):
requires_grad = True
mode = "continuous"
def __call__(
self,
D,
):
D = np.asarray(
D,
dtype=float,
)
return 1.0 / (
1.0
+
D ** 2
)
Then:
can resolve it.
Kernel parameters
A custom kernel can call:
so the parameter appears in:
and can be updated through:
requires_grad
Set:
only when the kernel should participate in the current gradient optimization path.
If set to:
GradientCOBRA and MixCOBRA can fall back from:
to:
The package trusts this flag; it does not analyze differentiability automatically.
Kernel mode
Current built-ins use:
The base helper:
returns true only for exact mode:
and:
returns true only for:
There is no current:
helper.
Extending aggregators
Custom aggregators inherit:
and implement:
The base class provides a generic:
batch loop.
Regression aggregator example
import numpy as np
from kfc_procedure.cobra.core.aggregators import (
BaseAggregator,
AggregatorFactory,
)
@AggregatorFactory.register(
"median"
)
class MedianAggregator(
BaseAggregator
):
def aggregate(
self,
values,
weights=None,
**kwargs,
):
return float(
np.median(
np.asarray(
values,
dtype=float,
)
)
)
Then:
can resolve it through the current high-level factory path.
Classification probabilities
A classification aggregator can additionally implement:
The base implementation raises:
If a custom classifier aggregator should support:
implement this method explicitly.
Batch optimization
If the aggregator can efficiently process all query rows together, override:
instead of using the base per-row loop.
WeightedVoteAggregator is an example of a current built-in that supplies a
specialized vectorized batch path.
Extending losses
A custom loss inherits:
and implements:
The optimizer assumes smaller returned values are better.
RMSE example
import numpy as np
from kfc_procedure.cobra.core.losses import (
BaseLoss,
LossFactory,
)
@LossFactory.register(
"rmse"
)
class RMSELoss(
BaseLoss
):
def __call__(
self,
y_true,
y_pred,
):
y_true = np.asarray(
y_true
)
y_pred = np.asarray(
y_pred
)
return float(
np.sqrt(
np.mean(
(
y_true
-
y_pred
) ** 2
)
)
)
Use:
Parameterized loss
Factories forward constructor parameters directly, so:
can configure a custom loss.
@LossFactory.register(
"scaled_mae"
)
class ScaledMAE(
BaseLoss
):
def __init__(
self,
scale=1.0,
):
self.scale = scale
def __call__(
self,
y_true,
y_pred,
):
return float(
self.scale
*
np.mean(
np.abs(
np.asarray(
y_true
)
-
np.asarray(
y_pred
)
)
)
)
Then:
Extending normalizers
Custom normalizers inherit:
and implement:
The base class supplies:
Max-absolute example
import numpy as np
from kfc_procedure.cobra.core.normalizers import (
BaseNormalizer,
NormalizerFactory,
)
@NormalizerFactory.register(
"maxabs"
)
class MaxAbsNormalizer(
BaseNormalizer
):
def fit(
self,
x,
**kwargs,
):
x = np.asarray(
x,
dtype=float,
)
self.scale_ = (
np.max(
np.abs(
x
),
axis=0,
)
+
1e-12
)
return self
def transform(
self,
x,
**kwargs,
):
return (
np.asarray(
x,
dtype=float,
)
/
self.scale_
)
Normalizer integration limitation
The current main COBRA estimators do not resolve:
inside their ordinary fitting workflows.
GradientCOBRA and MixCOBRA currently use scalar normalization through:
and CombinedClassifier applies no normalizer layer.
So registering a custom normalizer makes it available at the lower level but does not automatically add it to the main estimator pipeline.
Extending splitters
Custom train/calibration splitters inherit:
and return:
The abstract signature is:
Fixed splitter example
import numpy as np
from kfc_procedure.cobra.core.splitters import (
BaseDataSplitter,
SplitterFactory,
)
from kfc_procedure.cobra.core.types import (
SplitIndices,
)
@SplitterFactory.register(
"first_70_percent"
)
class First70PercentSplitter(
BaseDataSplitter
):
def split(
self,
x,
y,
*,
groups=None,
):
n = len(
x
)
cut = int(
0.7
*
n
)
return SplitIndices(
train_idx=np.arange(
cut
),
eval_idx=np.arange(
cut,
n,
),
)
Splitter high-level limitation
The shared:
accepts:
but the current public fit() signatures of:
do not expose that argument.
Therefore custom splitter registration is a lower-level extension unless you:
call resolve_training_context() yourself
extend the estimator fit API
or prepare explicit X_l/y_l data externally.
Extending cross-validation
Custom CV strategies inherit:
and implement:
The split() method yields:
objects.
Fixed two-fold example
import numpy as np
from kfc_procedure.cobra.core.cv import (
BaseCrossValidator,
CVFactory,
)
from kfc_procedure.cobra.core.types import (
SplitIndices,
)
@CVFactory.register(
"fixed_two_fold"
)
class FixedTwoFold(
BaseCrossValidator
):
def split(
self,
x,
y,
*,
groups=None,
):
n = len(
x
)
cut = n // 2
a = np.arange(
cut
)
b = np.arange(
cut,
n,
)
yield SplitIndices(
train_idx=a,
eval_idx=b,
fold_id=0,
)
yield SplitIndices(
train_idx=b,
eval_idx=a,
fold_id=1,
)
def get_n_splits(
self,
):
return 2
CV high-level limitation
The current primary COBRA estimators construct:
internally.
Their constructors expose:
but not:
Therefore registering a custom CV implementation alone does not make it selectable from the current high-level estimator API.
An estimator source extension is required for direct selection.
Extending optimizers
All optimizers inherit:
and implement:
A custom optimizer intended for the high-level COBRA estimators should return at least:
Custom fixed optimizer
import numpy as np
from kfc_procedure.cobra.core.optimizers import (
BaseOptimizer,
OptimizerFactory,
)
@OptimizerFactory.register(
"fixed_bandwidth",
categories={
"search",
},
)
class FixedBandwidthOptimizer(
BaseOptimizer
):
def __init__(
self,
bandwidth=1.0,
**kwargs,
):
super().__init__(
**kwargs,
)
self.bandwidth = bandwidth
def optimize(
self,
objective,
init_param=None,
**kwargs,
):
x = np.array([
self.bandwidth
])
score = float(
objective(
x
)
)
return {
"x": x,
"score": score,
"history": [
{
"iter": 0,
"x": x.copy(),
"score": score,
}
],
}
Optimizer categories matter
GradientCOBRA and MixCOBRA check categories.
For:
they expect:
For:
they expect:
So a custom optimizer must be registered with the appropriate category if it will be selected by those high-level estimators.
Custom gradient update rule
For a new gradient algorithm, subclass:
and implement:
The base class then supplies:
Sign-gradient example
@OptimizerFactory.register(
"sign_gd",
categories={
"optimizer",
"gradient",
},
)
class SignGradientOptimizer(
BaseGradientOptimizer
):
def step(
self,
x,
lr,
grad,
state,
):
return (
x
-
lr
*
np.sign(
grad
),
state,
)
Custom search optimizer
If the method works with an explicit candidate set, subclassing:
can reuse:
Then implement the component-specific:
method.
Extending a high-level estimator
Some registries are not yet exposed by the main constructors.
In those cases, extending the estimator itself may be the right approach.
For example, a custom GradientCOBRA subclass could add:
The current source does not provide those high-level constructor hooks directly.
Example high-level subclass pattern
class MyGradientCOBRA(
GradientCOBRA
):
def __init__(
self,
*,
cv="kfold",
cv_params=None,
**kwargs,
):
super().__init__(
**kwargs,
)
self.cv = cv
self.cv_params = (
{}
if cv_params is None
else dict(
cv_params
)
)
def _resolve_components(
self,
):
super()._resolve_components()
self.cv_ = CVFactory.create(
self.cv,
**self.cv_params,
)
This is an extension pattern inferred from the current component-resolution design.
The exact override point should be checked against the version of
GradientCOBRA you are extending.
Preserve scikit-learn constructor conventions
Many package classes follow a scikit-learn-like style.
For custom estimator-like objects:
store constructor parameters as attributes
avoid doing expensive fitting in __init__
return self from fit()
keep predict() deterministic given fitted state.
This is especially useful when objects are inspected or passed into wrappers.
Factory constructor forwarding is direct
BaseFactory.create() does:
There is no generic signature filtering.
Therefore unsupported constructor arguments normally raise through the target class constructor.
Exception: scikit-learn F-Step wrapper
SklearnLocalModel is a special case.
It inspects:
and keeps only supported kwargs before creating the scikit-learn model.
This filtering behavior belongs to the adapter, not to BaseFactory.
Do not assume other factories silently discard unsupported parameters.
Function registration caveat
BaseFactory.register() is typed/documented as registering classes.
The current register_all_sklearn_models() helper creates a closure:
and registers that callable through LocalModelFactory.
Because BaseFactory.create() simply calls the stored target, this works in
practice.
However, info() assumes the registered target has attributes such as:
A normal function does, so this dynamic builder remains compatible with that inspection.
Import timing matters
Decorators execute when their modules are imported.
A custom registration is therefore active only after Python has imported the module containing the decorated class.
Example:
Your application must import:
before:
Built-in registration also depends on imports
The package __init__.py files import built-in implementation modules so their
decorators populate the registries.
If you import an unusually narrow internal module directly, registry contents can depend on which implementation modules have already been imported.
When inspecting available components, use the normal public package imports where possible.
Avoid registration at prediction time
Register custom components during application/module setup rather than inside a hot prediction loop.
Repeatedly executing the same decorator can produce a registry conflict because the normalized name is already present.
Testing a custom component
A useful test sequence is:
1. register it
2. verify Factory.contains(name)
3. inspect Factory.info(name)
4. create it through Factory.create(name)
5. test its base interface directly
6. test integration with the high-level estimator
7. test fitted-state attributes
8. test serialization/deployment requirements separately.
Factory smoke test
assert KernelFactory.contains(
"inverse_square"
)
print(
KernelFactory.info(
"inverse_square"
)
)
kernel = KernelFactory.create(
"inverse_square"
)
Interface smoke test
For a custom distance:
For a custom kernel:
For a custom aggregator:
Integration smoke test
Use a very small model configuration first:
model = GradientCOBRA(
distance="squared_l2",
kernel="inverse_square",
max_iter=5,
n_cv=2,
n_jobs=1,
random_state=42,
)
This makes extension failures easier to isolate before running a large optimization.
Debug registry conflicts
before registering a new name.
You can also test:
The source does not provide an overwrite flag.
Debug category mismatch
For a custom KFC combiner:
For a custom optimizer:
Debug constructor errors
Because most factories forward kwargs directly, inspect the actual custom constructor signature.
For example:
then configure:
rather than an unsupported key.
Debug high-level non-selection
If:
works but the main estimator cannot select it, check whether that component type is actually exposed by the estimator API.
Current examples of lower-level registries not fully exposed include:
NormalizerFactory
SplitterFactory custom selection
CVFactory custom selection
KernelAdapterFactory custom selection.
Registration and high-level configurability are separate concepts.
Extending package exports
A custom component defined in your application does not need to be added to the
kfc_procedure package's __init__.py files for the factory to use it after
import.
If you are contributing the component to the package itself, then updating the
relevant __init__.py can make the class importable through the package's
normal public namespace and can ensure its registration module is imported.
Adding a built-in component to the project
A source-level contribution typically involves:
1. create the implementation module
2. inherit the appropriate base class
3. add the factory registration decorator
4. import the implementation from the package-level __init__.py if needed
5. add unit tests
6. add user/API documentation
7. verify existing registry names do not conflict.
Suggested test areas
For new components, test at least:
constructor parameters
factory registration and aliases
factory category support
input shape handling
fit/predict or transform behavior
empty/invalid input behavior
deterministic behavior when relevant
integration with a small KFC/COBRA pipeline
error behavior for unsupported configuration.
Current extension-point matrix
| Component | Base class | Factory | Selectable by current high-level API? |
|---|---|---|---|
| KFC divergence | BaseBregmanDivergence |
BregmanDivergenceFactory |
Yes |
| KFC local model | BaseLocalModel |
LocalModelFactory |
Yes |
| KFC combiner | BaseCombiner |
CombinerFactory |
Yes |
| COBRA estimator | BaseEstimator |
EstimatorFactory |
Yes, via estimator pool helpers |
| distance | BaseDistance |
DistanceFactory |
Yes |
| kernel adapter | BaseKernelAdapter |
KernelAdapterFactory |
No direct public selector |
| kernel | BaseKernel |
KernelFactory |
Yes |
| aggregator | BaseAggregator |
AggregatorFactory |
Yes |
| loss | BaseLoss |
LossFactory |
Yes |
| normalizer | BaseNormalizer |
NormalizerFactory |
Not wired into main estimators |
| splitter | BaseDataSplitter |
SplitterFactory |
Resolver supports it; main fit APIs do not |
| cross-validator | BaseCrossValidator |
CVFactory |
Main estimators currently hard-code kfold |
| optimizer | BaseOptimizer |
OptimizerFactory |
Yes, subject to category/API behavior |
Current high-level configuration map
KFC
Directly configurable:
with corresponding parameter dictionaries.
GradientCOBRA
Directly configurable component families include:
Not currently exposed as general selectors:
MixCOBRA
Directly configurable:
Adapter selection is controlled indirectly by:
rather than by a general adapter name.
CombinedClassifier
Directly configurable:
The current optimization path is primarily grid-oriented.
Extension safety checklist
Before relying on a custom implementation, verify:
the module containing it has been imported
the registration name does not conflict
categories match high-level checks
constructor accepts all injected/forwarded parameters
matrix shapes match the expected interface
fit methods return self where expected
predict/transform methods use fitted state consistently
the component is actually selectable by the high-level estimator
random_state handling is explicit when stochastic
parallel execution can serialize the object if it enters Joblib loky workers.
A complete custom COBRA component chain
Because components are independent, you can register several custom pieces:
model = GradientCOBRA(
distance="squared_l2",
kernel="inverse_square",
aggregator="median",
loss="rmse",
optimizer="grid",
)
provided each name has already been registered with the correct factory.
The normal high-level component resolver will create those objects using their respective factories.
A complete custom KFC chain
Likewise:
model = KFCRegressor(
divergences=[
"scaled_euclidean",
],
divergences_params={
"scaled_euclidean": {
"scale": 2.0,
},
},
local_model="median_regressor",
combiner="trimmed_mean",
combiner_params={
"trim": 1,
},
random_state=42,
)
works conceptually when each custom registration satisfies the current K-Step, F-Step, and C-Step interfaces and constructor behavior.
Extending versus passing objects directly
The source often supports both:
Using a registered string gives you:
Using a pre-built object can bypass:
The best path depends on the component and the current high-level integration.
When direct objects are especially useful
A pre-built object can be useful when:
its constructor does not accept injected random_state
you need complex construction logic
you need to retain external references
you are prototyping before adding a formal registry entry.
Be aware that object reuse semantics differ by component.
For example, the current F-Step reuses a non-string local-model object across cluster fits rather than cloning it.
Factory registration is process-local
The current registries are Python class-level dictionaries.
A custom registration exists in the Python process after its module has been imported.
The source does not provide persistent registry storage across separate process launches.
Your application or package must import the registration module again in each new process.
Parallel worker considerations
COBRA base-estimator fitting uses Joblib's:
backend when:
If a custom estimator object participates in that path, it needs to be usable by Joblib's worker process mechanism.
When debugging a custom estimator integration, setting:
can isolate factory/interface errors from process-serialization issues.
Documentation conventions for extensions
When adding a new built-in component, document:
registry name and aliases
base interface
constructor parameters
mathematical formula or algorithm
expected shapes
task/category support
fitted attributes
high-level configuration example
known limitations
interaction with optimization and random_state.
This keeps extension documentation aligned with the rest of the user guide.
Quick reference
| What you want to extend | Register with |
|---|---|
| KFC divergence | BregmanDivergenceFactory |
| KFC local model | LocalModelFactory |
| KFC combiner | CombinerFactory |
| COBRA estimator | EstimatorFactory |
| distance | DistanceFactory |
| kernel adapter | KernelAdapterFactory |
| kernel | KernelFactory |
| aggregator | AggregatorFactory |
| loss | LossFactory |
| normalizer | NormalizerFactory |
| splitter | SplitterFactory |
| CV strategy | CVFactory |
| optimizer | OptimizerFactory |
Mental model
KFC Procedure is extensible through contracts and registries: implement the smallest required interface, register the implementation, then let the orchestration layer resolve it by name.
The main caveat is that not every registry is currently exposed as a high-level constructor option. For those components, the registry is already available at the lower layer, but the surrounding estimator must be extended before the new component can be selected through the public API.