pypu-learn 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pypu/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """PyPU: positive-unlabeled (PU) learning in Python.
2
+
3
+ PU learning trains a binary classifier from positive examples and unlabeled examples
4
+ (a mix of positives and negatives), without any example labeled negative. PyPU collects
5
+ PU learning methods behind one scikit-learn style interface: every model is fitted with
6
+ ``fit(X, s)``, where ``s[i] == 1`` marks a labeled positive and ``s[i] == 0`` an unlabeled
7
+ sample.
8
+
9
+ Subpackages
10
+ -----------
11
+ models
12
+ The PU classifiers, e.g. ``from pypu.models import NaivePU``.
13
+ utils
14
+ Input validation and helpers shared by the models.
15
+ """
16
+
17
+ from pypu.utils import show_versions
18
+
19
+ try:
20
+ from pypu._version import __version__
21
+ except ImportError: # pragma: no cover - only when running from a source tree without installing
22
+ __version__ = "0.0.0+unknown"
23
+
24
+ __all__ = ["__version__", "show_versions"]
pypu/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.0.1'
22
+ __version_tuple__ = version_tuple = (0, 0, 1)
23
+
24
+ __commit_id__ = commit_id = None
pypu/base.py ADDED
@@ -0,0 +1,184 @@
1
+ """Base class shared by every PU classifier in PyPU."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABCMeta, abstractmethod
6
+
7
+ import numpy as np
8
+ from scipy.special import logit
9
+ from sklearn.base import BaseEstimator, ClassifierMixin
10
+ from sklearn.utils.validation import check_consistent_length, check_is_fitted, validate_data
11
+
12
+ from pypu.utils.validation import check_pu_labels
13
+
14
+ # Probabilities are clipped to [_EPS, 1 - _EPS] before taking log-odds.
15
+ _EPS = 1e-12
16
+
17
+
18
+ class BasePUClassifier(ClassifierMixin, BaseEstimator, metaclass=ABCMeta):
19
+ """Base class for positive-unlabeled (PU) classifiers.
20
+
21
+ Every PU classifier in PyPU follows the scikit-learn estimator API, so it works with
22
+ :func:`~sklearn.base.clone`, :class:`~sklearn.pipeline.Pipeline` and the model selection
23
+ tools:
24
+
25
+ - ``fit(X, s)`` learns from features ``X`` and PU labels ``s``, where ``s[i] == 1``
26
+ marks a labeled positive and ``s[i] == 0`` an unlabeled sample (positive or negative).
27
+ - ``predict_proba(X)`` returns an array of shape ``(n_samples, 2)`` whose second column
28
+ is the estimated probability of the positive class.
29
+ - ``decision_function(X)`` returns one score per sample: higher means more likely
30
+ positive. By default it is the log-odds of the positive-class probability, so ``0``
31
+ corresponds to a probability of ``0.5``.
32
+ - ``predict(X)`` returns hard labels in ``{0, 1}``: a sample is predicted positive when
33
+ its positive-class probability is at least :attr:`threshold_`.
34
+
35
+ ``score(X, y)`` (from :class:`~sklearn.base.ClassifierMixin`) computes the accuracy
36
+ against *true* labels ``y``, not against PU labels.
37
+
38
+ Subclasses implement :meth:`_fit` and :meth:`_predict_proba`, and may override
39
+ :meth:`_decision_function` (to return raw scores) and :meth:`_compute_threshold` (to
40
+ change the decision threshold). Input validation and the fitted attributes listed below
41
+ are handled by this class.
42
+
43
+ Attributes
44
+ ----------
45
+ classes_ : ndarray of shape (2,)
46
+ The class labels, always ``[0, 1]``.
47
+ n_features_in_ : int
48
+ Number of features seen during :meth:`fit`.
49
+ feature_names_in_ : ndarray of shape (n_features_in_,)
50
+ Names of features seen during :meth:`fit`. Defined only when ``X`` has feature
51
+ names that are all strings, e.g. a pandas DataFrame.
52
+ threshold_ : float
53
+ Positive-class probability threshold used by :meth:`predict`.
54
+ """
55
+
56
+ def fit(self, X, s):
57
+ """Fit the model to PU data.
58
+
59
+ Parameters
60
+ ----------
61
+ X : array-like of shape (n_samples, n_features)
62
+ Training samples.
63
+ s : array-like of shape (n_samples,)
64
+ PU labels: ``1`` for a labeled positive, ``0`` for an unlabeled sample.
65
+
66
+ Returns
67
+ -------
68
+ self : object
69
+ The fitted estimator.
70
+ """
71
+ X = self._validate_X(X, reset=True)
72
+ s = check_pu_labels(s)
73
+ check_consistent_length(X, s)
74
+ self._fit(X, s)
75
+ self.classes_ = np.array([0, 1])
76
+ self.threshold_ = float(self._compute_threshold(X, s))
77
+ return self
78
+
79
+ def predict_proba(self, X):
80
+ """Estimate class probabilities.
81
+
82
+ Parameters
83
+ ----------
84
+ X : array-like of shape (n_samples, n_features)
85
+ Samples.
86
+
87
+ Returns
88
+ -------
89
+ proba : ndarray of shape (n_samples, 2)
90
+ Probability of the negative class (column 0) and of the positive class (column 1).
91
+ """
92
+ check_is_fitted(self)
93
+ X = self._validate_X(X, reset=False)
94
+ proba = np.asarray(self._predict_proba(X), dtype=np.float64).reshape(-1)
95
+ return np.column_stack([1.0 - proba, proba])
96
+
97
+ def decision_function(self, X):
98
+ """Score samples; higher means more likely positive.
99
+
100
+ Parameters
101
+ ----------
102
+ X : array-like of shape (n_samples, n_features)
103
+ Samples.
104
+
105
+ Returns
106
+ -------
107
+ scores : ndarray of shape (n_samples,)
108
+ One score per sample.
109
+ """
110
+ check_is_fitted(self)
111
+ X = self._validate_X(X, reset=False)
112
+ return np.asarray(self._decision_function(X), dtype=np.float64).reshape(-1)
113
+
114
+ def predict(self, X):
115
+ """Predict class labels.
116
+
117
+ Parameters
118
+ ----------
119
+ X : array-like of shape (n_samples, n_features)
120
+ Samples.
121
+
122
+ Returns
123
+ -------
124
+ y_pred : ndarray of shape (n_samples,)
125
+ ``1`` for samples whose positive-class probability is at least
126
+ :attr:`threshold_`, ``0`` otherwise.
127
+ """
128
+ proba = self.predict_proba(X)[:, 1]
129
+ return self.classes_[(proba >= self.threshold_).astype(np.int64)]
130
+
131
+ # ------------------------------------------------------------------ hooks for subclasses
132
+
133
+ @abstractmethod
134
+ def _fit(self, X, s):
135
+ """Fit the model.
136
+
137
+ Parameters
138
+ ----------
139
+ X : ndarray of shape (n_samples, n_features)
140
+ Validated training samples (float64 or float32).
141
+ s : ndarray of shape (n_samples,)
142
+ Validated PU labels (int64, 1 = labeled positive, 0 = unlabeled).
143
+ """
144
+
145
+ @abstractmethod
146
+ def _predict_proba(self, X):
147
+ """Return the positive-class probability of each sample.
148
+
149
+ Parameters
150
+ ----------
151
+ X : ndarray of shape (n_samples, n_features)
152
+ Validated samples.
153
+
154
+ Returns
155
+ -------
156
+ proba : ndarray of shape (n_samples,)
157
+ Positive-class probabilities, in ``[0, 1]``.
158
+ """
159
+
160
+ def _decision_function(self, X):
161
+ """Return a score per sample; by default the log-odds of the positive class."""
162
+ proba = np.clip(self._predict_proba(X), _EPS, 1.0 - _EPS)
163
+ return logit(proba)
164
+
165
+ def _compute_threshold(self, X, s):
166
+ """Return the probability threshold used by :meth:`predict`; ``0.5`` by default.
167
+
168
+ Called at the end of :meth:`fit`, after :meth:`_fit`, with the validated training data.
169
+ """
170
+ return 0.5
171
+
172
+ def _validate_X(self, X, *, reset):
173
+ """Validate ``X``; ``reset=True`` in ``fit``, ``False`` when predicting."""
174
+ return validate_data(self, X, reset=reset, dtype=[np.float64, np.float32])
175
+
176
+ # ------------------------------------------------------------------ scikit-learn protocol
177
+
178
+ def __sklearn_is_fitted__(self):
179
+ return hasattr(self, "threshold_")
180
+
181
+ def __sklearn_tags__(self):
182
+ tags = super().__sklearn_tags__()
183
+ tags.classifier_tags.multi_class = False
184
+ return tags
@@ -0,0 +1,42 @@
1
+ """PU learning models.
2
+
3
+ Every model follows the interface of :class:`pypu.base.BasePUClassifier`::
4
+
5
+ from pypu.models import NaivePU
6
+
7
+ clf = NaivePU().fit(X_train, s_train) # s: 1 = labeled positive, 0 = unlabeled
8
+ proba = clf.predict_proba(X_test)[:, 1]
9
+
10
+ Models are imported lazily: importing :mod:`pypu.models` does not import the optional
11
+ dependencies (such as PyTorch) of models that are not used.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from importlib import import_module
17
+ from typing import TYPE_CHECKING
18
+
19
+ # Public model name -> module that defines it. Register every new model here, in __all__ and
20
+ # in the TYPE_CHECKING block below (tests check that the three agree).
21
+ _MODELS: dict[str, str] = {
22
+ "NaivePU": "pypu.models.naive",
23
+ }
24
+
25
+ __all__ = ["NaivePU"]
26
+
27
+ if TYPE_CHECKING:
28
+ from pypu.models.naive import NaivePU
29
+
30
+
31
+ def __getattr__(name: str):
32
+ try:
33
+ module_name = _MODELS[name]
34
+ except KeyError:
35
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
36
+ model = getattr(import_module(module_name), name)
37
+ globals()[name] = model # later lookups skip __getattr__
38
+ return model
39
+
40
+
41
+ def __dir__():
42
+ return sorted(set(globals()) | set(_MODELS))
pypu/models/naive.py ADDED
@@ -0,0 +1,89 @@
1
+ """Naive baseline that treats every unlabeled sample as negative."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ from sklearn.base import clone
7
+ from sklearn.linear_model import LogisticRegression
8
+
9
+ from pypu.base import BasePUClassifier
10
+ from pypu.utils.validation import check_prior, threshold_from_prior
11
+
12
+
13
+ class NaivePU(BasePUClassifier):
14
+ r"""Naive PU baseline: a classifier trained with unlabeled samples as negatives.
15
+
16
+ The wrapped classifier learns to separate labeled positives (``s == 1``) from unlabeled
17
+ samples (``s == 0``), so it estimates :math:`P(s = 1 \mid x)` rather than
18
+ :math:`P(y = 1 \mid x)`. Under the SCAR assumption (labeled positives are selected
19
+ completely at random) the two differ by the constant factor
20
+ :math:`c = P(s = 1 \mid y = 1)` [1]_: the ranking of samples, and therefore ROC AUC, is
21
+ preserved, but the probabilities are too low and a ``0.5`` threshold under-predicts
22
+ positives. Passing the class ``prior`` sets the threshold so that a ``prior`` fraction of
23
+ the training samples is predicted positive.
24
+
25
+ It is the usual lower bound when comparing PU learning methods.
26
+
27
+ Parameters
28
+ ----------
29
+ estimator : classifier, default=None
30
+ A scikit-learn classifier implementing ``predict_proba``. It is cloned before
31
+ fitting. ``None`` uses :class:`~sklearn.linear_model.LogisticRegression`.
32
+ prior : float, default=None
33
+ Class prior :math:`\pi = P(y = 1)`. If given, :attr:`threshold_` is set so that a
34
+ ``prior`` fraction of the training samples is predicted positive. Otherwise the
35
+ threshold is ``0.5``.
36
+
37
+ Attributes
38
+ ----------
39
+ estimator_ : classifier
40
+ The fitted classifier.
41
+ classes_ : ndarray of shape (2,)
42
+ The class labels, always ``[0, 1]``.
43
+ n_features_in_ : int
44
+ Number of features seen during ``fit``.
45
+ feature_names_in_ : ndarray of shape (n_features_in_,)
46
+ Names of features seen during ``fit``, when ``X`` has string feature names.
47
+ threshold_ : float
48
+ Positive-class probability threshold used by ``predict``.
49
+
50
+ References
51
+ ----------
52
+ .. [1] C. Elkan and K. Noto. "Learning classifiers from only positive and unlabeled
53
+ data." KDD 2008.
54
+
55
+ Examples
56
+ --------
57
+ >>> import numpy as np
58
+ >>> from sklearn.datasets import make_classification
59
+ >>> from pypu.models import NaivePU
60
+ >>> X, y = make_classification(n_samples=500, weights=[0.7], random_state=0)
61
+ >>> rng = np.random.default_rng(0)
62
+ >>> s = ((y == 1) & (rng.random(len(y)) < 0.5)).astype(int) # label half the positives
63
+ >>> clf = NaivePU(prior=y.mean()).fit(X, s)
64
+ >>> clf.predict_proba(X).shape
65
+ (500, 2)
66
+ """
67
+
68
+ def __init__(self, estimator=None, prior=None):
69
+ self.estimator = estimator
70
+ self.prior = prior
71
+
72
+ def _fit(self, X, s):
73
+ if self.prior is not None:
74
+ check_prior(self.prior, s)
75
+ estimator = LogisticRegression() if self.estimator is None else clone(self.estimator)
76
+ if not hasattr(estimator, "predict_proba"):
77
+ raise TypeError(
78
+ f"estimator must implement predict_proba; {type(estimator).__name__} does not."
79
+ )
80
+ self.estimator_ = estimator.fit(X, s)
81
+
82
+ def _predict_proba(self, X):
83
+ positive_column = np.flatnonzero(self.estimator_.classes_ == 1)[0]
84
+ return self.estimator_.predict_proba(X)[:, positive_column]
85
+
86
+ def _compute_threshold(self, X, s):
87
+ if self.prior is None:
88
+ return 0.5
89
+ return threshold_from_prior(self._predict_proba(X), self.prior)
pypu/utils/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Utilities shared by the PyPU models."""
2
+
3
+ from pypu.utils._show_versions import show_versions
4
+ from pypu.utils.validation import check_prior, check_pu_labels, threshold_from_prior
5
+
6
+ __all__ = ["check_prior", "check_pu_labels", "show_versions", "threshold_from_prior"]
@@ -0,0 +1,27 @@
1
+ """Print the versions of PyPU and its dependencies, for bug reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ import sys
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+ # Distributions reported by show_versions: core dependencies first, then optional ones.
10
+ _DEPENDENCIES = ("numpy", "scipy", "scikit-learn", "lightgbm", "xgboost", "torch", "pyod")
11
+
12
+
13
+ def show_versions() -> None:
14
+ """Print the versions of PyPU, Python and the main dependencies.
15
+
16
+ Include this output when reporting a bug.
17
+ """
18
+ import pypu
19
+
20
+ print(f"pypu: {pypu.__version__}")
21
+ print(f"python: {sys.version.split()[0]} ({platform.platform()})")
22
+ for dist in _DEPENDENCIES:
23
+ try:
24
+ dist_version = version(dist)
25
+ except PackageNotFoundError:
26
+ dist_version = "not installed"
27
+ print(f"{dist}: {dist_version}")
@@ -0,0 +1,120 @@
1
+ """Validation of PU labels and class priors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numbers
6
+ import warnings
7
+
8
+ import numpy as np
9
+ from sklearn.utils.multiclass import type_of_target
10
+ from sklearn.utils.validation import column_or_1d
11
+
12
+ _PU_LABELS_HINT = "PU labels must be 1 (labeled positive) or 0 (unlabeled)"
13
+
14
+
15
+ def check_pu_labels(s) -> np.ndarray:
16
+ """Validate PU labels.
17
+
18
+ Parameters
19
+ ----------
20
+ s : array-like of shape (n_samples,)
21
+ PU labels: ``1`` for a labeled positive, ``0`` for an unlabeled sample.
22
+ Booleans are accepted (``True`` means labeled positive).
23
+
24
+ Returns
25
+ -------
26
+ s : ndarray of shape (n_samples,), dtype int64
27
+ The validated labels.
28
+
29
+ Raises
30
+ ------
31
+ ValueError
32
+ If the labels are not 0/1, or if there is no labeled positive or no unlabeled sample.
33
+ """
34
+ s = column_or_1d(s, warn=True)
35
+ if s.size == 0:
36
+ raise ValueError("PU labels are empty.")
37
+ # Same error wording as scikit-learn for non-binary targets.
38
+ target_type = type_of_target(s, input_name="s", raise_unknown=True)
39
+ if target_type == "continuous":
40
+ raise ValueError(f"Unknown label type: continuous. {_PU_LABELS_HINT}.")
41
+
42
+ values = np.unique(s)
43
+ if target_type == "multiclass":
44
+ raise ValueError(
45
+ f"Only binary classification is supported. {_PU_LABELS_HINT}, got values "
46
+ f"{values[:10].tolist()}."
47
+ )
48
+ if s.dtype.kind not in "biuf" or not np.isin(values, (0, 1)).all():
49
+ raise ValueError(
50
+ f"{_PU_LABELS_HINT}, got values {values[:10].tolist()}. If unlabeled samples are "
51
+ "encoded as -1, map them to 0."
52
+ )
53
+ if values.size < 2:
54
+ missing = "labeled positive (s == 1)" if values[0] == 0 else "unlabeled sample (s == 0)"
55
+ raise ValueError(f"PU labels contain only one class: there is no {missing}.")
56
+ return s.astype(np.int64)
57
+
58
+
59
+ def check_prior(prior, s=None) -> float:
60
+ r"""Validate a class prior :math:`\pi = P(y = 1)`.
61
+
62
+ Parameters
63
+ ----------
64
+ prior : float
65
+ The class prior, in the open interval ``(0, 1)``.
66
+ s : array-like of shape (n_samples,), default=None
67
+ Validated PU labels. When given, a warning is raised if ``prior`` is lower than the
68
+ fraction of labeled positives, which is impossible when the labeled positives are
69
+ drawn from the same sample as the unlabeled data.
70
+
71
+ Returns
72
+ -------
73
+ prior : float
74
+ The validated prior.
75
+
76
+ Raises
77
+ ------
78
+ TypeError
79
+ If ``prior`` is not a real number.
80
+ ValueError
81
+ If ``prior`` is not in ``(0, 1)``.
82
+ """
83
+ if isinstance(prior, bool) or not isinstance(prior, numbers.Real):
84
+ raise TypeError(f"prior must be a float in (0, 1), got {prior!r}.")
85
+ prior = float(prior)
86
+ if not 0.0 < prior < 1.0:
87
+ raise ValueError(f"prior must be in the open interval (0, 1), got {prior}.")
88
+ if s is not None:
89
+ labeled_fraction = float(np.mean(s))
90
+ if prior < labeled_fraction:
91
+ warnings.warn(
92
+ f"prior={prior} is lower than the fraction of labeled positives "
93
+ f"({labeled_fraction:.4f}). When the labeled positives come from the same sample "
94
+ "as the unlabeled data the prior cannot be lower than that fraction: check that "
95
+ "you passed the class prior P(y=1) and not the label frequency P(s=1 | y=1).",
96
+ UserWarning,
97
+ stacklevel=2,
98
+ )
99
+ return prior
100
+
101
+
102
+ def threshold_from_prior(scores, prior: float) -> float:
103
+ """Return the threshold that labels the top ``prior`` fraction of ``scores`` positive.
104
+
105
+ Parameters
106
+ ----------
107
+ scores : array-like of shape (n_samples,)
108
+ Scores where higher means more likely positive (e.g. positive-class probabilities).
109
+ prior : float
110
+ Fraction of samples to label positive, in ``(0, 1)``.
111
+
112
+ Returns
113
+ -------
114
+ threshold : float
115
+ The ``1 - prior`` quantile of ``scores``; samples with ``score >= threshold``
116
+ are labeled positive.
117
+ """
118
+ prior = check_prior(prior)
119
+ scores = np.asarray(scores, dtype=np.float64).reshape(-1)
120
+ return float(np.quantile(scores, 1.0 - prior))
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: pypu-learn
3
+ Version: 0.0.1
4
+ Summary: Positive-unlabeled (PU) learning in Python, with a scikit-learn style API.
5
+ Project-URL: Homepage, https://github.com/izavits/PyPU
6
+ Project-URL: Repository, https://github.com/izavits/PyPU
7
+ Project-URL: Issues, https://github.com/izavits/PyPU/issues
8
+ Project-URL: Changelog, https://github.com/izavits/PyPU/blob/main/CHANGELOG.md
9
+ Author: Ilias Zavitsanos
10
+ License-Expression: BSD-3-Clause
11
+ License-File: LICENSE
12
+ Keywords: machine-learning,positive-unlabeled,pu-learning,scikit-learn,semi-supervised,weak-supervision
13
+ Classifier: Development Status :: 2 - Pre-Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: numpy>=1.26
26
+ Requires-Dist: scikit-learn>=1.6
27
+ Requires-Dist: scipy>=1.11
28
+ Description-Content-Type: text/markdown
29
+
30
+ # PyPU
31
+
32
+ **Positive-unlabeled (PU) learning in Python, with a scikit-learn style API.**
33
+
34
+ [![CI](https://github.com/izavits/PyPU/actions/workflows/ci.yml/badge.svg)](https://github.com/izavits/PyPU/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/pypu-learn.svg)](https://pypi.org/project/pypu-learn/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/pypu-learn.svg)](https://pypi.org/project/pypu-learn/)
37
+ [![License: BSD-3-Clause](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://github.com/izavits/PyPU/blob/main/LICENSE)
38
+
39
+ > **Status: pre-alpha.** The common interface is in place and the methods are being ported.
40
+ > See the [roadmap](https://github.com/izavits/PyPU/blob/main/ROADMAP.md).
41
+
42
+ In PU learning the training data hold some examples labeled positive and many unlabeled
43
+ examples, which are a mix of positives and negatives. No example is labeled negative. This
44
+ happens whenever only one class gets recorded: confirmed frauds, known disease genes, reported
45
+ misstatements, clicked items.
46
+
47
+ PyPU collects PU learning methods behind one interface, in the spirit of
48
+ [PyOD](https://github.com/yzhao062/pyod) for outlier detection: import a model, `fit` it on
49
+ PU data, and predict on new data.
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install pypu-learn
55
+ ```
56
+
57
+ The package is imported as `pypu` and needs Python 3.11 or newer. (The PyPI name `pypu`
58
+ belongs to an unrelated project, hence `pypu-learn`.)
59
+
60
+ Until the first release is on PyPI, install from GitHub:
61
+
62
+ ```bash
63
+ pip install git+https://github.com/izavits/PyPU.git
64
+ ```
65
+
66
+ ## Quick start
67
+
68
+ ```python
69
+ import numpy as np
70
+ from sklearn.datasets import make_classification
71
+ from sklearn.metrics import roc_auc_score
72
+ from sklearn.model_selection import train_test_split
73
+
74
+ from pypu.models import NaivePU
75
+
76
+ # A fully labeled dataset...
77
+ X, y = make_classification(n_samples=2000, weights=[0.8], random_state=0)
78
+ X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
79
+
80
+ # ...turned into PU data: label half of the training positives, leave the rest unlabeled.
81
+ rng = np.random.default_rng(0)
82
+ s_train = ((y_train == 1) & (rng.random(len(y_train)) < 0.5)).astype(int)
83
+
84
+ clf = NaivePU(prior=y_train.mean())
85
+ clf.fit(X_train, s_train) # s = 1: labeled positive, s = 0: unlabeled
86
+
87
+ scores = clf.predict_proba(X_test)[:, 1]
88
+ print("ROC AUC:", roc_auc_score(y_test, scores))
89
+ ```
90
+
91
+ Data utilities that turn labeled datasets into PU data in one call are coming in 0.1.0.
92
+
93
+ ## The interface
94
+
95
+ Every model derives from `pypu.base.BasePUClassifier` and is a scikit-learn estimator, so it
96
+ works with `clone`, `Pipeline` and `GridSearchCV`.
97
+
98
+ | Method | Returns |
99
+ | --- | --- |
100
+ | `fit(X, s)` | The fitted model. `s[i] = 1` marks a labeled positive, `s[i] = 0` an unlabeled sample. |
101
+ | `predict_proba(X)` | An `(n_samples, 2)` array; column 1 is the positive-class probability. |
102
+ | `decision_function(X)` | One score per sample; higher means more likely positive. |
103
+ | `predict(X)` | Labels in `{0, 1}`, thresholding the positive-class probability at `threshold_`. |
104
+
105
+ Models that need the class prior π = P(y = 1) take it as a `prior` parameter.
106
+
107
+ ## Methods
108
+
109
+ | Model | Description | Status |
110
+ | --- | --- | --- |
111
+ | `NaivePU` | Treats unlabeled samples as negatives; the usual lower bound | available |
112
+ | `IFPU` | Focal-loss PU risk with gradient boosting | planned (0.1.0) |
113
+ | `IFPNU` | Anomaly detector picks reliable negatives, then a focal PNU risk with gradient boosting | planned (0.1.0) |
114
+ | `SAREM` | EM for labels selected at random (SAR) | planned (0.1.0) |
115
+ | `ElkanNoto`, `PUBagging` | Classic PU baselines | planned (0.1.0) |
116
+ | `PUHellingerTree`, `PUHellingerForest` | Hellinger-distance decision trees and forests for PU data | planned (0.2.0) |
117
+ | `NNPU`, `LBE`, `PUe`, `ALMPU` | Neural PU methods (optional PyTorch dependency) | planned (0.3.0) |
118
+
119
+ Also planned: creating PU data from labeled datasets (uniform SCAR selection, KNN-biased and
120
+ other SAR selection mechanisms), class-prior estimation, and PU-specific metrics.
121
+
122
+ ## Contributing
123
+
124
+ Contributions are welcome. See
125
+ [CONTRIBUTING.md](https://github.com/izavits/PyPU/blob/main/CONTRIBUTING.md) for the
126
+ development setup, how to add a model, and the release process. Changes are listed in
127
+ [CHANGELOG.md](https://github.com/izavits/PyPU/blob/main/CHANGELOG.md).
128
+
129
+ ## Citing
130
+
131
+ If you use PyPU in your research, please cite it; GitHub's "Cite this repository" button uses
132
+ [CITATION.cff](https://github.com/izavits/PyPU/blob/main/CITATION.cff).
133
+
134
+ ## License
135
+
136
+ PyPU is distributed under the
137
+ [BSD 3-Clause License](https://github.com/izavits/PyPU/blob/main/LICENSE).
@@ -0,0 +1,12 @@
1
+ pypu/__init__.py,sha256=jQpD4zxbfx6oNWpMpQwf_WBPLBkBLb9kMke1X33xGUA,839
2
+ pypu/_version.py,sha256=8OsTLsIVB9D0HdPTmt5rVwyVUBe9xTVGkRslXicxzkM,520
3
+ pypu/base.py,sha256=7m-uVcrY1MybqpmF_B55h64b-iZOiCZKHN_nS-mm8nk,6581
4
+ pypu/models/__init__.py,sha256=5BIn2dKwYsEXtaqfCJAjMCustsN29-iShQSZZyAnNsA,1221
5
+ pypu/models/naive.py,sha256=pckviwY6gzcGq_9vd5Yav68I63jhBX6_sVPr83aEvVQ,3607
6
+ pypu/utils/__init__.py,sha256=k89kme_ki-1m_74aef-xGNeEDBVWY-l7VSIG1WIVOmQ,268
7
+ pypu/utils/_show_versions.py,sha256=F2lLrp9oDNFw_ys7_yXy8S1TirSLc_Wc_gvY9Kx7mdk,865
8
+ pypu/utils/validation.py,sha256=k6Fb31s4Sqez2dJNg8Csy1cEowrGNkxNssekPr9cOL8,4225
9
+ pypu_learn-0.0.1.dist-info/METADATA,sha256=K4wm80yqIKkqz79FEQ9ZY8mZnGgjUKvOBdeNLkS8l6c,5897
10
+ pypu_learn-0.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
11
+ pypu_learn-0.0.1.dist-info/licenses/LICENSE,sha256=5QchMJ0UqJ3ORojl0D3c_skp3PlswPioxsTF3pEjV6g,1524
12
+ pypu_learn-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Ilias Zavitsanos
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.