Skip to content

feat: dPULearn.mine_negatives + get_labels + named sample colors (prototype #308)#321

Draft
breimanntools wants to merge 7 commits into
masterfrom
feat/dpulearn-mined-mask
Draft

feat: dPULearn.mine_negatives + get_labels + named sample colors (prototype #308)#321
breimanntools wants to merge 7 commits into
masterfrom
feat/dpulearn-mined-mask

Conversation

@breimanntools

@breimanntools breimanntools commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Status: SOLID

Works end to end, all new local tests green, full ripple done. Three additive
conveniences; every existing API stays byte-identical. Ready for maintainer review.

What to review

  • dPULearn.mine_negatives design: a thin wrapper that stacks X_pos over
    X_unlabelled, fits with 1/2 markers, and returns the boolean mask
    (labels_[len(X_pos):] == 0). Returns the mask only (not a subframe) — the issue
    said "mask and/or subframe"; X_unlabelled[mask] gives the subframe, so the mask is
    the minimal primitive. Open question: do you also want a return_df/subframe option?
  • get_labels lives in data_handling (re-exported top-level). Added an optional
    col_label="label" beyond the issue's (df, positive_label=1) signature for
    multi-column frames — drop it if you want the exact issue signature.
  • get_labels raises if positive_label is absent from the column (friendlier than the
    silent all-zeros of the manual expression). Flag if you'd rather match the manual
    expression exactly (no raise).
  • Color constants are aliases (COLOR_SAMPLES_POS = COLOR_POS, ...) in _constants.py;
    DICT_COLOR values are untouched, golden test asserts equality.

Part of #305 / prototype for #308. Intentionally has no issue-closing keyword; #308 stays open.


Deliverables (#308)

  1. dPULearn.mine_negatives(X_pos, X_unlabelled, n_neg=None, n_unl_to_neg=None, metric=None, n_components=0.80) → boolean mask over X_unlabelled rows flagging the
    mined reliable negatives. Equals the notebook cell 18/24 manual
    vstack + [1]*..+[2]*.. + labels_[len(X_pos):]==0 path exactly (regression-tested,
    incl. on the real DOM_GSEC_PU feature matrix in the example notebook: 49 of 631).
    The pre-existing fit(X, labels=...) path is untouched (byte-identical, regression-tested).
  2. aa.get_labels(df, positive_label=1, col_label="label") → binary int label vector;
    the single-call form of (df[col]==x).astype(int).to_numpy(). Golden-tested on PU / binary
    / multi-class / string encodings.
  3. aa.COLOR_SAMPLES_POS / NEG / UNL / REL_NEG — named constants equal to
    plot_get_cdict("DICT_COLOR")["SAMPLES_*"] (golden test).

Public API

Wired get_labels + the 4 color constants into __init__.py / __all__ (#308 is on the
wire-to-public-API list). mine_negatives is a method on the already-public dPULearn.

Ripple

  • numpydoc with named Returns + per-method Examples include
  • 2 executed example notebooks (examples/data_handling/get_labels.ipynb,
    examples/pu_learning/dpul_mine_negatives.ipynb) — pass nbmake
  • 39 unit tests (positive + negative per symbol; golden/regression; existing-fit no-change)
  • release notes Unreleased entries (Data Handling / PU Learning / Plotting)
  • cheat-sheet rows for the two functional symbols

Local verification

  • New tests: 39 passed
  • tests/unit/api_tests (param-coverage, abbreviation registry, barrel, ...): 175 passed
  • dpulearn + data_handling + plotting suites: 780 passed
  • docstring checker: 0 defects; nbmake on both notebooks: 2 passed

🤖 Generated with Claude Code


Critical self-review

Adversarial review-and-improve pass (issue #308). Verdict: implementation is
correct and the three additive surfaces behave as specified. One clarity fix
applied; the rest verified, not rubber-stamped.

Fixed

  • get_labels validation order. check_df(cols_required=col_label) ran
    before check_str(col_label), so a non-string col_label surfaced an
    internal 'cols_required' error rather than a clear 'col_label' one.
    Reordered so the parameter is validated before it is used as a required-column
    key. No behaviour change on valid input.

Verified correct (with a concrete check)

  • Mined-mask equivalence (KPI). mine_negatives is purely additive: it
    vstacks X_pos/X_unlabelled, builds the [1]*n_pos + [2]*n_unl vector,
    calls the untouched fit(label_pos=1, label_unl=2), and returns
    labels_[n_pos:] == 0. Golden tests assert byte-equality vs the manual
    stacking path across seeds {0,1,7} and the cosine metric, and that n_neg
    == n_unl_to_neg with no pre-labeled negatives.
  • fit unchanged. The diff to _dpulearn.py is strictly additive (one
    helper + one method); the existing fit(X, labels=...) path is not touched,
    so byte-identity is structural, and the equivalence tests pin the sugar to it.
  • Color constants. COLOR_SAMPLES_* are defined in _constants.py,
    re-exported via from ._constants import * in utils.py (so ut.COLOR_SAMPLES_*
    resolves), and a golden test asserts equality vs
    plot_get_cdict("DICT_COLOR")["SAMPLES_*"].
  • API additions. __all__ has no duplicates, every entry resolves, and the
    data_handling front-door docstring lists get_labels. Meta-tests pass:
    test_param_coverage, test_class_abbreviation_registry,
    test_backend_import_hygiene, test_docstring_contracts, test_utils_barrel.
  • Guides compliance. No print() in library code, bare ValueError only,
    constants via ut.X, # I / # II skeleton, numpydoc with named Returns,
    no ADR refs. check_docstrings.py reports 0 defects on both changed modules.
  • Notebooks. Both new example notebooks execute clean under --nbmake, ship
    executed outputs, and use display_df for DataFrames.

Notes for the maintainer (not changed — design calls, per spec)

  • mine_negatives name/shape. The method both mutates the instance (sets
    labels_/df_pu_) and returns a mask, so it is not a pure .fit
    (returns self) nor a pure query. The name reads well and the dual behaviour is
    documented, but it is a third public verb on a Wrapper whose template is
    .fit/.eval. Worth a conscious sign-off on the name and the mutate+return
    shape.
  • n_neg vs n_unl_to_neg redundancy in mine_negatives. With no
    pre-labeled-negative path exposed, the two arguments are always equivalent
    here (unlike fit, where label_neg distinguishes them). Both are kept for
    parity with fit; you may prefer to expose only one to reduce surface.

Residual concern I did not "fix"

  • The TestFitUnchanged regression asserts contract properties (positives stay
    1, exactly N mined 0, values ⊆ {0,1,2}) rather than a pinned golden
    vector. It does not, on its own, catch a future algorithm change to fit
    (the equivalence tests would move with it). I left it as-is: byte-identity vs
    master is already guaranteed by the additive diff, and pinning a random
    PCA-selection vector is brittle and belongs in the dedicated exact-value
    regression suite, not here.

Iterative review log

  • Round 2 (correctness & edge cases): mine_negatives validated X_pos / X_unlabelled separately with the default check_X min_n_samples=3, so it raised for n_pos<3 while the manual stacked path accepts it (the >=3 floor belongs to the stacked matrix, enforced by fit) — breaking the "equals the manual path exactly" contract. Relaxed per-matrix validation to min_n_samples=1 (float coercion + feature-dim check kept; stacked fit still enforces >=3). Added tests: n_pos=1 mask-equals-manual; get_labels single-class column maps to all-ones (no spurious raise), NaN maps to 0. (39 -> 42 new tests green; full dpulearn suite 95 green.)
  • Round 3 (numerical robustness & API consistency): the new public parameter was spelled X_unlabelled (British, two L), but the entire codebase uses American "unlabeled" (85 uses, plus the label_unl / n_unl_to_neg markers). Renamed the new/unreleased parameter, helper check_match_X_pos_X_unlabeled, docstrings, tests, cheat-sheet and release-notes entries to X_unlabeled for sibling consistency; re-executed the example notebook (49 of 631, mask-equals-manual: True). No numerical-robustness defects found (seeded determinism, feature-dim + count validation all correct). Maintainer note: an even terser X_unl would pair most tightly with X_pos/X_neg/label_unl, but I kept the full word per the issue author's explicit X_unlabelled naming.
  • Round 4 (efficiency & simplicity): resolved the round-1 design flag. In mine_negatives there are never pre-labeled negatives, so n_neg (total) and n_unl_to_neg (from-pool) were always mathematically equivalent — a confusing always-equivalent duplicate. Collapsed the two count arguments into a single required n_neg (the method is new/unreleased, so non-breaking); it calls fit(n_unl_to_neg=n_neg) internally. Updated docstring, tests (dropped the now-moot both/neither/equivalence cases, added an n_neg<1 negative test), and the example notebook (re-executed: 49 of 631, mask-equals-manual: True). The body-and-return shape (fits the instance and returns the mask) is intentional/documented and kept as-is. No loop/vectorization defects (already np.vstack + boolean-mask, no iterrows).
  • Round 5 (guides, docs & tests completeness): the round-4 delegation left a leaky error message — an invalid n_neg raised 'n_unl_to_neg' should be an integer with n >= 1 (naming an internal parameter mine_negatives users never see). Added an explicit ut.check_number_range(name="n_neg", ...) in the frontend so the message names n_neg, closing the "frontend validates every public param" gap; strengthened the negative test with match="n_neg". Verified: numpydoc named Returns + one-line summaries, Examples includes resolve (both notebooks exist and pass nbmake), no ADR refs in any new .py, bare ValueError only, no print, citations resolve. Full area gate green: 917 unit tests (dpulearn / data_handling / plotting / api meta-tests incl. param-coverage, import-hygiene, barrel), docstring checker 0 defects, doc/signature-drift 0 candidates, nbmake 2/2.

…totype #308)

Three additive conveniences for the positive/unlabelled -> mined-negatives flow,
removing the recurring vstack/label-vector/color-lookup plumbing in the
gamma-secretase use case. All existing APIs stay byte-identical.

- dPULearn.mine_negatives(X_pos, X_unlabelled, ...): one-call sugar over fit that
  returns the reliable-negative boolean mask over X_unlabelled. Equals the manual
  labels_[len(X_pos):]==0 result exactly (regression-tested). fit(X, labels=...)
  unchanged.
- get_labels(df, positive_label=1, col_label="label"): binary int label vector,
  the single-call form of (df[col]==x).astype(int).to_numpy().
- COLOR_SAMPLES_POS/NEG/UNL/REL_NEG: public named aliases for the canonical sample
  colors, equal to plot_get_cdict("DICT_COLOR")["SAMPLES_*"] (golden-tested).

Wired get_labels + the 4 color constants into __init__/__all__ (on the #308
wire-to-public-API list). Ripple: numpydoc + 2 executed example notebooks
(get_labels, dpul_mine_negatives), 39 unit tests (positive+negative+regression),
release-notes Unreleased entries, cheat-sheet rows.

Part of #305 / prototype for #308

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.94%. Comparing base (f1cfa72) to head (0262929).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##           master     #321   +/-   ##
=======================================
  Coverage   94.93%   94.94%           
=======================================
  Files         185      186    +1     
  Lines       17862    17897   +35     
  Branches     3032     3034    +2     
=======================================
+ Hits        16957    16992   +35     
  Misses        598      598           
  Partials      307      307           
Files with missing lines Coverage Δ
aaanalysis/__init__.py 97.29% <100.00%> (+0.03%) ⬆️
aaanalysis/_constants.py 100.00% <100.00%> (ø)
aaanalysis/data_handling/__init__.py 100.00% <100.00%> (ø)
aaanalysis/data_handling/_get_labels.py 100.00% <100.00%> (ø)
aaanalysis/pu_learning/_dpulearn.py 97.97% <100.00%> (+0.22%) ⬆️
Components Coverage Δ
cpp_core 94.95% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

breimanntools and others added 6 commits July 1, 2026 04:50
… in get_labels

Reorder the get_labels Validate block so check_str(col_label) runs before
check_df(cols_required=col_label). A non-str col_label now surfaces a clear
'col_label' error instead of an internal 'cols_required' one. No behaviour
change on valid input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mine_negatives validated X_pos and X_unlabelled separately with the default
check_X min_n_samples=3, so it rejected n_pos<3 inputs that the manual stacking
path accepts (the >=3 floor belongs to the stacked matrix, which fit enforces).
Relax the per-matrix check to min_n_samples=1 to restore exact equivalence; add
tests for the small-positive-set equivalence and get_labels single-class/NaN
mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sistency

The rest of the package spells it 'unlabeled' (American, 85 uses) and abbreviates
the marker as label_unl / n_unl_to_neg; the new public mine_negatives parameter
used the British two-L 'X_unlabelled'. Rename the new/unreleased parameter, its
match helper, docstrings, tests, cheat-sheet and release-notes entries, and
re-execute the example notebook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With no pre-labeled negatives, n_neg (total) and n_unl_to_neg (from the pool)
are always equivalent in mine_negatives, so exposing both was redundant. Replace
them with a single required n_neg (the method is new/unreleased, so non-breaking);
it calls fit(n_unl_to_neg=n_neg) internally. Update docstring, tests, and the
re-executed example notebook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d error

mine_negatives delegated n_neg validation to fit (which sees it as n_unl_to_neg),
so an invalid n_neg raised an error naming the internal parameter. Validate n_neg
explicitly in the frontend so the message names n_neg, and assert the name in the
negative test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-mask

# Conflicts:
#	docs/source/index/release_notes.rst
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant