Distances
Distance functions are a core part of the COBRA components in
kfc_procedure.
They define how two observations are compared once those observations have been represented in the relevant COBRA space.
The current source provides five built-in distance implementations:
The implementation lives under:
kfc_procedure/cobra/core/distances/
├── base.py
├── euclidean.py
├── manhattan.py
├── minkowski.py
├── cosine.py
└── hamming.py
These distances are used by:
through the shared:
Distance in the COBRA pipeline
The general flow is:
flowchart LR
P["Prediction/input representation"]
D["Pairwise distance matrix"]
A["Kernel adapter"]
K["Kernel weights"]
G["Aggregator"]
Y["Final prediction"]
P --> D --> A --> K --> G --> Y
The distance step converts two matrices:
into a pairwise matrix:
Each entry is:
Built-in distances
| Registry name | Alias | Class | Main idea |
|---|---|---|---|
euclidean |
l2 |
EuclideanDistance |
L2 distance |
manhattan |
l1 |
ManhattanDistance |
L1 distance |
minkowski |
lp |
MinkowskiDistance |
generalized Lp distance |
cosine |
— | CosineDistance |
angular dissimilarity |
hamming |
— | HammingDistance |
fraction of mismatched coordinates |
The distance interface
Every distance implementation inherits:
The abstract interface is:
class BaseDistance(ABC):
def __init__(
self,
**kwargs,
):
...
def set_params(
self,
**params,
):
...
def get_params(
self,
deep=True,
):
...
@abstractmethod
def matrix(
self,
x,
y,
):
...
The only required computational method is:
Expected matrix shapes
If:
and:
then:
is expected to be:
BaseDistance parameters
The base constructor stores every keyword argument in:
and also creates same-named attributes.
Conceptually:
results in:
and:
get_params()
returns a copy of:
The signature includes:
for scikit-learn-style compatibility, but the current implementation does not
use deep.
set_params()
updates both:
and:
and returns:
for chaining.
DistanceFactory
Distances are registered with:
which subclasses the package's:
You can create a distance by name:
from kfc_procedure.cobra.core.distances import (
DistanceFactory,
)
distance = DistanceFactory.create(
"euclidean"
)
Aliases
The current registrations are:
while:
have one registered name each.
Registry normalization
Because DistanceFactory inherits BaseFactory, registry names are:
before lookup.
So:
resolves the same normalized key as:
Euclidean distance
The implementation class is:
registered as:
The source defines:
Equivalent form:
Implementation
The source uses the identity:
The code is:
x2 = np.sum(
x ** 2,
axis=1,
keepdims=True,
)
y2 = np.sum(
y ** 2,
axis=1,
keepdims=True,
).T
xy = x @ y.T
dist = np.sqrt(
np.maximum(
x2 + y2 - 2 * xy,
0.0,
)
)
Numerical clipping
Before the square root, the source applies:
This prevents tiny floating-point errors from producing negative squared distances.
Example
import numpy as np
from kfc_procedure.cobra.core.distances import (
EuclideanDistance,
)
x = np.array([
[0.0, 0.0],
[1.0, 1.0],
])
y = np.array([
[1.0, 0.0],
])
distance = EuclideanDistance()
D = distance.matrix(
x,
y,
)
print(
D
)
The result has shape:
Manhattan distance
The implementation class is:
registered as:
The source defines:
Implementation backend
The current source delegates to:
with:
The implementation is:
Example
from kfc_procedure.cobra.core.distances import (
ManhattanDistance,
)
distance = ManhattanDistance()
D = distance.matrix(
x,
y,
)
Minkowski distance
The implementation class is:
registered as:
It generalizes L1 and L2 distances:
Constructor
The default is:
This is a source-specific default.
Special cases
The source documentation identifies:
as Manhattan distance and:
as Euclidean distance.
Implementation
The current implementation constructs pairwise coordinate differences through broadcasting:
then computes:
Example
from kfc_procedure.cobra.core.distances import (
MinkowskiDistance,
)
distance = MinkowskiDistance(
p=4,
)
D = distance.matrix(
x,
y,
)
distance_params
Because MinkowskiDistance exposes:
through its constructor, COBRA estimators can configure it using:
For example:
The source passes:
directly into:
Cosine distance
The implementation class is:
registered as:
The source defines cosine similarity as:
and cosine distance as:
Implementation
The code computes row norms:
x_norm = np.linalg.norm(
x,
axis=1,
keepdims=True,
)
y_norm = np.linalg.norm(
y,
axis=1,
keepdims=True,
).T
then:
and returns:
Zero-vector protection
The denominator contains:
to avoid division by zero.
This means the implementation does not raise simply because one of the vectors has norm zero.
Range noted by the source
The module notes that cosine distance is theoretically in:
It also notes that values are typically in:
for non-negative data.
Example
from kfc_procedure.cobra.core.distances import (
CosineDistance,
)
distance = CosineDistance()
D = distance.matrix(
x,
y,
)
Hamming distance
The implementation class is:
registered as:
The source defines normalized Hamming distance as:
So the output measures the fraction of coordinates that differ.
Output range
Because the distance is normalized by the number of features:
Intended data type
The source explicitly describes Hamming distance as intended for:
representations.
It also states that continuous features should be discretized before use.
Numba implementation
The primary Hamming path uses:
for:
The function loops over all sample pairs and all feature coordinates.
For every pair:
then:
NumPy fallback
The public matrix() method wraps the Numba path in:
If the Numba path fails, it uses:
This produces the same normalized mismatch fraction.
Hamming and CombinedClassifier
The default distance for:
is:
This aligns with the current prediction-space representation used by
CombinedClassifier, which consists of hard class predictions such as:
For two prediction vectors:
one of four coordinates differs, so:
Distance defaults by estimator
The current source defaults are:
| Estimator | Default distance |
|---|---|
GradientCOBRA |
euclidean |
MixCOBRARegressor |
euclidean |
CombinedClassifier |
hamming |
These defaults reflect the representations used by the corresponding estimators.
Distance configuration
Each high-level COBRA estimator accepts:
and:
For example:
or:
How distance objects are resolved
The shared resolver contains:
The high-level estimators use equivalent factory construction.
Therefore:
selects the registered implementation and:
are forwarded to its constructor.
Inspect the fitted distance object
After fitting a COBRA estimator:
To inspect parameters:
For Minkowski:
Prediction-space distance
For GradientCOBRA, the distance is computed on normalized prediction vectors.
During fitting:
At prediction time:
So the distance object itself is independent of COBRA's normalization step.
It receives whatever representation the estimator passes into it.
CombinedClassifier distance
CombinedClassifier stores its calibration prediction representation as:
and computes:
New prediction patterns are compared with:
No separate prediction normalization is applied by the current CombinedClassifier before this distance calculation.
MixCOBRA distances
MixCOBRA can maintain two separate distance matrices.
In two-parameter mode:
is computed on:
and:
is computed on:
The same configured distance implementation is used for both spaces.
Two-space combination
The distance matrices are later combined by the two-parameter adapter as:
The distance class itself does not know about:
Those parameters belong to the adapter layer.
One-parameter MixCOBRA
With:
the source first concatenates:
into:
Then the configured distance is computed once on the combined representation.
So:
still controls geometry, while the one-parameter adapter later scales the result.
Distance vs kernel
Distance and kernel are separate concepts.
The distance computes:
The adapter may rescale it, for example:
Then the kernel turns that value into a similarity weight:
So choosing:
does not by itself define the final aggregation weight.
Example with RBF kernel
Suppose Euclidean distance produces:
If the adapter applies:
then:
The RBF kernel then receives:
rather than the original:
This separation is why distance configuration and bandwidth optimization are handled independently in the source.
Euclidean vs Minkowski p=2
The source provides both:
and:
These represent the same L2 distance mathematically.
However, the implementation paths differ.
EuclideanDistance uses the dot-product identity:
while MinkowskiDistance uses explicit pairwise broadcasting.
Therefore they are not implemented identically even when:
Manhattan vs Minkowski p=1
Similarly:
and:
represent the same L1 formula mathematically.
But:
delegates to SciPy's:
while:
uses NumPy broadcasting.
Memory behavior
The implementations differ in how they form pairwise distances.
EuclideanDistance avoids a full:
difference tensor.
ManhattanDistance delegates to SciPy.
MinkowskiDistance explicitly creates the broadcasted expression:
which conceptually has three dimensions.
HammingDistance uses a Numba loop first, avoiding the broadcasted tensor in
its primary path, with a broadcasted NumPy fallback.
The source does not contain an automatic memory-based distance selector.
Shape compatibility
The current distance implementations assume compatible feature dimensions.
For example:
and:
must represent the same coordinate dimension.
The classes do not define a common explicit manual check for mismatched feature counts.
Incompatible shapes therefore fail through the underlying NumPy/SciPy operations.
Input validation scope
The built-in distance classes mainly perform:
before calculation.
They do not define a shared validation layer for:
Those conditions are therefore handled implicitly by the mathematical operations or by later stages.
Custom distance
A custom metric can subclass:
and implement:
For example:
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)
y = np.asarray(y)
diff = (
x[:, None, :]
-
y[None, :, :]
)
return np.sum(
diff ** 2,
axis=2,
)
Then:
can resolve it.
Custom distance with parameters
Because BaseDistance already supports arbitrary keyword parameters, a custom
distance can expose them through the constructor.
@DistanceFactory.register(
"scaled_l1"
)
class ScaledL1Distance(
BaseDistance
):
def __init__(
self,
scale=1.0,
):
super().__init__(
scale=scale,
)
def matrix(
self,
x,
y,
):
x = np.asarray(x)
y = np.asarray(y)
return self.scale * np.sum(
np.abs(
x[:, None, :]
-
y[None, :, :]
),
axis=2,
)
Then:
works through the normal factory path.
Registering aliases
The factory supports multiple names:
Both names resolve to the same class.
Duplicate registration
Because DistanceFactory inherits BaseFactory, duplicate normalized names are
rejected.
For example, registering both:
in the same call is considered a duplicate after normalization.
Existing registry conflicts raise:
Inspect available distances
from kfc_procedure.cobra.core.distances import (
DistanceFactory,
)
print(
DistanceFactory.available()
)
The current built-ins include aliases, so the list should include names such as:
subject to normal module loading.
Inspect one registration
This uses the common factory metadata interface inherited from BaseFactory.
Direct comparison example
import numpy as np
from kfc_procedure.cobra.core.distances import (
DistanceFactory,
)
x = np.array([
[1.0, 0.0],
[1.0, 1.0],
])
y = np.array([
[0.0, 1.0],
])
for name in [
"euclidean",
"manhattan",
"minkowski",
"cosine",
]:
distance = DistanceFactory.create(
name
)
print(
name,
distance.matrix(
x,
y,
),
)
This compares the source implementations on the same coordinate representation.
Hamming example
x = np.array([
[0, 1, 1, 0],
[1, 1, 0, 0],
])
y = np.array([
[0, 0, 1, 0],
])
distance = DistanceFactory.create(
"hamming"
)
D = distance.matrix(
x,
y,
)
print(
D
)
For the first row, one of four coordinates differs:
Choosing a distance
The source exposes different geometries rather than enforcing one globally.
A source-aligned summary is:
-
Euclidean
Standard L2 geometry.
Registry:
-
Manhattan
Coordinate-wise absolute differences.
Registry:
-
Minkowski
Generalized Lp distance with configurable:
-
Cosine
Compares vector direction through angular dissimilarity.
-
Hamming
Counts normalized coordinate mismatches.
Used by default in
CombinedClassifier.
Debugging distance configuration
Check the configured name
Check parameters
Check the resolved object
After fitting:
Check effective parameters
Check matrix shape
Expected:
Check symmetry carefully
A metric such as Euclidean satisfies:
But the returned matrices:
and:
have transposed shapes.
To compare them:
Current implementation summary
| Distance | Formula | Constructor parameter | Implementation |
|---|---|---|---|
| Euclidean | \(\sqrt{\sum (x-y)^2}\) | none | vectorized dot-product identity |
| Manhattan | (\sum | x-y | ) |
| Minkowski | ((\sum | x-y | ^p)^{1/p}) |
| Cosine | \(1-\frac{x^\top y}{\|x\|\|y\|}\) | none | vectorized norms/dot products |
| Hamming | mean coordinate mismatch | none | Numba, NumPy fallback |
Quick reference
| Registry | Alias | Typical current use |
|---|---|---|
euclidean |
l2 |
default GradientCOBRA / MixCOBRA |
manhattan |
l1 |
alternative L1 geometry |
minkowski |
lp |
configurable Lp geometry |
cosine |
— | angular comparison |
hamming |
— | default CombinedClassifier prediction-space comparison |
Mental model
The distance layer decides what “nearby” means before the kernel turns that geometry into aggregation weights.
Distance selection changes the geometry of the COBRA neighborhood, while the kernel and optimizer determine how that geometry is converted into prediction weights.