Skip to content

Reference

Fuzzy C-means Model

Attributes:

Name Type Description
n_clusters int

The number of clusters to form as well as the number

max_iter int

Maximum number of iterations of the fuzzy C-means

m float

Degree of fuzziness: \(m \in (1, \infty)\).

error float

Relative tolerance with regards to Frobenius norm of

random_state Optional[int]

Determines random number generation for

init str

Initialization of the partition. "random" draws a

n_init int

Number of independent runs, each from a different

trained bool

Variable to store whether or not the model has been

Returns:

Type Description
FCM

A FCM model.

Exceptions:

Type Description
ReferenceError

If called without the model being trained

Source code in fcmeans/main.py
class FCM(BaseModel):
    r"""Fuzzy C-means Model

    Attributes:
        n_clusters (int): The number of clusters to form as well as the number
        of centroids to generate by the fuzzy C-means.
        max_iter (int): Maximum number of iterations of the fuzzy C-means
        algorithm for a single run.
        m (float): Degree of fuzziness: $m \in (1, \infty)$.
        error (float): Relative tolerance with regards to Frobenius norm of
        the difference
        in the cluster centers of two consecutive iterations to declare
        convergence.
        random_state (Optional[int]): Determines random number generation for
        centroid initialization.
        Use an int to make the randomness deterministic.
        init (str): Initialization of the partition. `"random"` draws a
        random fuzzy partition; `"k-means++"` seeds the centers with the
        k-means++ rule (each new center is drawn with probability
        proportional to its squared distance to the closest chosen one) and
        derives the partition from them. Ignored by `FCMedoids`, which
        always seeds with k-means++.
        n_init (int): Number of independent runs, each from a different
        initialization. The run with the lowest objective function is kept.
        The first run uses `random_state` itself, so `n_init > 1` is never
        worse than `n_init = 1` for the same `random_state`.
        trained (bool): Variable to store whether or not the model has been
        trained.

    Returns:
        FCM: A FCM model.

    Raises:
        ReferenceError: If called without the model being trained
    """

    model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)

    n_clusters: int = Field(5, ge=1)
    max_iter: int = Field(150, ge=1, le=1000)
    m: float = Field(2.0, ge=1.0)
    error: float = Field(1e-5, ge=1e-9)
    random_state: Optional[int] = None
    init: Literal["random", "k-means++"] = "random"
    n_init: int = Field(default=1, ge=1)
    trained: bool = False
    verbose: Optional[bool] = False
    distance: Optional[Union[DistanceOptions, Callable]] = (
        DistanceOptions.euclidean
    )
    distance_params: Optional[dict] = {}

    def _init_u(self, X: NDArray) -> None:
        """Initialize the fuzzy partition matrix `u`."""
        self.rng = np.random.default_rng(self.random_state)
        if self.init == "k-means++":
            self._centers = self._seed_centers(X)
            self.u = FCM._memberships(
                FCM._dist(
                    X, self._centers, self.distance, self.distance_params
                ),
                self.m,
            )
            return
        u = self.rng.uniform(size=(X.shape[0], self.n_clusters))
        self.u = u / u.sum(axis=1, keepdims=True)

    def _seed_centers(self, X: NDArray) -> NDArray:
        """Pick `n_clusters` samples with the k-means++ rule."""
        n = X.shape[0]
        chosen = [self.rng.integers(n)]
        closest = self._sq_dist_to(X, chosen[0])
        for _ in range(1, self.n_clusters):
            total = closest.sum()
            chosen.append(
                self.rng.choice(n, p=closest / total if total > 0 else None)
            )
            closest = np.minimum(closest, self._sq_dist_to(X, chosen[-1]))
        return X[chosen]

    def _sq_dist_to(self, X: NDArray, i: int) -> NDArray:
        """Squared distance from every sample to sample `i`."""
        d = FCM._dist(X, X[[i]], self.distance, self.distance_params)
        return d[:, 0] ** 2

    def _update_centers(self, X: NDArray) -> None:
        """Update `_centers` from the current partition matrix `u`."""
        self._centers = FCM._next_centers(X, self.u, self.m)

    def _distances(self, X: NDArray) -> NDArray:
        """Distance from each sample in X to each center."""
        return FCM._dist(X, self._centers, self.distance, self.distance_params)

    def _update_u(self, X: NDArray) -> None:
        """Update `u` from the current centers."""
        self.u = self.soft_predict(X)

    def _objective(self, X: NDArray) -> float:
        """Objective value (or a monotone transform) to compare runs."""
        return float((self.u**self.m * self._distances(X) ** 2).sum())

    def _fit_once(self, X: NDArray) -> None:
        """Run the algorithm once, from a single initialization."""
        self._init_u(X)
        for _ in tqdm.tqdm(
            range(self.max_iter), desc="Training", disable=not self.verbose
        ):
            u_old = self.u.copy()
            self._update_centers(X)
            self._update_u(X)
            # Stopping rule
            if np.linalg.norm(self.u - u_old) < self.error:
                break
        self.trained = True

    @validate_call(config=dict(arbitrary_types_allowed=True))
    def fit(self, X: NDArray) -> None:
        """Train the fuzzy-c-means model

        With `n_init > 1` the model is trained `n_init` times and the run
        with the lowest objective function is kept.

        Args:
            X (NDArray): Training instances to cluster.
        """
        if self.n_init == 1:
            self._fit_once(X)
            return
        seed = self.random_state
        extra = np.random.default_rng(seed).integers(2**32, size=self.n_init)
        seeds = [seed, *map(int, extra[1:])]
        best: tuple[float, dict, dict] = (np.inf, {}, {})
        try:
            for self.random_state in seeds:
                self._fit_once(X)
                obj = np.nan_to_num(self._objective(X), nan=np.inf)
                if obj < best[0] or not best[1]:
                    # snapshot of every fitted attribute of this run
                    best = (
                        obj,
                        dict(self.__dict__),
                        dict(self.model_extra or {}),
                    )
            self.__dict__.update(best[1])
            for name, value in best[2].items():
                setattr(self, name, value)
        finally:
            self.random_state = seed

    @validate_call(config=dict(arbitrary_types_allowed=True))
    def soft_predict(self, X: NDArray) -> NDArray:
        """Soft predict of FCM

        Args:
            X (NDArray): New data to predict.

        Returns:
            NDArray: Fuzzy partition array, returned as an array with
            n_samples rows and n_clusters columns.
        """
        return FCM._memberships(self._distances(X), self.m)

    @validate_call(config=dict(arbitrary_types_allowed=True))
    def predict(self, X: NDArray) -> NDArray:
        """Predict the closest cluster each sample in X belongs to.

        Args:
            X (NDArray): New data to predict.

        Raises:
            ReferenceError: If it called without the model being trained.

        Returns:
            NDArray: Index of the cluster each sample belongs to.
        """
        self._require_trained()
        X = np.expand_dims(X, axis=0) if len(X.shape) == 1 else X
        return self.soft_predict(X).argmax(axis=-1)

    def _require_trained(self) -> None:
        if not self.trained:
            raise ReferenceError(
                "You need to train the model. Run `.fit()` method to this."
            )

    @staticmethod
    def _memberships(d: NDArray, m: float) -> NDArray:
        """Fuzzy partition from the sample-to-center distances `d`."""
        with np.errstate(divide="ignore", invalid="ignore"):
            temp = d ** (2 / (m - 1))
            u = 1.0 / (temp * (1.0 / temp).sum(axis=1, keepdims=True))
        # a sample on a center belongs only to it (evenly split on ties)
        zero = d == 0
        rows = zero.any(axis=1)
        u[rows] = zero[rows] / zero[rows].sum(axis=1, keepdims=True)
        return u

    @staticmethod
    def _dist(
        A: NDArray,
        B: NDArray,
        distance: Optional[Union[DistanceOptions, Callable]] = (
            DistanceOptions.euclidean
        ),
        distance_params: Optional[dict] = {},
    ) -> NDArray:
        """Compute the distance between two matrices"""
        if callable(distance):
            return distance(A, B, distance_params)
        elif distance == "minkowski":
            if isinstance(distance_params, dict):
                p = distance_params.get("p", 1.0)
            else:
                p = 1.0
            return FCM._minkowski(A, B, p)
        elif distance == "cosine":
            return FCM._cosine(A, B)
        else:
            return FCM._euclidean(A, B)

    @staticmethod
    def _euclidean(A: NDArray, B: NDArray) -> NDArray:
        """Compute the euclidean distance between two matrices"""
        return np.sqrt(np.einsum("ijk->ij", (A[:, None, :] - B) ** 2))

    @staticmethod
    def _minkowski(A: NDArray, B: NDArray, p: float) -> NDArray:
        """Compute the minkowski distance between two matrices"""
        return np.einsum("ijk->ij", np.abs(A[:, None, :] - B) ** p) ** (1 / p)

    @staticmethod
    def _cosine_similarity(A: NDArray, B: NDArray) -> NDArray:
        """Compute the cosine similarity between two matrices"""
        p1 = np.sqrt(np.sum(A**2, axis=1))[:, np.newaxis]
        p2 = np.sqrt(np.sum(B**2, axis=1))[np.newaxis, :]
        return np.dot(A, B.T) / (p1 * p2)

    @staticmethod
    def _cosine(A: NDArray, B: NDArray) -> NDArray:
        """Compute the cosine distance between two matrices"""
        return np.abs(1 - FCM._cosine_similarity(A, B))

    @staticmethod
    def _next_centers(X: NDArray, u: NDArray, m: float):
        """Update cluster centers"""
        um = u**m
        return (X.T @ um / np.sum(um, axis=0)).T

    @property
    def centers(self) -> NDArray:
        self._require_trained()
        return self._centers

    @property
    def partition_coefficient(self) -> float:
        """Partition coefficient

        Equation 12a of
        [this paper](https://doi.org/10.1016/0098-3004(84)90020-7).
        """
        self._require_trained()
        return np.sum(np.mean(self.u**2, axis=0))

    @property
    def partition_entropy_coefficient(self):
        self._require_trained()
        return -np.sum(np.mean(self.u * np.log2(self.u), axis=0))

__class_vars__ special

The names of the class variables defined on the model.

__private_attributes__ special

Metadata about the private attributes of the model.

__pydantic_complete__ special

Whether model building is completed, or if there are still undefined fields.

__pydantic_computed_fields__ special

A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_custom_init__ special

Whether the model has a custom __init__ method.

__pydantic_decorators__ special

Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.

__pydantic_extra_info__ special

A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.

This is a private attribute, not meant to be used outside Pydantic.

__pydantic_fields__ special

A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects. This replaces Model.__fields__ from Pydantic V1.

__pydantic_generic_metadata__ special

A dictionary containing metadata about generic Pydantic models.

The origin and args items map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and the parameter item maps to the __parameter__ attribute of generic classes.

__pydantic_parent_namespace__ special

Parent namespace of the model, used for automatic rebuilding of models.

__pydantic_post_init__ special

The name of the post-init method for the model, if defined.

__pydantic_setattr_handlers__ special

__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__

__signature__ special

The synthesized __init__ [Signature][inspect.Signature] of the model.

model_config

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

partition_coefficient: float property readonly

Partition coefficient

Equation 12a of this paper.

fit(self, X)

Train the fuzzy-c-means model

With n_init > 1 the model is trained n_init times and the run with the lowest objective function is kept.

Parameters:

Name Type Description Default
X NDArray

Training instances to cluster.

required
Source code in fcmeans/main.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def fit(self, X: NDArray) -> None:
    """Train the fuzzy-c-means model

    With `n_init > 1` the model is trained `n_init` times and the run
    with the lowest objective function is kept.

    Args:
        X (NDArray): Training instances to cluster.
    """
    if self.n_init == 1:
        self._fit_once(X)
        return
    seed = self.random_state
    extra = np.random.default_rng(seed).integers(2**32, size=self.n_init)
    seeds = [seed, *map(int, extra[1:])]
    best: tuple[float, dict, dict] = (np.inf, {}, {})
    try:
        for self.random_state in seeds:
            self._fit_once(X)
            obj = np.nan_to_num(self._objective(X), nan=np.inf)
            if obj < best[0] or not best[1]:
                # snapshot of every fitted attribute of this run
                best = (
                    obj,
                    dict(self.__dict__),
                    dict(self.model_extra or {}),
                )
        self.__dict__.update(best[1])
        for name, value in best[2].items():
            setattr(self, name, value)
    finally:
        self.random_state = seed

predict(self, X)

Predict the closest cluster each sample in X belongs to.

Parameters:

Name Type Description Default
X NDArray

New data to predict.

required

Exceptions:

Type Description
ReferenceError

If it called without the model being trained.

Returns:

Type Description
NDArray

Index of the cluster each sample belongs to.

Source code in fcmeans/main.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def predict(self, X: NDArray) -> NDArray:
    """Predict the closest cluster each sample in X belongs to.

    Args:
        X (NDArray): New data to predict.

    Raises:
        ReferenceError: If it called without the model being trained.

    Returns:
        NDArray: Index of the cluster each sample belongs to.
    """
    self._require_trained()
    X = np.expand_dims(X, axis=0) if len(X.shape) == 1 else X
    return self.soft_predict(X).argmax(axis=-1)

soft_predict(self, X)

Soft predict of FCM

Parameters:

Name Type Description Default
X NDArray

New data to predict.

required

Returns:

Type Description
NDArray

Fuzzy partition array, returned as an array with n_samples rows and n_clusters columns.

Source code in fcmeans/main.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
    """Soft predict of FCM

    Args:
        X (NDArray): New data to predict.

    Returns:
        NDArray: Fuzzy partition array, returned as an array with
        n_samples rows and n_clusters columns.
    """
    return FCM._memberships(self._distances(X), self.m)

Possibilistic C-means Model

Relaxes the FCM constraint \(\sum_i u_{ij} = 1\), so \(u_{ij}\) is the typicality of sample \(j\) with respect to cluster \(i\) rather than a share of membership. Outliers get low typicality in every cluster.

The model is initialized with a FCM run: its partition gives the starting centers and the scale parameters \(\eta_i\) (Krishnapuram and Keller, 1993), which are kept fixed afterwards.

Membership update:

\[u_{ij} = \frac{1}{1 + (d_{ij}^2 / \eta_i)^{1/(m-1)}}\]

Accepts the same hyperparameters as FCM.

Attributes:

Name Type Description
eta NDArray

Scale parameter of each cluster, set by fit.

Exceptions:

Type Description
ReferenceError

If called without the model being trained.

NotImplementedError

If partition_coefficient or

Source code in fcmeans/pcm.py
class PCM(FCM):
    r"""Possibilistic C-means Model

    Relaxes the FCM constraint $\sum_i u_{ij} = 1$, so $u_{ij}$ is the
    *typicality* of sample $j$ with respect to cluster $i$ rather than a
    share of membership. Outliers get low typicality in every cluster.

    The model is initialized with a FCM run: its partition gives the
    starting centers and the scale parameters $\eta_i$ (Krishnapuram and
    Keller, 1993), which are kept fixed afterwards.

    Membership update:

    $$u_{ij} = \frac{1}{1 + (d_{ij}^2 / \eta_i)^{1/(m-1)}}$$

    Accepts the same hyperparameters as [FCM][fcmeans.FCM].

    Attributes:
        eta (NDArray): Scale parameter of each cluster, set by `fit`.

    Raises:
        ReferenceError: If called without the model being trained.
        NotImplementedError: If `partition_coefficient` or
        `partition_entropy_coefficient` is requested; both assume rows of
        `u` sum to one.
    """

    def _init_u(self, X: NDArray) -> None:
        """Initialize `u`, `_centers` and `eta` from a FCM run."""
        fcm = FCM(
            n_clusters=self.n_clusters,
            max_iter=self.max_iter,
            m=self.m,
            error=self.error,
            random_state=self.random_state,
            init=self.init,
            distance=self.distance,
            distance_params=self.distance_params,
        )
        fcm.fit(X)
        self.rng = fcm.rng
        self.u = fcm.u
        self._centers = fcm.centers
        um = self.u**self.m
        d2 = FCM._dist(X, self._centers, self.distance, self.distance_params)
        self.eta = (um * d2**2).sum(axis=0) / um.sum(axis=0)

    def _objective(self, X: NDArray) -> float:
        """PCM objective: weighted distances plus the `eta` penalty."""
        d = FCM._dist(X, self._centers, self.distance, self.distance_params)
        um = self.u**self.m
        return float(
            (um * d**2).sum() + (self.eta * (1 - self.u) ** self.m).sum()
        )

    @validate_call(config=dict(arbitrary_types_allowed=True))
    def soft_predict(self, X: NDArray) -> NDArray:
        """Typicality of each sample to each cluster

        Args:
            X (NDArray): New data to predict.

        Returns:
            NDArray: Typicality array with n_samples rows and n_clusters
            columns. Rows do not sum to one.
        """
        d2 = (
            FCM._dist(X, self._centers, self.distance, self.distance_params)
            ** 2
        )
        return 1.0 / (1.0 + (d2 / self.eta) ** (1 / (self.m - 1)))

    @property
    def partition_coefficient(self) -> float:
        """Not defined for possibilistic partitions."""
        raise NotImplementedError("Rows of `u` do not sum to one in PCM.")

    @property
    def partition_entropy_coefficient(self) -> float:
        """Not defined for possibilistic partitions."""
        raise NotImplementedError("Rows of `u` do not sum to one in PCM.")

__class_vars__ special

The names of the class variables defined on the model.

__private_attributes__ special

Metadata about the private attributes of the model.

__pydantic_complete__ special

Whether model building is completed, or if there are still undefined fields.

__pydantic_computed_fields__ special

A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_custom_init__ special

Whether the model has a custom __init__ method.

__pydantic_decorators__ special

Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.

__pydantic_extra_info__ special

A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.

This is a private attribute, not meant to be used outside Pydantic.

__pydantic_fields__ special

A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects. This replaces Model.__fields__ from Pydantic V1.

__pydantic_generic_metadata__ special

A dictionary containing metadata about generic Pydantic models.

The origin and args items map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and the parameter item maps to the __parameter__ attribute of generic classes.

__pydantic_parent_namespace__ special

Parent namespace of the model, used for automatic rebuilding of models.

__pydantic_post_init__ special

The name of the post-init method for the model, if defined.

__pydantic_setattr_handlers__ special

__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__

__signature__ special

The synthesized __init__ [Signature][inspect.Signature] of the model.

model_config

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

partition_coefficient: float property readonly

Not defined for possibilistic partitions.

partition_entropy_coefficient: float property readonly

Not defined for possibilistic partitions.

soft_predict(self, X)

Typicality of each sample to each cluster

Parameters:

Name Type Description Default
X NDArray

New data to predict.

required

Returns:

Type Description
NDArray

Typicality array with n_samples rows and n_clusters columns. Rows do not sum to one.

Source code in fcmeans/pcm.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
    """Typicality of each sample to each cluster

    Args:
        X (NDArray): New data to predict.

    Returns:
        NDArray: Typicality array with n_samples rows and n_clusters
        columns. Rows do not sum to one.
    """
    d2 = (
        FCM._dist(X, self._centers, self.distance, self.distance_params)
        ** 2
    )
    return 1.0 / (1.0 + (d2 / self.eta) ** (1 / (self.m - 1)))

Fuzzy-possibilistic C-means Model

Mixed model of Pal, Pal and Bezdek (1997). Each sample has a fuzzy membership \(u_{ij}\) (as in FCM, summing to one over the clusters) and a typicality \(t_{ij}\) (summing to one over the samples of each cluster). Centers are computed from both:

\[v_i = \frac{\sum_j (u_{ij}^m + t_{ij}^\eta) x_j} {\sum_j (u_{ij}^m + t_{ij}^\eta)}\]

Accepts the same hyperparameters as FCM, plus eta. soft_predict and predict use the fuzzy memberships only, since typicality is normalized over the training samples and is not defined for new data.

Attributes:

Name Type Description
eta float

Typicality exponent: \(\eta \in (1, \infty)\).

t NDArray

Typicality matrix of the training data, with n_samples

Exceptions:

Type Description
ReferenceError

If called without the model being trained.

Source code in fcmeans/fpcm.py
class FPCM(FCM):
    r"""Fuzzy-possibilistic C-means Model

    Mixed model of Pal, Pal and Bezdek (1997). Each sample has a fuzzy
    membership $u_{ij}$ (as in FCM, summing to one over the clusters) and a
    typicality $t_{ij}$ (summing to one over the samples of each cluster).
    Centers are computed from both:

    $$v_i = \frac{\sum_j (u_{ij}^m + t_{ij}^\eta) x_j}
    {\sum_j (u_{ij}^m + t_{ij}^\eta)}$$

    Accepts the same hyperparameters as [FCM][fcmeans.FCM], plus `eta`.
    `soft_predict` and `predict` use the fuzzy memberships only, since
    typicality is normalized over the training samples and is not defined
    for new data.

    Attributes:
        eta (float): Typicality exponent: $\eta \in (1, \infty)$.
        t (NDArray): Typicality matrix of the training data, with n_samples
        rows and n_clusters columns, set by `fit`.

    Raises:
        ReferenceError: If called without the model being trained.
    """

    eta: float = Field(2.0, gt=1.0)

    def _init_u(self, X: NDArray) -> None:
        """Initialize `u` randomly and derive `t` from it."""
        super()._init_u(X)
        self.t = self.u / self.u.sum(axis=0, keepdims=True)

    def _objective(self, X: NDArray) -> float:
        """Fuzzy and typicality weighted sum of squared distances."""
        d = FCM._dist(X, self._centers, self.distance, self.distance_params)
        return float(((self.u**self.m + self.t**self.eta) * d**2).sum())

    def _update_centers(self, X: NDArray) -> None:
        """Update `_centers` from the fuzzy and typicality weights."""
        w = self.u**self.m + self.t**self.eta
        self._centers = (X.T @ w / w.sum(axis=0)).T

    def _update_u(self, X: NDArray) -> None:
        """Update `u` and `t` from the current centers."""
        super()._update_u(X)
        d = FCM._dist(X, self._centers, self.distance, self.distance_params)
        w = d ** (-2 / (self.eta - 1))
        self.t = w / w.sum(axis=0, keepdims=True)

__class_vars__ special

The names of the class variables defined on the model.

__private_attributes__ special

Metadata about the private attributes of the model.

__pydantic_complete__ special

Whether model building is completed, or if there are still undefined fields.

__pydantic_computed_fields__ special

A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_custom_init__ special

Whether the model has a custom __init__ method.

__pydantic_decorators__ special

Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.

__pydantic_extra_info__ special

A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.

This is a private attribute, not meant to be used outside Pydantic.

__pydantic_fields__ special

A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects. This replaces Model.__fields__ from Pydantic V1.

__pydantic_generic_metadata__ special

A dictionary containing metadata about generic Pydantic models.

The origin and args items map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and the parameter item maps to the __parameter__ attribute of generic classes.

__pydantic_parent_namespace__ special

Parent namespace of the model, used for automatic rebuilding of models.

__pydantic_post_init__ special

The name of the post-init method for the model, if defined.

__pydantic_setattr_handlers__ special

__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__

__signature__ special

The synthesized __init__ [Signature][inspect.Signature] of the model.

model_config

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Gustafson-Kessel Model

FCM with an adaptive distance: each cluster \(i\) has its own norm matrix \(A_i\), so clusters can be ellipsoids of different orientations instead of spheres (Gustafson and Kessel, 1978). With the cluster volume fixed at one:

\[F_i = \frac{\sum_j u_{ij}^m (x_j - v_i)(x_j - v_i)^\top} {\sum_j u_{ij}^m} \qquad A_i = \det(F_i)^{1/p} F_i^{-1} \qquad d_{ij}^2 = (x_j - v_i)^\top A_i (x_j - v_i)\]

where \(p\) is the number of features. Memberships and centers are updated as in FCM.

Accepts the same hyperparameters as FCM, except that distance cannot be changed: the adaptive norm replaces it.

Attributes:

Name Type Description
reg float

Regularization of the covariance matrices. Each \(F_i\)

norm_matrices NDArray

Norm matrices \(A_i\) with shape

Exceptions:

Type Description
ReferenceError

If called without the model being trained.

ValidationError

If distance is not the default.

Source code in fcmeans/gk.py
class GK(FCM):
    r"""Gustafson-Kessel Model

    FCM with an adaptive distance: each cluster $i$ has its own norm matrix
    $A_i$, so clusters can be ellipsoids of different orientations instead of
    spheres (Gustafson and Kessel, 1978). With the cluster volume fixed at
    one:

    $$F_i = \frac{\sum_j u_{ij}^m (x_j - v_i)(x_j - v_i)^\top}
    {\sum_j u_{ij}^m}
    \qquad
    A_i = \det(F_i)^{1/p} F_i^{-1}
    \qquad
    d_{ij}^2 = (x_j - v_i)^\top A_i (x_j - v_i)$$

    where $p$ is the number of features. Memberships and centers are updated
    as in FCM.

    Accepts the same hyperparameters as [FCM][fcmeans.FCM], except that
    `distance` cannot be changed: the adaptive norm replaces it.

    Attributes:
        reg (float): Regularization of the covariance matrices. Each $F_i$
        gets `reg * trace(F_i) / p` added to its diagonal, which keeps it
        invertible when a cluster is degenerate (constant or collinear
        features, fewer samples than features).
        norm_matrices (NDArray): Norm matrices $A_i$ with shape
        (n_clusters, n_features, n_features), set by `fit`.

    Raises:
        ReferenceError: If called without the model being trained.
        ValidationError: If `distance` is not the default.
    """

    reg: float = Field(1e-6, ge=0.0)

    @field_validator("distance")
    @classmethod
    def _default_distance_only(cls, v):
        if v != DistanceOptions.euclidean:
            raise ValueError(
                "GK uses its own adaptive norm; `distance` is fixed."
            )
        return v

    def _covariances(self, X: NDArray) -> NDArray:
        """Regularized fuzzy covariance matrix of each cluster."""
        diff = X[:, None, :] - self._centers
        um = self.u**self.m
        p = X.shape[1]
        F = np.einsum("nc,ncp,ncq->cpq", um, diff, diff)
        F /= um.sum(axis=0)[:, None, None]
        F += (
            self.reg * np.trace(F, axis1=1, axis2=2)[:, None, None] / p
        ) * np.eye(p)
        return F

    def _update_centers(self, X: NDArray) -> None:
        """Update `_centers` and the cluster shape (see `_update_shape`)."""
        super()._update_centers(X)
        self._update_shape(X)

    def _update_shape(self, X: NDArray) -> None:
        """Update the norm matrices `A_i` from the current centers."""
        F = self._covariances(X)
        p = X.shape[1]
        _, logdet = np.linalg.slogdet(F)
        scale = np.exp(logdet / p)[:, None, None]
        self.norm_matrices = scale * np.linalg.inv(F)

    def _distances(self, X: NDArray) -> NDArray:
        """Distance from each sample to each center under its own norm."""
        diff = X[:, None, :] - self._centers
        return np.sqrt(
            np.einsum("ncp,cpq,ncq->nc", diff, self.norm_matrices, diff)
        )

__class_vars__ special

The names of the class variables defined on the model.

__private_attributes__ special

Metadata about the private attributes of the model.

__pydantic_complete__ special

Whether model building is completed, or if there are still undefined fields.

__pydantic_computed_fields__ special

A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_custom_init__ special

Whether the model has a custom __init__ method.

__pydantic_decorators__ special

Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.

__pydantic_extra_info__ special

A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.

This is a private attribute, not meant to be used outside Pydantic.

__pydantic_fields__ special

A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects. This replaces Model.__fields__ from Pydantic V1.

__pydantic_generic_metadata__ special

A dictionary containing metadata about generic Pydantic models.

The origin and args items map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and the parameter item maps to the __parameter__ attribute of generic classes.

__pydantic_parent_namespace__ special

Parent namespace of the model, used for automatic rebuilding of models.

__pydantic_post_init__ special

The name of the post-init method for the model, if defined.

__pydantic_setattr_handlers__ special

__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__

__signature__ special

The synthesized __init__ [Signature][inspect.Signature] of the model.

model_config

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Gath-Geva Model

Fuzzy maximum likelihood clustering (Gath and Geva, 1989). Like GK, each cluster has its own covariance matrix \(F_i\), and it also has a prior probability \(\alpha_i\), so clusters can differ in shape, size and density. The squared distance is the inverse of a Gaussian density:

\[\alpha_i = \frac{\sum_j u_{ij}^m}{\sum_{l,j} u_{lj}^m} \qquad d_{ij}^2 = \frac{\sqrt{\det F_i}}{\alpha_i} \exp\left(\tfrac{1}{2} (x_j - v_i)^\top F_i^{-1} (x_j - v_i)\right)\]

with \(F_i\) the fuzzy covariance matrix of GK. Memberships are computed from \(d_{ij}^2\) as in FCM, in the log domain so that samples far from every cluster do not overflow.

The exponential distance makes GG very sensitive to its starting point, so the model is initialized with a GK run (same hyperparameters) and the GG iterations start from its partition.

Accepts the same hyperparameters as GK (distance cannot be changed).

Attributes:

Name Type Description
covariances NDArray

Covariance matrices \(F_i\) with shape

priors NDArray

Prior probabilities \(\alpha_i\), summing to one, set

Exceptions:

Type Description
ReferenceError

If called without the model being trained.

ValidationError

If distance is not the default.

Source code in fcmeans/gg.py
class GG(GK):
    r"""Gath-Geva Model

    Fuzzy maximum likelihood clustering (Gath and Geva, 1989). Like GK, each
    cluster has its own covariance matrix $F_i$, and it also has a prior
    probability $\alpha_i$, so clusters can differ in shape, size and
    density. The squared distance is the inverse of a Gaussian density:

    $$\alpha_i = \frac{\sum_j u_{ij}^m}{\sum_{l,j} u_{lj}^m}
    \qquad
    d_{ij}^2 = \frac{\sqrt{\det F_i}}{\alpha_i}
    \exp\left(\tfrac{1}{2} (x_j - v_i)^\top F_i^{-1} (x_j - v_i)\right)$$

    with $F_i$ the fuzzy covariance matrix of [GK][fcmeans.GK]. Memberships
    are computed from $d_{ij}^2$ as in FCM, in the log domain so that samples
    far from every cluster do not overflow.

    The exponential distance makes GG very sensitive to its starting point,
    so the model is initialized with a GK run (same hyperparameters) and the
    GG iterations start from its partition.

    Accepts the same hyperparameters as [GK][fcmeans.GK] (`distance` cannot
    be changed).

    Attributes:
        covariances (NDArray): Covariance matrices $F_i$ with shape
        (n_clusters, n_features, n_features), set by `fit`.
        priors (NDArray): Prior probabilities $\alpha_i$, summing to one, set
        by `fit`.

    Raises:
        ReferenceError: If called without the model being trained.
        ValidationError: If `distance` is not the default.
    """

    def _init_u(self, X: NDArray) -> None:
        """Initialize `u` from a GK run."""
        gk = GK(
            n_clusters=self.n_clusters,
            max_iter=self.max_iter,
            m=self.m,
            error=self.error,
            random_state=self.random_state,
            init=self.init,
            reg=self.reg,
        )
        gk.fit(X)
        self.rng = gk.rng
        self.u = gk.u

    def _update_shape(self, X: NDArray) -> None:
        """Update covariances, priors and their determinant and inverse."""
        um = self.u**self.m
        self.covariances = self._covariances(X)
        self.priors = um.sum(axis=0) / um.sum()
        self._logdet = np.linalg.slogdet(self.covariances)[1]
        self._inv_cov = np.linalg.inv(self.covariances)

    def _objective(self, X: NDArray) -> float:
        """Log of the GG objective (the objective itself can overflow)."""
        diff = X[:, None, :] - self._centers
        mahalanobis = np.einsum("ncp,cpq,ncq->nc", diff, self._inv_cov, diff)
        log_d2 = 0.5 * self._logdet - np.log(self.priors) + 0.5 * mahalanobis
        with np.errstate(divide="ignore"):
            a = self.m * np.log(self.u) + log_d2
        top = a.max()
        return float(top + np.log(np.exp(a - top).sum()))

    @validate_call(config=dict(arbitrary_types_allowed=True))
    def soft_predict(self, X: NDArray) -> NDArray:
        """Soft predict of GG

        Args:
            X (NDArray): New data to predict.

        Returns:
            NDArray: Fuzzy partition array, returned as an array with
            n_samples rows and n_clusters columns.
        """
        diff = X[:, None, :] - self._centers
        mahalanobis = np.einsum("ncp,cpq,ncq->nc", diff, self._inv_cov, diff)
        log_d2 = 0.5 * self._logdet - np.log(self.priors) + 0.5 * mahalanobis
        z = -log_d2 / (self.m - 1)
        z -= z.max(axis=1, keepdims=True)
        e = np.exp(z)
        return e / e.sum(axis=1, keepdims=True)

__class_vars__ special

The names of the class variables defined on the model.

__private_attributes__ special

Metadata about the private attributes of the model.

__pydantic_complete__ special

Whether model building is completed, or if there are still undefined fields.

__pydantic_computed_fields__ special

A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_custom_init__ special

Whether the model has a custom __init__ method.

__pydantic_decorators__ special

Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.

__pydantic_extra_info__ special

A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.

This is a private attribute, not meant to be used outside Pydantic.

__pydantic_fields__ special

A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects. This replaces Model.__fields__ from Pydantic V1.

__pydantic_generic_metadata__ special

A dictionary containing metadata about generic Pydantic models.

The origin and args items map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and the parameter item maps to the __parameter__ attribute of generic classes.

__pydantic_parent_namespace__ special

Parent namespace of the model, used for automatic rebuilding of models.

__pydantic_post_init__ special

The name of the post-init method for the model, if defined.

__pydantic_setattr_handlers__ special

__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__

__signature__ special

The synthesized __init__ [Signature][inspect.Signature] of the model.

model_config

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

soft_predict(self, X)

Soft predict of GG

Parameters:

Name Type Description Default
X NDArray

New data to predict.

required

Returns:

Type Description
NDArray

Fuzzy partition array, returned as an array with n_samples rows and n_clusters columns.

Source code in fcmeans/gg.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def soft_predict(self, X: NDArray) -> NDArray:
    """Soft predict of GG

    Args:
        X (NDArray): New data to predict.

    Returns:
        NDArray: Fuzzy partition array, returned as an array with
        n_samples rows and n_clusters columns.
    """
    diff = X[:, None, :] - self._centers
    mahalanobis = np.einsum("ncp,cpq,ncq->nc", diff, self._inv_cov, diff)
    log_d2 = 0.5 * self._logdet - np.log(self.priors) + 0.5 * mahalanobis
    z = -log_d2 / (self.m - 1)
    z -= z.max(axis=1, keepdims=True)
    e = np.exp(z)
    return e / e.sum(axis=1, keepdims=True)

Fuzzy C-medoids Model

FCM whose prototypes are restricted to be samples of the data set, the medoids (Krishnapuram, Joshi, Nasraoui and Yi, 2001). It minimizes the FCM objective with \(v_i \in X\):

\[J = \sum_{i=1}^{c} \sum_{j=1}^{n} u_{ij}^m d^2(x_j, v_i)\]

Memberships are updated as in FCM. Each medoid is the sample that minimizes the membership-weighted cost of its cluster:

\[v_i = \operatorname*{arg\,min}_{x_k \in X} \sum_{j=1}^{n} u_{ij}^m d^2(x_j, x_k)\]

Since the search only compares distances, any distance of FCM works, including a custom callable. The initial medoids are seeded with k-means++ on the distance matrix, using random_state, and the model has converged when the medoids stop changing.

The distance between all pairs of training samples is computed once, so fit needs O(n²) memory.

Accepts the same hyperparameters as FCM.

Attributes:

Name Type Description
medoid_indices NDArray

Indices in the training data of the

Exceptions:

Type Description
ReferenceError

If called without the model being trained.

ValueError

If n_clusters is greater than the number of samples.

Source code in fcmeans/fcmedoids.py
class FCMedoids(FCM):
    r"""Fuzzy C-medoids Model

    FCM whose prototypes are restricted to be samples of the data set, the
    *medoids* (Krishnapuram, Joshi, Nasraoui and Yi, 2001). It minimizes the
    FCM objective with $v_i \in X$:

    $$J = \sum_{i=1}^{c} \sum_{j=1}^{n} u_{ij}^m d^2(x_j, v_i)$$

    Memberships are updated as in FCM. Each medoid is the sample that
    minimizes the membership-weighted cost of its cluster:

    $$v_i = \operatorname*{arg\,min}_{x_k \in X}
    \sum_{j=1}^{n} u_{ij}^m d^2(x_j, x_k)$$

    Since the search only compares distances, any `distance` of
    [FCM][fcmeans.FCM] works, including a custom callable. The initial
    medoids are seeded with k-means++ on the distance matrix, using
    `random_state`, and the model has converged when the medoids stop
    changing.

    The distance between all pairs of training samples is computed once, so
    `fit` needs O(n²) memory.

    Accepts the same hyperparameters as [FCM][fcmeans.FCM].

    Attributes:
        medoid_indices (NDArray): Indices in the training data of the
        medoids, set by `fit`. `centers` is the training data at these
        indices.

    Raises:
        ReferenceError: If called without the model being trained.
        ValueError: If `n_clusters` is greater than the number of samples.
    """

    def _init_u(self, X: NDArray) -> None:
        """Draw the initial medoids and derive `u` from them."""
        n = X.shape[0]
        if self.n_clusters > n:
            raise ValueError(
                f"n_clusters ({self.n_clusters}) cannot exceed the number "
                f"of samples ({n})."
            )
        self.rng = np.random.default_rng(self.random_state)
        # ponytail: full n x n matrix, O(n^2) memory. Chunk or restrict
        # candidate medoids if it matters.
        chunks = np.array_split(X, max(1, n // 256))
        self._d2 = np.vstack(
            [
                FCM._dist(c, X, self.distance, self.distance_params) ** 2
                for c in chunks
            ]
        )
        # k-means++ seeding on the distance matrix: each new medoid is drawn
        # with probability proportional to its cost to the closest chosen one
        chosen = [self.rng.integers(n)]
        closest = self._d2[:, chosen[0]]
        for _ in range(1, self.n_clusters):
            total = closest.sum()
            chosen.append(
                self.rng.choice(n, p=closest / total if total > 0 else None)
            )
            closest = np.minimum(closest, self._d2[:, chosen[-1]])
        self.medoid_indices = np.array(chosen)
        self._centers = X[self.medoid_indices]
        self.u = self.soft_predict(X)

    def _update_centers(self, X: NDArray) -> None:
        """Update the medoids from the current partition matrix `u`."""
        cost = (self.u**self.m).T @ self._d2
        self.medoid_indices = cost.argmin(axis=1)
        self._centers = X[self.medoid_indices]

    @validate_call(config=dict(arbitrary_types_allowed=True))
    def fit(self, X: NDArray) -> None:
        """Train the fuzzy c-medoids model

        Args:
            X (NDArray): Training instances to cluster.
        """
        try:
            super().fit(X)
        finally:
            self.__dict__.pop("_d2", None)

__class_vars__ special

The names of the class variables defined on the model.

__private_attributes__ special

Metadata about the private attributes of the model.

__pydantic_complete__ special

Whether model building is completed, or if there are still undefined fields.

__pydantic_computed_fields__ special

A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_custom_init__ special

Whether the model has a custom __init__ method.

__pydantic_decorators__ special

Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.

__pydantic_extra_info__ special

A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.

This is a private attribute, not meant to be used outside Pydantic.

__pydantic_fields__ special

A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects. This replaces Model.__fields__ from Pydantic V1.

__pydantic_generic_metadata__ special

A dictionary containing metadata about generic Pydantic models.

The origin and args items map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and the parameter item maps to the __parameter__ attribute of generic classes.

__pydantic_parent_namespace__ special

Parent namespace of the model, used for automatic rebuilding of models.

__pydantic_post_init__ special

The name of the post-init method for the model, if defined.

__pydantic_setattr_handlers__ special

__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__

__signature__ special

The synthesized __init__ [Signature][inspect.Signature] of the model.

model_config

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

fit(self, X)

Train the fuzzy c-medoids model

Parameters:

Name Type Description Default
X NDArray

Training instances to cluster.

required
Source code in fcmeans/fcmedoids.py
@validate_call(config=dict(arbitrary_types_allowed=True))
def fit(self, X: NDArray) -> None:
    """Train the fuzzy c-medoids model

    Args:
        X (NDArray): Training instances to cluster.
    """
    try:
        super().fit(X)
    finally:
        self.__dict__.pop("_d2", None)

Kernel Fuzzy C-means Model

FCM with the distance measured in the feature space of a Gaussian kernel \(K(x, v) = \exp(-\gamma \|x - v\|^2)\), while the prototypes stay in the input space (Wu, Xie and Yu, 2003):

\[d^2(x, v) = \|\phi(x) - \phi(v)\|^2 = 2 (1 - K(x, v))\]

Memberships are updated as in FCM with this distance. Centers are the kernel-weighted means

\[v_i = \frac{\sum_j u_{ij}^m K(x_j, v_i) x_j} {\sum_j u_{ij}^m K(x_j, v_i)}\]

so samples far from a center, such as outliers, get almost no weight.

Accepts the same hyperparameters as FCM, plus gamma; distance cannot be changed.

Attributes:

Name Type Description
gamma Optional[float]

Kernel coefficient, greater than zero. If

gamma_ float

Value of gamma used by the fitted model.

Exceptions:

Type Description
ReferenceError

If called without the model being trained.

ValidationError

If distance is not the default or gamma is not

Source code in fcmeans/kfcm.py
class KFCM(FCM):
    r"""Kernel Fuzzy C-means Model

    FCM with the distance measured in the feature space of a Gaussian kernel
    $K(x, v) = \exp(-\gamma \|x - v\|^2)$, while the prototypes stay in the
    input space (Wu, Xie and Yu, 2003):

    $$d^2(x, v) = \|\phi(x) - \phi(v)\|^2 = 2 (1 - K(x, v))$$

    Memberships are updated as in FCM with this distance. Centers are the
    kernel-weighted means

    $$v_i = \frac{\sum_j u_{ij}^m K(x_j, v_i) x_j}
    {\sum_j u_{ij}^m K(x_j, v_i)}$$

    so samples far from a center, such as outliers, get almost no weight.

    Accepts the same hyperparameters as [FCM][fcmeans.FCM], plus `gamma`;
    `distance` cannot be changed.

    Attributes:
        gamma (Optional[float]): Kernel coefficient, greater than zero. If
        None, `1 / (n_features * X.var())` is used.
        gamma_ (float): Value of `gamma` used by the fitted model.

    Raises:
        ReferenceError: If called without the model being trained.
        ValidationError: If `distance` is not the default or `gamma` is not
        positive.
    """

    gamma: Optional[float] = Field(None, gt=0.0)

    @field_validator("distance")
    @classmethod
    def _default_distance_only(cls, v):
        if v != DistanceOptions.euclidean:
            raise ValueError(
                "KFCM uses the Gaussian kernel distance; `distance` is fixed."
            )
        return v

    def _init_u(self, X: NDArray) -> None:
        """Initialize `u`, the centers and `gamma_` from a FCM run."""
        fcm = FCM(
            n_clusters=self.n_clusters,
            max_iter=self.max_iter,
            m=self.m,
            error=self.error,
            random_state=self.random_state,
            init=self.init,
        )
        fcm.fit(X)
        self.rng = fcm.rng
        self.u = fcm.u
        self._centers = fcm.centers
        var = X.var()
        self.gamma_ = self.gamma or (
            1.0 / (X.shape[1] * var) if var > 0 else 1.0
        )

    def _kernel(self, X: NDArray) -> NDArray:
        """Gaussian kernel between the samples and the centers."""
        return np.exp(-self.gamma_ * self._sq_dist(X))

    def _sq_dist(self, X: NDArray) -> NDArray:
        """Squared euclidean distance between samples and centers."""
        return np.einsum("ijk->ij", (X[:, None, :] - self._centers) ** 2)

    def _update_centers(self, X: NDArray) -> None:
        """Update `_centers` as kernel-weighted means."""
        w = self.u**self.m * self._kernel(X)
        den = w.sum(axis=0)[:, None]
        # a center that no sample reaches (all weights 0) stays where it is
        self._centers = np.divide(
            (X.T @ w).T, den, out=self._centers.copy(), where=den > 0
        )

    def _distances(self, X: NDArray) -> NDArray:
        """Distance in feature space, from `1 - K` computed without loss."""
        return np.sqrt(-2.0 * np.expm1(-self.gamma_ * self._sq_dist(X)))

__class_vars__ special

The names of the class variables defined on the model.

__private_attributes__ special

Metadata about the private attributes of the model.

__pydantic_complete__ special

Whether model building is completed, or if there are still undefined fields.

__pydantic_computed_fields__ special

A dictionary of computed field names and their corresponding [ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects.

__pydantic_custom_init__ special

Whether the model has a custom __init__ method.

__pydantic_decorators__ special

Metadata containing the decorators defined on the model. This replaces Model.__validators__ and Model.__root_validators__ from Pydantic V1.

__pydantic_extra_info__ special

A wrapper around the __pydantic_extra__ annotation, if explicitly annotated on a model.

This is a private attribute, not meant to be used outside Pydantic.

__pydantic_fields__ special

A dictionary of field names and their corresponding [FieldInfo][pydantic.fields.FieldInfo] objects. This replaces Model.__fields__ from Pydantic V1.

__pydantic_generic_metadata__ special

A dictionary containing metadata about generic Pydantic models.

The origin and args items map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and the parameter item maps to the __parameter__ attribute of generic classes.

__pydantic_parent_namespace__ special

Parent namespace of the model, used for automatic rebuilding of models.

__pydantic_post_init__ special

The name of the post-init method for the model, if defined.

__pydantic_setattr_handlers__ special

__setattr__ handlers. Memoizing the handlers leads to a dramatic performance improvement in __setattr__

__signature__ special

The synthesized __init__ [Signature][inspect.Signature] of the model.

model_config

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Cluster validity indices for fuzzy partitions.

All indices take the data X (n x d), the membership matrix u (n x c) and the cluster centers (c x d) of a trained model, and use the euclidean distance. They need at least two clusters.

davies_bouldin(X, u, centers, m=2.0)

Fuzzy Davies-Bouldin index (lower is better).

The crisp scatter of each cluster is replaced by a membership weighted one, \(S_i = \sqrt{\sum_k u_{ik}^m \lVert x_k - v_i \rVert^2 / \sum_k u_{ik}^m}\), and

\[ DB = \frac{1}{c} \sum_i \max_{j \neq i} \frac{S_i + S_j}{\lVert v_i - v_j \rVert} \]

Davies, D. L., and D. W. Bouldin. "A cluster separation measure." IEEE TPAMI 1.2 (1979): 224-227.

Source code in fcmeans/validation.py
def davies_bouldin(
    X: NDArray, u: NDArray, centers: NDArray, m: float = 2.0
) -> float:
    r"""Fuzzy Davies-Bouldin index (lower is better).

    The crisp scatter of each cluster is replaced by a membership weighted
    one, $S_i = \sqrt{\sum_k u_{ik}^m \lVert x_k - v_i \rVert^2 /
    \sum_k u_{ik}^m}$, and

    $$
    DB = \frac{1}{c} \sum_i \max_{j \neq i}
    \frac{S_i + S_j}{\lVert v_i - v_j \rVert}
    $$

    Davies, D. L., and D. W. Bouldin. "[A cluster separation
    measure.](https://doi.org/10.1109/TPAMI.1979.4766909)" _IEEE TPAMI_ 1.2
    (1979): 224-227.
    """
    d2c = _sq_center_dist(centers)
    um = u**m
    scatter = np.sqrt(np.sum(um * _sq_dist(X, centers), axis=0) / um.sum(0))
    with np.errstate(divide="ignore"):
        ratio = (scatter[:, None] + scatter) / np.sqrt(d2c)
    np.fill_diagonal(ratio, -np.inf)
    return float(ratio.max(axis=1).mean())

fukuyama_sugeno(X, u, centers, m=2.0)

Fukuyama-Sugeno index (lower is better).

\[ FS = \sum_{i,k} u_{ik}^m \left(\lVert x_k - v_i \rVert^2 - \lVert v_i - \bar{v} \rVert^2\right) \]

where \(\bar{v}\) is the mean of the centers.

Fukuyama, Y., and M. Sugeno. "A new method of choosing the number of clusters for the fuzzy c-means method." Proc. 5th Fuzzy Systems Symposium (1989): 247-250.

Source code in fcmeans/validation.py
def fukuyama_sugeno(
    X: NDArray, u: NDArray, centers: NDArray, m: float = 2.0
) -> float:
    r"""Fukuyama-Sugeno index (lower is better).

    $$
    FS = \sum_{i,k} u_{ik}^m \left(\lVert x_k - v_i \rVert^2 -
    \lVert v_i - \bar{v} \rVert^2\right)
    $$

    where $\bar{v}$ is the mean of the centers.

    Fukuyama, Y., and M. Sugeno. "A new method of choosing the number of
    clusters for the fuzzy c-means method." _Proc. 5th Fuzzy Systems
    Symposium_ (1989): 247-250.
    """
    _sq_center_dist(centers)
    spread = np.sum((centers - centers.mean(axis=0)) ** 2, axis=1)
    return float(np.sum(u**m * (_sq_dist(X, centers) - spread)))

fuzzy_silhouette(X, u, centers, m=2.0, alpha=1.0)

Fuzzy silhouette (higher is better).

The silhouette \(s_k\) of each sample is computed on the crisp partition given by u.argmax(axis=1) and averaged with weights \((u_{pk} - u_{qk})^\alpha\), where \(u_{pk}\) and \(u_{qk}\) are the two largest memberships of sample \(k\). Samples with no clear cluster weigh less. m is unused and only kept for a common signature.

Builds the n x n distance matrix, so it needs O(n^2) memory.

Campello, R. J. G. B., and E. R. Hruschka. "A fuzzy extension of the silhouette width criterion for cluster analysis." Fuzzy Sets and Systems 157.21 (2006): 2858-2875.

Source code in fcmeans/validation.py
def fuzzy_silhouette(
    X: NDArray,
    u: NDArray,
    centers: NDArray,
    m: float = 2.0,
    alpha: float = 1.0,
) -> float:
    r"""Fuzzy silhouette (higher is better).

    The silhouette $s_k$ of each sample is computed on the crisp partition
    given by `u.argmax(axis=1)` and averaged with weights
    $(u_{pk} - u_{qk})^\alpha$, where $u_{pk}$ and $u_{qk}$ are the two
    largest memberships of sample $k$. Samples with no clear cluster weigh
    less. `m` is unused and only kept for a common signature.

    Builds the n x n distance matrix, so it needs O(n^2) memory.

    Campello, R. J. G. B., and E. R. Hruschka. "[A fuzzy extension of the
    silhouette width criterion for cluster
    analysis.](https://doi.org/10.1016/j.fss.2006.07.006)" _Fuzzy Sets and
    Systems_ 157.21 (2006): 2858-2875.
    """
    _sq_center_dist(centers)
    n, c = u.shape
    labels = u.argmax(axis=1)
    onehot = np.eye(c)[labels]
    counts = onehot.sum(axis=0)
    if (counts > 0).sum() < 2:
        raise ValueError(
            "The partition needs at least two non-empty clusters."
        )

    sq = np.sum(X**2, axis=1)
    D = np.sqrt(np.maximum(sq[:, None] + sq - 2 * X @ X.T, 0.0))
    sums = D @ onehot  # total distance from each sample to each cluster
    own = np.arange(n), labels
    with np.errstate(divide="ignore", invalid="ignore"):
        a = sums[own] / (counts[labels] - 1)
        mean_to = sums / counts
    mean_to[own] = np.inf
    b = mean_to.min(axis=1)
    with np.errstate(divide="ignore", invalid="ignore"):
        s = (b - a) / np.maximum(a, b)
    s[(counts[labels] == 1) | ~np.isfinite(s)] = 0.0  # singleton or all equal

    top2 = np.sort(u, axis=1)[:, -2:]
    w = (top2[:, 1] - top2[:, 0]) ** alpha
    return float(np.sum(w * s) / np.sum(w))

select_n_clusters(X, n_clusters=range(2, 11), index='xie_beni', model=<class 'fcmeans.main.FCM'>, **params)

Fit model for each number of clusters and score it with index.

Parameters:

Name Type Description Default
X NDArray

Data to cluster.

required
n_clusters Iterable[int]

Numbers of clusters to try, each >= 2.

range(2, 11)
index str

Name of a function in INDICES, such as xie_beni.

'xie_beni'
model type[FCM]

FCM or a variant whose u rows sum to one.

<class 'fcmeans.main.FCM'>
**params

Passed to model, for example m or random_state.

{}

Returns:

Type Description
tuple[int, dict[int, float]]

The best number of clusters and the score of each one.

Source code in fcmeans/validation.py
def select_n_clusters(
    X: NDArray,
    n_clusters: Iterable[int] = range(2, 11),
    index: str = "xie_beni",
    model: type[FCM] = FCM,
    **params,
) -> tuple[int, dict[int, float]]:
    """Fit `model` for each number of clusters and score it with `index`.

    Args:
        X (NDArray): Data to cluster.
        n_clusters (Iterable[int]): Numbers of clusters to try, each >= 2.
        index (str): Name of a function in `INDICES`, such as `xie_beni`.
        model (type[FCM]): `FCM` or a variant whose `u` rows sum to one.
        **params: Passed to `model`, for example `m` or `random_state`.

    Returns:
        tuple[int, dict[int, float]]: The best number of clusters and the
        score of each one.
    """
    if index not in INDICES:
        raise ValueError(
            f"Unknown index {index!r}, use one of {list(INDICES)}"
        )
    func, higher_is_better = INDICES[index]
    scores = {}
    for c in n_clusters:
        fcm = model(n_clusters=c, **params)
        fcm.fit(X)
        scores[c] = func(X, fcm.u, fcm.centers, fcm.m)
    pick = max if higher_is_better else min
    return pick(scores, key=scores.__getitem__), scores

xie_beni(X, u, centers, m=2.0)

Xie-Beni index (lower is better).

\[ XB = \frac{\sum_{i,k} u_{ik}^m \lVert x_k - v_i \rVert^2} {n \min_{i \neq j} \lVert v_i - v_j \rVert^2} \]

Xie, X. L., and G. Beni. "A validity measure for fuzzy clustering." IEEE TPAMI 13.8 (1991): 841-847.

Source code in fcmeans/validation.py
def xie_beni(
    X: NDArray, u: NDArray, centers: NDArray, m: float = 2.0
) -> float:
    r"""Xie-Beni index (lower is better).

    $$
    XB = \frac{\sum_{i,k} u_{ik}^m \lVert x_k - v_i \rVert^2}
    {n \min_{i \neq j} \lVert v_i - v_j \rVert^2}
    $$

    Xie, X. L., and G. Beni. "[A validity measure for fuzzy
    clustering.](https://doi.org/10.1109/34.85677)" _IEEE TPAMI_ 13.8
    (1991): 841-847.
    """
    d2c = _sq_center_dist(centers)
    compactness = np.sum(u**m * _sq_dist(X, centers))
    return float(compactness / (len(X) * d2c.min()))