Metadata-Version: 2.4
Name: gmm-membership
Version: 0.3.2
Summary: Membership functions derived from one-dimensional Gaussian mixture posteriors
Author-email: Aleksandra Suwalska <Aleksandra.Suwalska@polsl.pl>
License-Expression: MIT
Project-URL: Repository, https://github.com/Aleksandra795/gmm-membership
Project-URL: Publication, https://doi.org/10.3390/ijms241814033
Keywords: gaussian-mixture-model,membership-functions,posterior-probabilities,rare-cell-subtypes,mass-cytometry
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy<3,>=1.24
Requires-Dist: scipy<2,>=1.10
Requires-Dist: matplotlib<4,>=3.7
Requires-Dist: seaborn<1,>=0.13
Provides-Extra: dev
Requires-Dist: pytest<9,>=8; extra == "dev"
Requires-Dist: ruff<1,>=0.6; extra == "dev"
Dynamic: license-file

# GMM Membership Functions

`gmm-membership` converts posterior probabilities from one-dimensional Gaussian mixture models into ordered membership functions. It corrects tail-dominance reversals caused by components with unequal standard deviations and returns the intersections of adjacent corrected curves as cluster thresholds.

## Methodological reference

This is a Python impementation of the MATLAB algorithm described in:

Suwalska A, Polanska J. GMM-Based Expanded Feature Space as a Way to Extract Useful Information for Rare Cell Subtypes Identification in Single-Cell Mass Cytometry. *International Journal of Molecular Sciences*. 2023;24(18):14033. doi:10.3390/ijms241814033.

## Algorithm summary

For a one-dimensional Gaussian mixture with component weights $\pi_k$, means $\mu_k$, and standard deviations $\sigma_k$, the posterior probability of component $k$ is

$$
P(k \mid x) =
\frac{
    \pi_k\,\mathcal{N}(x \mid \mu_k,\sigma_k)
}{
    \sum_j \pi_j\,\mathcal{N}(x \mid \mu_j,\sigma_j)
}
$$

When component variances differ substantially, a broad component can dominate a distant tail even when another component has the nearest ordered mean. The dominant-component sequence can therefore decrease as the feature value increases, for example from Component 2 back to Component 1.

The implemented pipeline performs the following operations:

1. Sort components by increasing mean.
2. Compute posterior probabilities on a regular grid.
3. Smooth each posterior curve with a Savitzky-Golay projection filter.
4. Remove components that never dominate within their own local region.
5. Recompute and normalize posterior probabilities for retained components.
6. Construct a left-boundary membership function anchored at one on the left.
7. Construct interior membership functions anchored at zero at both extremes.
8. Construct a right-boundary membership function anchored at one on the right.
9. Use shape-preserving PCHIP interpolation between retained probability segments and anchors.
10. Locate the ordered intersection between every adjacent corrected curve pair. These `n_components - 1` values are returned as cluster thresholds.

The resulting values are membership functions, not a probability simplex. Their row-wise sum is not constrained to one.

## Installation

Recommended, from pip:

```bash
python -m pip install gmm-membership
```

Directly from GitHub:

```bash
python -m pip install git+https://github.com/Aleksandra795/gmm-membership.git
```

## Minimal workflow

```python
import numpy as np

from gmm_membership import build_ordered_membership_functions

# Columns: mean, standard deviation, component weight
parameters = np.array([
    [-2.50, 0.22, 0.08],
    [-1.60, 1.10, 0.22],
    [ 0.50, 0.35, 0.12],
    [ 2.40, 0.95, 0.22],
    [ 4.00, 1.20, 0.26],
    [ 4.80, 0.25, 0.10],
])

result = build_ordered_membership_functions(
    parameters,
    grid_min=-6.0,
    grid_max=8.8,
)

posterior = result.posterior_probabilities
membership = result.membership_functions
thresholds = result.thresholds

new_observations = np.array([-2.6, -1.4, 0.4, 4.9])
expanded_features = result.transform(new_observations)
```

## Visualization functions

Each function returns `(figure, axes)` and optionally saves the figure through `save_path`.

### GMM density and observations

```python
from gmm_membership import plot_gmm_density

figure, axes = plot_gmm_density(
    data,
    result.parameters,
    grid=result.grid,
    save_path="gmm_density.png",
)
```

![GMM density](https://raw.githubusercontent.com/Aleksandra795/gmm-membership/main/figures/01_gmm_density.png)

### Posterior probabilities before correction

```python
from gmm_membership import plot_posterior_probabilities

figure, axes = plot_posterior_probabilities(
    result.grid,
    result.posterior_probabilities,
    cmap="colorblind",
    save_path="posterior_probabilities.png",
)
```

![Posterior probabilities](https://raw.githubusercontent.com/Aleksandra795/gmm-membership/main/figures/02_posterior_probabilities.png)

### Membership functions after correction

```python
from gmm_membership import plot_membership_functions

figure, axes = plot_membership_functions(
    result.grid,
    result.membership_functions,
    thresholds=result.thresholds,
    show_thresholds=True,
    cmap="colorblind",
    save_path="membership_functions.png",
)
```

![Membership functions](https://raw.githubusercontent.com/Aleksandra795/gmm-membership/main/figures/03_membership_functions.png)

The `cmap` argument accepts a Seaborn palette name, a Matplotlib colormap name or object, or a custom color list. Custom lists are cycled when too short and truncated when too long.

```python
custom_colors = ["#0072B2", "#D55E00", "#009E73"]

plot_membership_functions(
    result.grid,
    result.membership_functions,
    thresholds=result.thresholds,
    show_thresholds=True,
    cmap=custom_colors,
)
```

## Reproducible synthetic example

The package includes a fixed six-component dataset with 10,000 observations. Components 1 and 2 create a left-tail conflict, while Components 5 and 6 create a right-tail conflict.

```bash
python examples/synthetic_tail_conflict.py
```

The example saves:

```text
demo_output/
├── 01_gmm_density.png
├── 02_posterior_probabilities.png
├── 03_membership_functions.png
├── cluster_thresholds.csv
├── gmm_parameters.csv
├── membership_functions.csv
├── posterior_probabilities.csv
└── synthetic_data.csv
```

The example prints and exports the five cluster boundaries returned through `result.thresholds`. Each boundary is the interpolated intersection of adjacent corrected membership functions at their ordered change of dominance.

## Public API

Core computation:

```text
build_ordered_membership_functions
compute_membership_thresholds
compute_posterior_probabilities
compute_weighted_component_densities
identify_active_components
evaluate_membership_functions
```

Diagnostics:

```text
find_dominance_reversals
```

Visualization:

```text
plot_gmm_density
plot_posterior_probabilities
plot_membership_functions
```

Synthetic data:

```text
sample_gaussian_mixture
make_tail_conflict_dataset
```

See [docs/API.md](docs/API.md) for a compact API description.

## Interpretation

The corrected outputs are membership functions rather than normalized posterior probabilities. Consequently, membership values across components are not required to sum to one for every observation.

## Citation

When this implementation is used to apply the ordered GMM membership-function method, cite:

Suwalska A, Polanska J. GMM-Based Expanded Feature Space as a Way to Extract Useful Information for Rare Cell Subtypes Identification in Single-Cell Mass Cytometry. International Journal of Molecular Sciences. 2023;24(18):14033. doi:10.3390/ijms241814033.
