Skip to content

Sklearn api compliance - #92

Merged
vc1492a merged 20 commits into
feat/sklearn-api-conventionfrom
sklearn-api-compliance
Sep 22, 2026
Merged

vc1492a merged 20 commits into
feat/sklearn-api-conventionfrom
sklearn-api-compliance

Conversation

@SaritNike

Copy link
Copy Markdown
Collaborator

In the branch feat/sklearn-api-convention we have realized that quite a lot was actually still missing to be fully compliant with the sklearn API. This is why we have decided to add and test these new fixes first in a separate branch.
Now that the check-api-compliance test is successfully passing we are ready to merge it all into the feat/sklearn-api-convention branch

SaritNike and others added 3 commits July 26, 2026 21:45
- Update model state validation in stream() to raise Scikit-Learn's NotFittedError.
- Track n_features_in_ and validate feature consistency in predict() and decision_function().- Change generated variable names to include _ suffix and add property decorators to ensure backward compatibility
- Fix class inheritance MRO order by moving BaseEstimator to the end.
- Enhance _convert_to_array() to reject empty arrays, complex types, and invalid numeric inputs with appropriate exceptions as scikit-learn demands.
- Fortify _check_missing_values() to validate both NaN and infinite values, and update test assertions.
Add a _compat shim so scikit-learn remains an extras install, densify sparse inputs, keep init params immutable during fit, and finish the outlier API (score_samples, offset_, decision/predict consistency) so check_estimator passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Accept scipy.sparse input by densifying at validation, fix stream() for sparse rows, raise a clear ImportError without scipy, and document the behavior plus a PyNomaly[sparse] extra.

Co-authored-by: Cursor <cursoragent@cursor.com>

@vc1492a vc1492a left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review done (manual + AI-assisted). Overall this is in good shape and ready to merge into the feature branch once the fit(X, y) comment below is addressed or consciously deferred.

Verified locally against head fa903ec:

  • Full test suite: 46 passed, 4 skipped
  • check_estimator(LoOP()): all checks pass (one documented xfail: check_fit1d, since LoOP intentionally accepts 1-d input and reshapes to (-1, 1))
  • Param immutability: get_params() identical before/after fit() (effective values live on n_neighbors_, _fit_n_jobs, etc.)
  • Re-fit across input modes (raw data → distance matrix) produces correct, consistent scores
  • Sparse CSR/CSC: fit/predict/stream work, dense/sparse scores identical, clear ImportError without scipy
  • predict/decision_function/score_samples are mutually consistent (decision = scores - offset_, labels from decision >= 0)
  • Distance-matrix mode: n_features_in_ = 1, predict works on single-feature input

Findings (line comments):

  1. fit(X, y) with a (n, 1) label array silently becomes distance_matrix and returns an unfitted model — the one thing I'd fix before merge
  2. fit() warns-and-returns on invalid input instead of raising ValueError (acceptable for BC, revisit in the deprecation-warnings PR)
  3. stream() return annotation says np.ndarray but now returns float
  4. docs/api.md still shows legacy non-underscore attribute names as primary

On the read-side breaking change we discussed: confirmed distance_matrix after fit is still the init input (often None), and the computed matrix is only on distance_matrix_. That's correct per sklearn's config/state firewall — it just needs to be loud in the changelog and the follow-up warnings PR.

No automation will merge this — approving only records the review; merge stays manual.

Comment thread PyNomaly/loop.py Resolved
Comment thread PyNomaly/loop.py Resolved
Comment thread PyNomaly/loop.py Resolved
Comment thread docs/api.md
| Parameter | Type | Default | Description |
|---|---|---|---|
| `data` | `np.ndarray` or `pd.DataFrame` | `None` | Input data as a 2D array with shape (n_observations, n_features). Mutually exclusive with `distance_matrix`. |
| `data` | `np.ndarray`, `pd.DataFrame`, or `scipy.sparse` matrix | `None` | Input data as a 2D array with shape (n_observations, n_features). Sparse matrices are accepted and densified internally (requires scipy). Mutually exclusive with `distance_matrix`. |

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs in this file still reference the legacy non-underscore attributes (clf.local_outlier_probabilities on line 50, the attribute table at lines 86/90). They still work as read-only property aliases, so nothing is wrong — but since fitted state now canonically lives on local_outlier_probabilities_, distance_matrix_, etc., the docs should show the underscore names as primary and mention the aliases as deprecated. Good candidate for the follow-up PR that adds warnings on legacy attribute access (note: distance_matrix after fit is the init input, not the computed matrix — that one has no alias by design and is the main read-side breaking change to document).

vc1492a and others added 2 commits August 25, 2026 10:04
Align the annotation and API docs with the actual scalar return value, and note the type change in the 1.0.0 changelog.

Co-authored-by: Cursor <cursoragent@cursor.com>
Restrict the deprecated positional distance_matrix fallback in fit() to calls with no raw data and a y wider than one column, so sklearn-style fit(X, y) with (n, 1) labels fits normally instead of returning an unfitted model. Adds a regression test.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vc1492a vc1492a self-assigned this Aug 25, 2026
Move data from positional constructor calls into fit(), fix the distance-matrix self-removal NameError in README/user-guide, and note the positional constructor break in the 1.0.0 changelog.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vc1492a

vc1492a commented Aug 25, 2026

Copy link
Copy Markdown
Owner

@SaritNike finished my review and addressed two small issues I identified with the help of Fable. I also took some time to update our documentation and example scripts to reflect the changes in this PR.

Please take another look. If this looks good to you, let's go ahead and merge in and address the deprecation warnings in a follow up.

@SaritNike

Copy link
Copy Markdown
Collaborator Author

I took another round and reviewed it. we can merge it into the feature branch

SaritNike and others added 9 commits September 4, 2026 23:06
pandas is not a hard dependency, so bind the name only under
typing.TYPE_CHECKING for the string type hints instead of importing it
at runtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lity.

The exceptions moved to PyNomaly.exceptions in 1.0.0 and were no longer
importable from PyNomaly.loop. Import them back into loop, list them in
loop.__all__, add a test for the import path, and document the new home
in the changelog and API docs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e job.

Drop --exit-zero from the E9/F63/F7/F82 flake8 run so syntax errors and
undefined names fail the build. The style/complexity run stays advisory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed_failed_checks.

The keyword was added in scikit-learn 1.6, which needs Python 3.9+.
On Python 3.8 (scikit-learn 1.3.x) the test raised TypeError and failed
CI; it now skips with a clear reason there and runs everywhere else.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Remove 3.8 from the CI matrix, declare python_requires='>=3.9' with
3.9 through 3.14 classifiers in setup.py, update the supported-version
statements in the README and docs, and record the removal under 1.0.0
in the changelog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every remaining CI Python version (3.9+) resolves scikit-learn 1.6.1 or
newer, all of which support check_estimator(expected_failed_checks=...),
so the compliance test now always runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@SaritNike

Copy link
Copy Markdown
Collaborator Author

I see that you have added more commits. Feel free to merge when you are ready

vc1492a and others added 4 commits September 22, 2026 10:28
Covers 1-d, (n, 1), and (n, 2) label arrays alongside raw data X,
asserting the raw-data path is used, no FutureWarning fires, and the
scores equal fit(X). Complements the (n, 1) test added in 9f74189 for
the fit(X, y) finding on PR #92.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Translate SaritNike's contract test from the 0.4.0 constructor to the
scikit-learn-style API: configuration-only constructor, data passed to
fit(X, y=None, ...), _-suffixed fitted attributes with their read-only
legacy aliases, the sklearn outlier methods, get_params() keys, the LoOP
alias, and the exception classes now defined in PyNomaly.exceptions and
re-exported from PyNomaly.loop. The structure and intent of the original
test are kept; cluster_labels_u is dropped from the expected attributes
because it became private (_cluster_labels_u) in 1.0.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vc1492a
vc1492a merged commit a35b62e into feat/sklearn-api-convention Sep 22, 2026
12 checks passed
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.

2 participants