pu-toolbox 1.0.0__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.
- pu_toolbox/__init__.py +56 -0
- pu_toolbox/advisor/__init__.py +17 -0
- pu_toolbox/advisor/_types.py +130 -0
- pu_toolbox/advisor/recommender.py +195 -0
- pu_toolbox/advisor/rules.py +349 -0
- pu_toolbox/cli/__init__.py +52 -0
- pu_toolbox/cli/demo.py +62 -0
- pu_toolbox/cli/info.py +89 -0
- pu_toolbox/cli/run.py +198 -0
- pu_toolbox/core/__init__.py +46 -0
- pu_toolbox/core/base.py +320 -0
- pu_toolbox/core/config.py +19 -0
- pu_toolbox/core/exceptions.py +32 -0
- pu_toolbox/core/labels.py +185 -0
- pu_toolbox/core/random.py +44 -0
- pu_toolbox/core/tags.py +118 -0
- pu_toolbox/core/validation.py +327 -0
- pu_toolbox/diagnostics/__init__.py +29 -0
- pu_toolbox/diagnostics/report.py +522 -0
- pu_toolbox/diagnostics/sensitivity.py +340 -0
- pu_toolbox/estimators/__init__.py +0 -0
- pu_toolbox/estimators/bias_aware/__init__.py +5 -0
- pu_toolbox/estimators/bias_aware/lbe.py +117 -0
- pu_toolbox/estimators/bias_aware/pusb.py +75 -0
- pu_toolbox/estimators/bias_aware/pusb_kernel.py +295 -0
- pu_toolbox/estimators/classic/__init__.py +4 -0
- pu_toolbox/estimators/classic/elkan_noto.py +450 -0
- pu_toolbox/estimators/classic/llsvm.py +335 -0
- pu_toolbox/estimators/deep/__init__.py +17 -0
- pu_toolbox/estimators/deep/dgpu.py +417 -0
- pu_toolbox/estimators/deep/infomax_pu.py +483 -0
- pu_toolbox/estimators/deep/self_pu.py +837 -0
- pu_toolbox/estimators/deep/vision.py +209 -0
- pu_toolbox/estimators/deep/weighted_contrastive_pu.py +443 -0
- pu_toolbox/estimators/risk/__init__.py +17 -0
- pu_toolbox/estimators/risk/_class_prior.py +41 -0
- pu_toolbox/estimators/risk/dist_pu.py +147 -0
- pu_toolbox/estimators/risk/kldce.py +1093 -0
- pu_toolbox/estimators/risk/ldce.py +563 -0
- pu_toolbox/estimators/risk/nnpu.py +552 -0
- pu_toolbox/estimators/risk/pnu.py +320 -0
- pu_toolbox/estimators/risk/upu.py +494 -0
- pu_toolbox/losses/__init__.py +19 -0
- pu_toolbox/losses/llsvm.py +127 -0
- pu_toolbox/losses/nnpu.py +252 -0
- pu_toolbox/losses/pnu.py +102 -0
- pu_toolbox/losses/upu.py +150 -0
- pu_toolbox/metrics/__init__.py +19 -0
- pu_toolbox/metrics/classification.py +219 -0
- pu_toolbox/model_selection/__init__.py +6 -0
- pu_toolbox/model_selection/split.py +123 -0
- pu_toolbox/preprocessing/__init__.py +85 -0
- pu_toolbox/preprocessing/data_profiler.py +481 -0
- pu_toolbox/preprocessing/profiling.py +351 -0
- pu_toolbox/preprocessing/pu_labeling.py +410 -0
- pu_toolbox/preprocessing/selection_bias.py +326 -0
- pu_toolbox/prior/__init__.py +9 -0
- pu_toolbox/prior/kernel_mean.py +263 -0
- pu_toolbox/prior/pen_l1.py +87 -0
- pu_toolbox/prior/recpe.py +171 -0
- pu_toolbox/registry/__init__.py +47 -0
- pu_toolbox/registry/builtin_methods.py +458 -0
- pu_toolbox/registry/metadata.py +117 -0
- pu_toolbox/registry/registry.py +303 -0
- pu_toolbox/utils/__init__.py +25 -0
- pu_toolbox/utils/activations.py +16 -0
- pu_toolbox/utils/basis.py +163 -0
- pu_toolbox/utils/centroid.py +104 -0
- pu_toolbox/utils/serialization.py +65 -0
- pu_toolbox/workflows/__init__.py +14 -0
- pu_toolbox/workflows/pipeline.py +1006 -0
- pu_toolbox/workflows/report.py +260 -0
- pu_toolbox-1.0.0.dist-info/METADATA +186 -0
- pu_toolbox-1.0.0.dist-info/RECORD +77 -0
- pu_toolbox-1.0.0.dist-info/WHEEL +4 -0
- pu_toolbox-1.0.0.dist-info/entry_points.txt +2 -0
- pu_toolbox-1.0.0.dist-info/licenses/LICENSE +21 -0
pu_toolbox/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""PU Learning Toolbox -- Positive-Unlabeled Learning in Python."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.0.0"
|
|
4
|
+
|
|
5
|
+
from .advisor import ScoringConfig, recommend_from_profile, recommend_methods
|
|
6
|
+
from .diagnostics import analyze_pu_sensitivity, build_diagnostic_report
|
|
7
|
+
from .estimators.bias_aware.lbe import LBEClassifier
|
|
8
|
+
from .estimators.bias_aware.pusb import PUSBClassifier
|
|
9
|
+
from .estimators.bias_aware.pusb_kernel import PUSBKernelClassifier
|
|
10
|
+
from .estimators.classic.elkan_noto import ElkanNotoClassifier
|
|
11
|
+
from .estimators.classic.llsvm import LLSVMClassifier
|
|
12
|
+
from .estimators.deep.dgpu import DGPUClassifier
|
|
13
|
+
from .estimators.deep.infomax_pu import InfoMaxPUClassifier
|
|
14
|
+
from .estimators.deep.self_pu import SelfPUClassifier
|
|
15
|
+
from .estimators.deep.weighted_contrastive_pu import WeightedContrastivePUClassifier
|
|
16
|
+
from .estimators.risk.dist_pu import DistPUClassifier
|
|
17
|
+
from .estimators.risk.kldce import KLDCEClassifier
|
|
18
|
+
from .estimators.risk.ldce import LDCEClassifier
|
|
19
|
+
from .estimators.risk.nnpu import NonNegativePUClassifier
|
|
20
|
+
from .estimators.risk.pnu import PNUClassifier
|
|
21
|
+
from .estimators.risk.upu import UPUClassifier
|
|
22
|
+
from .preprocessing import make_sar_dataset, profile_pu_data
|
|
23
|
+
from .prior.kernel_mean import KernelMeanPriorEstimator
|
|
24
|
+
from .prior.pen_l1 import ClassPriorEstimator
|
|
25
|
+
from .prior.recpe import ReCPEEstimator
|
|
26
|
+
from .workflows import PipelineReport, PUPipeline
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ClassPriorEstimator",
|
|
30
|
+
"DGPUClassifier",
|
|
31
|
+
"DistPUClassifier",
|
|
32
|
+
"ElkanNotoClassifier",
|
|
33
|
+
"InfoMaxPUClassifier",
|
|
34
|
+
"KLDCEClassifier",
|
|
35
|
+
"KernelMeanPriorEstimator",
|
|
36
|
+
"LBEClassifier",
|
|
37
|
+
"LDCEClassifier",
|
|
38
|
+
"LLSVMClassifier",
|
|
39
|
+
"NonNegativePUClassifier",
|
|
40
|
+
"PNUClassifier",
|
|
41
|
+
"PUSBClassifier",
|
|
42
|
+
"PUSBKernelClassifier",
|
|
43
|
+
"PipelineReport",
|
|
44
|
+
"PUPipeline",
|
|
45
|
+
"ReCPEEstimator",
|
|
46
|
+
"ScoringConfig",
|
|
47
|
+
"SelfPUClassifier",
|
|
48
|
+
"UPUClassifier",
|
|
49
|
+
"WeightedContrastivePUClassifier",
|
|
50
|
+
"analyze_pu_sensitivity",
|
|
51
|
+
"build_diagnostic_report",
|
|
52
|
+
"make_sar_dataset",
|
|
53
|
+
"profile_pu_data",
|
|
54
|
+
"recommend_from_profile",
|
|
55
|
+
"recommend_methods",
|
|
56
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Algorithm advisor — data-driven PU method recommendation."""
|
|
2
|
+
|
|
3
|
+
from ._types import MethodCandidate as MethodCandidate
|
|
4
|
+
from ._types import RecommendationResult as RecommendationResult
|
|
5
|
+
from .recommender import recommend_from_profile as recommend_from_profile
|
|
6
|
+
from .recommender import recommend_methods as recommend_methods
|
|
7
|
+
from .rules import DEFAULT_CONFIG as DEFAULT_CONFIG
|
|
8
|
+
from .rules import ScoringConfig as ScoringConfig
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"DEFAULT_CONFIG",
|
|
12
|
+
"MethodCandidate",
|
|
13
|
+
"RecommendationResult",
|
|
14
|
+
"ScoringConfig",
|
|
15
|
+
"recommend_from_profile",
|
|
16
|
+
"recommend_methods",
|
|
17
|
+
]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Data types for algorithm recommendations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from ..registry.metadata import AlgorithmMetadata
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MethodCandidate",
|
|
14
|
+
"RecommendationResult",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class MethodCandidate:
|
|
20
|
+
"""A single recommended method with score and rationale."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
score: float
|
|
24
|
+
rank: int
|
|
25
|
+
reasons: tuple[str, ...]
|
|
26
|
+
warnings: tuple[str, ...]
|
|
27
|
+
metadata: AlgorithmMetadata
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
return {
|
|
31
|
+
"name": self.name,
|
|
32
|
+
"score": round(self.score, 1),
|
|
33
|
+
"rank": self.rank,
|
|
34
|
+
"reasons": list(self.reasons),
|
|
35
|
+
"warnings": list(self.warnings),
|
|
36
|
+
"family": str(self.metadata.family.value),
|
|
37
|
+
"backend": str(self.metadata.backend.value),
|
|
38
|
+
"maturity": str(self.metadata.maturity.value),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class RecommendationResult:
|
|
44
|
+
"""Ranked algorithm recommendations with filters and warnings."""
|
|
45
|
+
|
|
46
|
+
candidates: tuple[MethodCandidate, ...]
|
|
47
|
+
filters_applied: dict[str, Any]
|
|
48
|
+
global_warnings: tuple[str, ...]
|
|
49
|
+
provenance: dict[str, Any]
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"schema_version": "1.0",
|
|
54
|
+
"candidates": [c.to_dict() for c in self.candidates],
|
|
55
|
+
"filters_applied": self.filters_applied,
|
|
56
|
+
"global_warnings": list(self.global_warnings),
|
|
57
|
+
"provenance": self.provenance,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def to_json(self, *, indent: int = 2) -> str:
|
|
61
|
+
return json.dumps(
|
|
62
|
+
self.to_dict(),
|
|
63
|
+
ensure_ascii=False,
|
|
64
|
+
indent=indent,
|
|
65
|
+
allow_nan=False,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def to_markdown(self) -> str:
|
|
69
|
+
lines = [
|
|
70
|
+
"# PU Method Recommendations",
|
|
71
|
+
"",
|
|
72
|
+
]
|
|
73
|
+
if self.global_warnings:
|
|
74
|
+
lines.append("## Warnings")
|
|
75
|
+
lines.append("")
|
|
76
|
+
for w in self.global_warnings:
|
|
77
|
+
lines.append(f"- {w}")
|
|
78
|
+
lines.append("")
|
|
79
|
+
lines.extend(
|
|
80
|
+
[
|
|
81
|
+
"## Recommended Methods",
|
|
82
|
+
"",
|
|
83
|
+
"| Rank | Method | Score | Family | Backend | Maturity |",
|
|
84
|
+
"|---:|---|---:|---|---|---|",
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
for c in self.candidates:
|
|
88
|
+
lines.append(
|
|
89
|
+
f"| {c.rank} | {c.name} | {c.score:.1f} "
|
|
90
|
+
f"| {c.metadata.family.value} | {c.metadata.backend.value} "
|
|
91
|
+
f"| {c.metadata.maturity.value} |"
|
|
92
|
+
)
|
|
93
|
+
lines.append("")
|
|
94
|
+
for c in self.candidates:
|
|
95
|
+
lines.append(f"### {c.rank}. {c.name}")
|
|
96
|
+
lines.append("")
|
|
97
|
+
if c.reasons:
|
|
98
|
+
for r in c.reasons:
|
|
99
|
+
lines.append(f"- {r}")
|
|
100
|
+
if c.warnings:
|
|
101
|
+
lines.append("")
|
|
102
|
+
for w in c.warnings:
|
|
103
|
+
lines.append(f"- **Warning**: {w}")
|
|
104
|
+
lines.append("")
|
|
105
|
+
return "\n".join(lines)
|
|
106
|
+
|
|
107
|
+
def save(
|
|
108
|
+
self,
|
|
109
|
+
path: str | Path,
|
|
110
|
+
*,
|
|
111
|
+
format: Literal["json", "markdown"] | None = None,
|
|
112
|
+
) -> Path:
|
|
113
|
+
destination = Path(path)
|
|
114
|
+
fmt = format
|
|
115
|
+
if fmt is None:
|
|
116
|
+
suffix = destination.suffix.lower()
|
|
117
|
+
if suffix == ".json":
|
|
118
|
+
fmt = "json"
|
|
119
|
+
elif suffix in {".md", ".markdown"}:
|
|
120
|
+
fmt = "markdown"
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"Cannot infer report format. Use a .json/.md suffix or pass format=."
|
|
124
|
+
)
|
|
125
|
+
if fmt not in {"json", "markdown"}:
|
|
126
|
+
raise ValueError(f"Unknown format {fmt!r}; expected 'json' or 'markdown'.")
|
|
127
|
+
content = self.to_markdown() if fmt == "markdown" else self.to_json() + "\n"
|
|
128
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
destination.write_text(content, encoding="utf-8")
|
|
130
|
+
return destination
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# ruff: noqa: N803
|
|
2
|
+
|
|
3
|
+
"""Algorithm recommender — match data profiles to registered PU methods."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from ..core.tags import Assumption, Maturity, Scenario
|
|
13
|
+
from ..preprocessing import PUDataProfile, profile_pu_data
|
|
14
|
+
from ..registry.registry import list_algorithms
|
|
15
|
+
from ._types import MethodCandidate, RecommendationResult
|
|
16
|
+
from .rules import DEFAULT_CONFIG, ScoringConfig, global_warnings, method_warnings, score_method
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"recommend_from_profile",
|
|
20
|
+
"recommend_methods",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def recommend_methods(
|
|
25
|
+
X: Any,
|
|
26
|
+
y_pu: np.ndarray,
|
|
27
|
+
*,
|
|
28
|
+
scenario: Scenario | str | None = None,
|
|
29
|
+
assumption: Assumption | str | None = None,
|
|
30
|
+
class_prior: float | None = None,
|
|
31
|
+
has_gpu: bool = False,
|
|
32
|
+
top_k: int = 5,
|
|
33
|
+
random_state: int | None = 42,
|
|
34
|
+
config: ScoringConfig | None = None,
|
|
35
|
+
) -> RecommendationResult:
|
|
36
|
+
"""Recommend PU methods by profiling data and matching against the registry.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
X : array-like of shape (n_samples, n_features)
|
|
41
|
+
y_pu : array-like of shape (n_samples,)
|
|
42
|
+
PU labels (1 = labeled positive, 0 = unlabeled).
|
|
43
|
+
scenario : Scenario or str, optional
|
|
44
|
+
Data collection scenario constraint.
|
|
45
|
+
assumption : Assumption or str, optional
|
|
46
|
+
Labeling mechanism assumption constraint.
|
|
47
|
+
class_prior : float, optional
|
|
48
|
+
Known class prior P(Y=1). Methods requiring it are excluded when None.
|
|
49
|
+
has_gpu : bool
|
|
50
|
+
Whether GPU is available.
|
|
51
|
+
top_k : int
|
|
52
|
+
Maximum number of candidates to return.
|
|
53
|
+
random_state : int, optional
|
|
54
|
+
Seed for the data profiler.
|
|
55
|
+
config : ScoringConfig, optional
|
|
56
|
+
Custom scoring weights. Uses ``DEFAULT_CONFIG`` when not provided.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
RecommendationResult
|
|
61
|
+
"""
|
|
62
|
+
profile = profile_pu_data(X, y_pu, class_prior=class_prior, random_state=random_state)
|
|
63
|
+
return recommend_from_profile(
|
|
64
|
+
profile,
|
|
65
|
+
scenario=scenario,
|
|
66
|
+
assumption=assumption,
|
|
67
|
+
class_prior=class_prior,
|
|
68
|
+
has_gpu=has_gpu,
|
|
69
|
+
top_k=top_k,
|
|
70
|
+
config=config,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def recommend_from_profile(
|
|
75
|
+
profile: PUDataProfile,
|
|
76
|
+
*,
|
|
77
|
+
scenario: Scenario | str | None = None,
|
|
78
|
+
assumption: Assumption | str | None = None,
|
|
79
|
+
class_prior: float | None = None,
|
|
80
|
+
class_prior_source: str | None = None,
|
|
81
|
+
has_gpu: bool = False,
|
|
82
|
+
top_k: int = 5,
|
|
83
|
+
config: ScoringConfig | None = None,
|
|
84
|
+
) -> RecommendationResult:
|
|
85
|
+
"""Recommend PU methods from an existing data profile.
|
|
86
|
+
|
|
87
|
+
Skips data profiling when a ``PUDataProfile`` is already available.
|
|
88
|
+
See :func:`recommend_methods` for parameter descriptions.
|
|
89
|
+
|
|
90
|
+
.. note::
|
|
91
|
+
|
|
92
|
+
``class_prior`` controls hard filtering (excluding methods that
|
|
93
|
+
require π when it is unavailable). It must be consistent with
|
|
94
|
+
the value used to build *profile*; passing a different value
|
|
95
|
+
produces contradictory results.
|
|
96
|
+
|
|
97
|
+
``class_prior_source`` records where *class_prior* came from
|
|
98
|
+
("user" / "estimated" / ...); it only affects the global-warning
|
|
99
|
+
wording, never filtering or scoring.
|
|
100
|
+
"""
|
|
101
|
+
if top_k < 1:
|
|
102
|
+
raise ValueError(f"top_k must be >= 1, got {top_k}")
|
|
103
|
+
|
|
104
|
+
cfg = config or DEFAULT_CONFIG
|
|
105
|
+
|
|
106
|
+
# register_all_builtin_methods is idempotent (registry-level dedup),
|
|
107
|
+
# so calling it per recommendation is safe and never goes stale.
|
|
108
|
+
from ..registry.builtin_methods import register_all_builtin_methods
|
|
109
|
+
|
|
110
|
+
register_all_builtin_methods()
|
|
111
|
+
|
|
112
|
+
scenario_enum = _resolve_enum(scenario, Scenario) if scenario is not None else None
|
|
113
|
+
assumption_enum = _resolve_enum(assumption, Assumption) if assumption is not None else None
|
|
114
|
+
|
|
115
|
+
all_methods = list_algorithms(trainable_only=True)
|
|
116
|
+
|
|
117
|
+
filters_applied: dict[str, Any] = {}
|
|
118
|
+
filtered = [m for m in all_methods if m.maturity != Maturity.DEPRECATED]
|
|
119
|
+
if len(filtered) != len(all_methods):
|
|
120
|
+
filters_applied["maturity"] = "excluded deprecated"
|
|
121
|
+
|
|
122
|
+
if scenario_enum is not None:
|
|
123
|
+
filtered = [m for m in filtered if scenario_enum in m.scenario]
|
|
124
|
+
filters_applied["scenario"] = scenario_enum.value
|
|
125
|
+
|
|
126
|
+
if profile.summary.get("is_sparse", False):
|
|
127
|
+
filtered = [m for m in filtered if m.supports_sparse]
|
|
128
|
+
filters_applied["sparse_support"] = True
|
|
129
|
+
|
|
130
|
+
if class_prior is None:
|
|
131
|
+
filtered = [m for m in filtered if not m.requires_class_prior]
|
|
132
|
+
filters_applied["class_prior_required"] = "excluded (not provided)"
|
|
133
|
+
|
|
134
|
+
if assumption_enum is not None:
|
|
135
|
+
filtered = [m for m in filtered if assumption_enum in m.assumption]
|
|
136
|
+
filters_applied["assumption"] = assumption_enum.value
|
|
137
|
+
|
|
138
|
+
scored = []
|
|
139
|
+
for meta in filtered:
|
|
140
|
+
sc, reasons = score_method(meta, profile, assumption_enum, has_gpu, cfg)
|
|
141
|
+
warns = method_warnings(meta, profile, assumption_enum)
|
|
142
|
+
scored.append((meta, sc, reasons, warns))
|
|
143
|
+
|
|
144
|
+
scored.sort(key=lambda t: t[1], reverse=True)
|
|
145
|
+
scored = scored[:top_k]
|
|
146
|
+
|
|
147
|
+
candidates = tuple(
|
|
148
|
+
MethodCandidate(
|
|
149
|
+
name=meta.name,
|
|
150
|
+
score=sc,
|
|
151
|
+
rank=i + 1,
|
|
152
|
+
reasons=tuple(reasons),
|
|
153
|
+
warnings=tuple(warns),
|
|
154
|
+
metadata=meta,
|
|
155
|
+
)
|
|
156
|
+
for i, (meta, sc, reasons, warns) in enumerate(scored)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
gw = list(global_warnings(profile, class_prior, class_prior_source))
|
|
160
|
+
if profile.summary.get("is_sparse", False) and not filtered:
|
|
161
|
+
# No registered method accepts sparse input; say so instead of
|
|
162
|
+
# silently returning zero candidates.
|
|
163
|
+
gw.append(
|
|
164
|
+
"Sparse data: no registered method supports sparse input. "
|
|
165
|
+
"Densify the matrix (e.g. X.toarray()) or profile dense data."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
provenance = {
|
|
169
|
+
"n_samples": profile.summary.get("n_samples"),
|
|
170
|
+
"n_features": profile.summary.get("n_features"),
|
|
171
|
+
"n_candidates_before_filter": len(all_methods),
|
|
172
|
+
"n_candidates_after_filter": len(filtered),
|
|
173
|
+
"top_k": top_k,
|
|
174
|
+
"has_gpu": has_gpu,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return RecommendationResult(
|
|
178
|
+
candidates=candidates,
|
|
179
|
+
filters_applied=filters_applied,
|
|
180
|
+
global_warnings=tuple(gw),
|
|
181
|
+
provenance=provenance,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_enum(value: str | Any, enum_cls: type) -> Any:
|
|
186
|
+
if isinstance(value, enum_cls):
|
|
187
|
+
return value
|
|
188
|
+
if isinstance(value, Enum):
|
|
189
|
+
raise ValueError(f"Expected {enum_cls.__name__}, got {type(value).__name__}: {value!r}")
|
|
190
|
+
for member in enum_cls:
|
|
191
|
+
if member.value.lower() == str(value).lower():
|
|
192
|
+
return member
|
|
193
|
+
if member.name.lower() == str(value).lower():
|
|
194
|
+
return member
|
|
195
|
+
raise ValueError(f"Unknown {enum_cls.__name__} value: {value!r}")
|