PyPU
Positive-unlabeled (PU) learning in Python, with a scikit-learn style API.
Status: alpha. Six models, PU data generation, evaluation and class-prior estimation are available; the API may still change before 1.0. More methods are planned: see the roadmap.
In PU learning the training data hold some examples labeled positive and many unlabeled examples, which are a mix of positives and negatives. No example is labeled negative. This happens whenever only one class gets recorded: confirmed frauds, known disease genes, reported misstatements, clicked items.
PyPU collects PU learning methods behind one interface, in the spirit of
PyOD for outlier detection: import a model, fit it on
PU data, and predict on new data.
Installation
pip install pypu-learn
The package is imported as pypu and needs Python 3.11 or newer. (The PyPI name pypu
belongs to an unrelated project, hence pypu-learn.)
On macOS, the LightGBM-based models need the OpenMP runtime, which LightGBM's wheels do not
include: brew install libomp.
To try features that are not released yet, install from GitHub:
pip install git+https://github.com/izavits/PyPU.git
Quick start
import numpy as np
from sklearn.datasets import make_classification
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from pypu.models import ElkanNoto
# A fully labeled dataset...
X, y = make_classification(n_samples=2000, weights=[0.8], random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
# ...turned into PU data: label half of the training positives, leave the rest unlabeled.
rng = np.random.default_rng(0)
s_train = ((y_train == 1) & (rng.random(len(y_train)) < 0.5)).astype(int)
clf = ElkanNoto(random_state=0)
clf.fit(X_train, s_train) # s = 1: labeled positive, s = 0: unlabeled
scores = clf.decision_function(X_test) # higher means more likely positive
print("ROC AUC:", roc_auc_score(y_test, scores))
print("Accuracy:", (clf.predict(X_test) == y_test).mean())
PU data from labeled datasets
pypu.datasets turns a fully labeled binary dataset into PU data, so methods can be compared
against the true labels. make_pu_split does it in one call: a stratified train/test split,
then PU labels on the training part.
from sklearn.datasets import make_classification
from sklearn.metrics import roc_auc_score
from pypu.datasets import make_pu_split, pu_summary
from pypu.models import NaivePU
X, y = make_classification(n_samples=2000, weights=[0.8], random_state=0)
X_train, X_test, s_train, y_train, y_test = make_pu_split(
X, y, label_frequency=0.3, mechanism="sar_knn", random_state=0
)
print(pu_summary(y_train, s_train)) # prior, label frequency, positives among the unlabeled
clf = NaivePU(prior=y_train.mean()).fit(X_train, s_train)
print("ROC AUC:", roc_auc_score(y_test, clf.predict_proba(X_test)[:, 1]))
The labeling mechanism decides which positives get labeled:
Function (mechanism=) |
Labeled positives |
|---|---|
label_scar ("scar") |
Chosen uniformly at random (SCAR) |
label_sar_knn ("sar_knn") |
Mostly near the negatives (bias="near") or far from them (bias="far"), by distance to the nearest negatives |
label_sar_propensity ("sar_propensity") |
Each labeled with a known logistic propensity on chosen features, which can be returned |
label_sar_cluster ("sar_cluster") |
Taken from only some k-means clusters of the positives |
label_sar_confidence ("sar_confidence") |
Mostly the ones a classifier recognises easily (bias="easy") or poorly (bias="hard") |
label_frequency is the fraction of positives that get labeled. To set the share of
positives among the unlabeled samples instead, convert it with
label_frequency_from_pollution. make_case_control makes data for the case-control
scenario, where the unlabeled set is a separate sample with the population's class prior.
Evaluation
pypu.metrics has two kinds of metrics.
- With the true labels of a test set:
r_precision,precision_at_k,recall_at_k,f1_at_prior(F1 with the toppriorfraction predicted positive) andmax_f1_score. They rank the scores, so they suit PU methods whose scores are not calibrated probabilities. scikit-learn's ROC AUC and average precision apply as they are. - From PU labels alone, for model selection when no true labels exist:
lee_liu_scoreneeds nothing else, whilepu_recall_score,pu_precision_score,pu_f1_scoreandpu_roc_auc_scoreestimate the usual metrics given the class prior. They assume SCAR labels and work as scikit-learn scorers:
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import make_scorer
from sklearn.model_selection import GridSearchCV
from pypu.datasets import make_pu_split
from pypu.metrics import pu_roc_auc_score, r_precision
from pypu.models import NaivePU
X, y = make_classification(n_samples=2000, weights=[0.8], random_state=0)
X_train, X_test, s_train, y_train, y_test = make_pu_split(X, y, label_frequency=0.3, random_state=0)
prior = y_train.mean() # in practice, an estimate
search = GridSearchCV(
NaivePU(prior=prior),
{"estimator": [LogisticRegression(C=C) for C in (0.01, 0.1, 1.0)]},
scoring=make_scorer(pu_roc_auc_score, response_method="predict_proba", prior=prior),
).fit(X_train, s_train) # only the PU labels are used
print("R-precision:", r_precision(y_test, search.predict_proba(X_test)[:, 1]))
The interface
Every model derives from pypu.base.BasePUClassifier and is a scikit-learn estimator, so it
works with clone, Pipeline and GridSearchCV.
| Method | Returns |
|---|---|
fit(X, s) |
The fitted model. s[i] = 1 marks a labeled positive, s[i] = 0 an unlabeled sample. |
predict_proba(X) |
An (n_samples, 2) array; column 1 is the positive-class probability. |
decision_function(X) |
One score per sample; higher means more likely positive. |
predict(X) |
Labels in {0, 1}, thresholding the positive-class probability at threshold_. |
Models that need the class prior π = P(y = 1) take it as a prior parameter.
Estimating the class prior
When the prior is unknown, estimate it from the PU training data with pypu.prior, assuming
SCAR labels:
from pypu.models import IFPU
from pypu.prior import BBEPrior
estimator = BBEPrior().fit(X_train, s_train)
clf = IFPU(prior=estimator.prior_).fit(X_train, s_train)
BBEPrior: Best Bin Estimation (Garg et al., NeurIPS 2021). It needs a region where (almost) all samples are positive, and copes with overlapping classes.ElkanNotoPrior: the estimators of Elkan and Noto (KDD 2008),method="e1","e2"or"e3". They are accurate when the classes barely overlap and overestimate the prior when they overlap more.
Both also estimate the label frequency (label_frequency_) and the share of positives among
the unlabeled samples (pollution_). Estimates are biased upwards when the classifier
cannot isolate a purely positive region, and noisy with few labeled positives; a more
flexible classifier (estimator=) can help BBE on non-linear data.
Methods
| Model | Description | Status |
|---|---|---|
NaivePU |
Treats unlabeled samples as negatives; the usual lower bound | available |
IFPU |
LightGBM with a focal-loss objective that treats unlabeled samples as positive with probability prior |
available |
IFPNU |
An anomaly detector picks reliable negatives, then LightGBM with a focal loss that weights positives, reliable negatives and the remaining unlabeled samples | available |
SAREM |
Bekker et al.: EM that learns the classifier together with the propensity, the probability that a positive gets labeled (SAR) | available |
ElkanNoto |
Elkan and Noto: a labeled-vs-unlabeled classifier corrected by the label frequency, or retrained on weighted unlabeled samples | available |
PUBagging |
Mordelet and Vert: classifiers trained on the labeled positives against bootstrap samples of the unlabeled ones | available |
PUHellingerTree, PUHellingerForest |
Hellinger-distance decision trees and forests for PU data | planned (0.2.0) |
NNPU, LBE, PUe, ALMPU |
Neural PU methods (optional PyTorch dependency) | planned (0.3.0) |
IFPU's defaults are the settings of the original iFPU experiments (10 trees, learning rate
0.1, focal gamma=3), and it reproduces their test scores exactly:
from pypu.models import IFPU
clf = IFPU(prior=0.2).fit(X_train, s_train) # prior: fraction of positives in the training data
IFPNU's defaults are those of the original iFPNU experiments (100 trees), and from the
same anomaly scores it reproduces the original code exactly. Its first step uses
scikit-learn's IsolationForest unless given another detector, such as any PyOD detector:
from pypu.models import IFPNU
clf = IFPNU(prior=0.2, random_state=0).fit(X_train, s_train)
clf.pnu_labels_ # 1: labeled positive, 0: reliable negative, -1: left unlabeled
SAREM needs no prior. It works best when the propensity is restricted to the features
that drive labeling:
from pypu.models import SAREM
clf = SAREM(propensity_features=[0, 3]).fit(X_train, s_train)
clf.predict_propensity(X_test) # P(labeled | positive, x)
ElkanNoto estimates the label frequency itself, or takes the prior. PUBagging builds
the same bags as the BaggingPuClassifier of earlier experiments, and its out-of-bag
probabilities score the unlabeled training samples:
from sklearn.svm import SVC
from pypu.models import ElkanNoto, PUBagging
clf = ElkanNoto(weighted=True).fit(X_train, s_train)
bagging = PUBagging(SVC(kernel="linear"), oob_proba=True).fit(X_train, s_train)
unlabeled_scores = bagging.oob_proba_[s_train == 0, 1]
Contributing
Contributions are welcome. See CONTRIBUTING.md for the development setup, how to add a model, and the release process. Changes are listed in CHANGELOG.md.
Citing
If you use PyPU in your research, please cite it; GitHub's "Cite this repository" button uses CITATION.cff.
License
PyPU is distributed under the BSD 3-Clause License.
Metadata
Release files for pypu-learn 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pypu_learn-0.1.0.tar.gz | 242.1 kB | With provenance | Details for pypu_learn-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pypu_learn-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for pypu_learn-0.1.0-py3-none-any.whl |
Total release size: 301.9 kB
Release files / pypu_learn-0.1.0.tar.gz
| Download URL | pypu_learn-0.1.0.tar.gz |
|---|---|
| Size | 242.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6264527f9fdc6d8f35ee1ebfc7c455c71ae0ffb569ebdf982ae0d112d634cc41
|
|
BLAKE2b-256 checksum How to use checksums |
5c3238722f8464d979576a7e5ec2b1082ba80715186de2d0bfd6a75ab5967190
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logRelease files / pypu_learn-0.1.0-py3-none-any.whl
| Download URL | pypu_learn-0.1.0-py3-none-any.whl |
|---|---|
| Size | 59.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c52ac3aaa96e253eb16fa1791131bb1f15fe22380180d1cf3c9b8b4c6d954ded
|
|
BLAKE2b-256 checksum How to use checksums |
9bbc1f99b7a247216a8082a4a7bcbfc74a1a101b85c7750eec7c2a462fa36082
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logSecurity for pypu-learn 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for pypu_learn-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.