Metadata-Version: 2.4
Name: mtune
Version: 0.0.8
Summary: Threshold-tuned ensemble classification for imbalanced binary data
Home-page: https://github.com/Naga270588/M-Tune
Author: Dr. Selvaraman Nagamani, Gori Sankar Borah, Hillul Chutia 
Author-email: nagamaniselvaraman@gmail.com, gorishankarbora45@gmail.com, hillulchutia@gmail.com
License: MIT
Project-URL: Source, https://github.com/Naga270588/M-Tune
Project-URL: Issues, https://github.com/Naga270588/M-Tune/issues
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9,<3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy==1.26.4
Requires-Dist: pandas==2.2.2
Requires-Dist: scikit-learn==1.5.1
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: project-url
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# M-Tune

M-Tune is a scikit-learn-compatible binary classification wrapper for
imbalanced data. It can train an ensemble by splitting the majority class,
pairing every split with the complete minority class, and fitting one cloned
base classifier per pair.

Each learner obtains the predicted minority-class probability for every row in
its training set, including rows whose true labels belong to both the majority
and minority classes. The mean of those probabilities becomes that learner's
decision threshold. Predictions from the ensemble are combined by hard
majority voting.

## Installation

```bash
pip install mtune
```

M-Tune requires Python 3.11 or later. The base classifier must implement
`fit` and `predict_proba`.

## Quick start

```python
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split

from mtune import Mtune

# Create an imbalanced binary classification data set.
X, y = make_classification(
    n_samples=2_000,
    n_features=20,
    n_informative=8,
    n_redundant=4,
    weights=[0.9, 0.1],
    random_state=42,
)
X = pd.DataFrame(X, columns=[f"feature_{i}" for i in range(X.shape[1])])
y = pd.Series(y, name="target")

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    stratify=y,
    random_state=42,
)

classifier = Mtune(
    base_model=LogisticRegression(max_iter=1_000),
    method="ensemble",
    n_ensemble=11,
    random_state=42,
)
classifier.fit(X_train, y_train)

y_pred = classifier.predict(X_test)
y_proba = classifier.predict_proba(X_test)

print(classification_report(y_test, y_pred))
print("Class order:", classifier.classes_)
print("First probability row:", y_proba[0])
```

## How the threshold is calculated

For every training observation, M-Tune selects one probability: the model's
predicted probability for the minority class. It then averages that probability
over all observations in the relevant training data set:

```text
threshold = sum(P(minority | x_i) for every training row x_i)
            / number of training rows
```

The average therefore contains values from observations whose true labels are
in the majority class as well as observations whose true labels are in the
minority class. It does **not** average the majority and minority columns of
`predict_proba`; those two columns sum to one, so averaging both columns would
always produce `0.5` in binary classification.

For example, if a learner produces the following minority-class probabilities:

```text
True majority rows: 0.02, 0.04, 0.06, 0.08
True minority rows: 0.30, 0.50
```

its threshold is `(0.02 + 0.04 + 0.06 + 0.08 + 0.30 + 0.50) / 6`, or
approximately `0.1667`.

`predict_proba` returns the arithmetic mean of the probability matrices from
the base learners. Its columns follow `classifier.classes_`, as they do for
scikit-learn classifiers. `predict` does not simply choose the largest value
from that matrix: it applies each learner's fitted threshold and then uses a
hard vote.

## How the ensemble works

For binary labels, M-Tune identifies the label with the most observations as
the majority class and the label with the fewest observations as the minority
class. In `method="ensemble"` mode it then:

1. Splits the majority-class rows into `n_ensemble` subsets.
2. Combines each subset with all minority-class rows.
3. Clones and fits the base classifier on each combined data set.
4. Selects the predicted minority-class probability for every row in that
   learner's training data, from both true classes, and uses the overall mean
   as the learner's threshold.
5. Converts each learner's probabilities into labels using its threshold.
6. Returns the label receiving the most votes. A tied vote is resolved in
   favor of the majority class.

The original majority rows are partitioned across learners, while the same
minority rows are reused by every learner.

## Direct mode

Direct mode trains one clone of the base classifier on the complete training
set and replaces its default decision rule with the mean predicted
minority-class probability across every training row from both true classes:

```python
classifier = Mtune(
    base_model=LogisticRegression(max_iter=1_000),
    method="direct",
)
classifier.fit(X_train, y_train)
y_pred = classifier.predict(X_test)

print("Learned threshold:", classifier.threshold)
```

## Parameters

| Parameter | Description |
| --- | --- |
| `base_model` | A scikit-learn-style probabilistic binary classifier. It is cloned before fitting. |
| `method` | `"ensemble"` (default) or `"direct"`. |
| `n_ensemble` | Number of majority-class splits and base learners in ensemble mode. Default: `11`. |
| `random_state` | Seed passed to pandas when each paired training set is shuffled. |

The random state of the base model is controlled separately. Set it on the
base model itself when reproducible fitting is required.

## Fitted attributes

| Attribute | Description |
| --- | --- |
| `classes_` | Sorted class labels. |
| `majority_class` | Label occurring most often in the fitted target. |
| `minority_class` | Label occurring least often in the fitted target. |
| `models_` | Fitted cloned classifier or classifiers. |
| `threshold` | Learned threshold in direct mode. |
| `thresholds_` | One learned threshold per learner in ensemble mode. |

## Current input requirements and limitations

- Classification must be binary.
- `X` must currently be a pandas `DataFrame`; NumPy feature arrays are not
  supported by version 0.0.8.
- The indexes of `X` and `y` must refer to the same rows because pandas aligns
  them while constructing the paired data sets.
- The target name must not duplicate a feature column name.
- `n_ensemble` must not exceed the number of majority-class training rows;
  otherwise an empty majority split can produce a one-class training set.
- The mean thresholds are estimated on the same samples used for fitting the
  learners. Evaluate the method on held-out data or with cross-validation.
- Base estimators without `predict_proba`, such as an uncalibrated `LinearSVC`,
  cannot be used directly.

## SVM usage and hyperparameter tuning

When using a support-vector classifier, enable probability estimation because
M-Tune requires `predict_proba`:

```python
from sklearn.svm import SVC
from mtune import Mtune

classifier = Mtune(
    base_model=SVC(probability=True),
    method="ensemble",
    n_ensemble=11,
    random_state=42,
)
classifier.fit(X_train, y_train)

y_pred = classifier.predict(X_test)
print("Learner thresholds:", classifier.thresholds_)
```

Because M-Tune follows scikit-learn's nested-estimator parameter convention,
base-model parameters are prefixed with `base_model__` during a grid search.
Parameters belonging to M-Tune itself, such as `n_ensemble`, do not use that
prefix:

```python
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.metrics import classification_report
from sklearn.svm import SVC

from mtune import Mtune

classifier = Mtune(
    base_model=SVC(probability=True),
    method="ensemble",
    random_state=42,
)

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

param_grid = {
    "n_ensemble": [3, 5, 7, 9],
    "base_model__C": [0.1, 1, 10],
    "base_model__kernel": ["linear", "rbf"],
    "base_model__gamma": ["scale", "auto"],
}

search = GridSearchCV(
    estimator=classifier,
    param_grid=param_grid,
    scoring="balanced_accuracy",
    cv=cv,
    n_jobs=-1,
)
search.fit(X_train, y_train)

best_model = search.best_estimator_
print("Best parameters:", search.best_params_)
print(classification_report(y_test, best_model.predict(X_test)))
```

The ensemble sizes in the grid are odd so that hard voting cannot end in a
tie. Grid search can be computationally expensive because every candidate and
cross-validation fold trains multiple base classifiers.

### Saving and applying the fitted ensemble

The complete fitted M-Tune estimator can be persisted with `joblib`:

```python
import joblib

joblib.dump(best_model, "mtune_svm.joblib")
loaded_model = joblib.load("mtune_svm.joblib")

# X_external must contain the same feature columns used during training.
external_labels = loaded_model.predict(X_external)
external_probabilities = loaded_model.predict_proba(X_external)
```

For an ensemble, `external_probabilities` contains the mean probability matrix
across the base learners. The final labels still come from thresholding each
learner separately and applying hard voting.

## Using another base classifier

Any cloneable probabilistic binary classifier can be supplied. For example:

```python
from sklearn.ensemble import RandomForestClassifier
from mtune import Mtune

classifier = Mtune(
    base_model=RandomForestClassifier(
        n_estimators=200,
        random_state=42,
        n_jobs=-1,
    ),
    n_ensemble=7,
    random_state=42,
)
classifier.fit(X_train, y_train)
```

## Project links

- [Source repository](https://github.com/Naga270588/M-Tune)
- [PyPI package](https://pypi.org/project/mtune/)

## License

M-Tune is distributed under the MIT License. See [LICENSE](LICENSE) for
details.

## Running the functional tests

The test suite uses Python's standard library and does not require a separate
test runner. From the directory containing `setup.py`, run:

```bash
python -m unittest discover -s tests -v
```

The same tests can also be discovered by pytest when pytest is installed:

```bash
python -m pytest
```
