oversampleqa 0.5.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.
@@ -0,0 +1,210 @@
1
+ """
2
+ oversampleqa: A diagnostic toolkit for validating oversampling methods.
3
+
4
+ This package implements validation methods for synthetic data generated by
5
+ oversampling techniques like SMOTE, ADASYN, and their variants.
6
+ """
7
+
8
+ __version__ = "0.5.1"
9
+ __author__ = "Diogo Ribeiro"
10
+ __email__ = "dfr@esmad.ipp.pt"
11
+ __orcid__ = "0009-0001-2022-7072"
12
+
13
+ from .advanced_benchmark import (
14
+ DatasetRepository,
15
+ StatisticalBenchmark,
16
+ create_benchmark_report,
17
+ format_statistical_summary,
18
+ )
19
+ from .benchmark import (
20
+ compute_ranking,
21
+ export_benchmark_results,
22
+ load_standard_datasets,
23
+ run_benchmark,
24
+ )
25
+ from .caching import ValidationCache
26
+ from .cli import main as cli_main
27
+ from .clustering import cluster_based_diagnostics
28
+ from .deprecation import deprecated
29
+ from .distance import (
30
+ braycurtis_distance,
31
+ canberra_distance,
32
+ chebyshev_distance,
33
+ correlation_distance,
34
+ distance_matrix,
35
+ energy_distance,
36
+ hamming_distance,
37
+ hassanat_distance,
38
+ hellinger_distance,
39
+ jaccard_distance,
40
+ jensen_shannon_distance,
41
+ mahalanobis_distance,
42
+ minkowski_distance,
43
+ wasserstein_1d_distance,
44
+ )
45
+ from .estimator import OversamplingValidator, validation_scorer
46
+ from .fidelity import (
47
+ BoundaryReport,
48
+ FidelityReport,
49
+ ManifoldMetrics,
50
+ MemorisationReport,
51
+ boundary_violation_rate,
52
+ fidelity_report,
53
+ memorisation_report,
54
+ precision_recall_density_coverage,
55
+ )
56
+ from .inference import (
57
+ NullCalibration,
58
+ TwoSampleTestResult,
59
+ cross_match_test,
60
+ mst_two_sample_test,
61
+ nn_two_sample_test,
62
+ null_error_rate,
63
+ )
64
+ from .memory_efficient_validator import MemoryEfficientValidator
65
+ from .metrics import (
66
+ calculate_error_rate,
67
+ check_model_fairness,
68
+ confidence_ratio,
69
+ duplication_rate,
70
+ local_density_divergence,
71
+ minority_recall_loss,
72
+ noise_sensitivity_diagnostic,
73
+ umap_manifold_distance,
74
+ )
75
+ from .optimized_distance import OptimizedDistanceMatrix
76
+ from .plotting import (
77
+ plot_class_balance,
78
+ plot_distance_histogram,
79
+ plot_error_boxplot,
80
+ plot_error_comparison,
81
+ plot_error_heatmap,
82
+ plot_error_ranking,
83
+ plot_noise_sensitivity,
84
+ plot_sample_distribution,
85
+ )
86
+ from .plugin_contract import (
87
+ METRIC_DOMAINS,
88
+ AxiomReport,
89
+ MetricPlugin,
90
+ check_metric_axioms,
91
+ )
92
+ from .plugin_system import plugin_manager, register_metric, register_validator
93
+ from .report import generate_report
94
+ from .reports import SCHEMA_VERSION, RunMetadata, ValidationReport
95
+ from .surrogate import evaluate_surrogate_models
96
+ from .typed_validator import (
97
+ PydanticValidationConfig,
98
+ TypedValidator,
99
+ registry,
100
+ validation_session,
101
+ )
102
+ from .types import (
103
+ BenchmarkConfig,
104
+ ConfigurationError,
105
+ MetricError,
106
+ OversampleQAError,
107
+ ReferenceSet,
108
+ ValidationConfig,
109
+ ValidationDetails,
110
+ ValidationError,
111
+ ValidationMode,
112
+ ValidationResult,
113
+ )
114
+ from .validator import (
115
+ extract_synthetic_samples,
116
+ validate_multiclass_oversampling,
117
+ validate_oversampling,
118
+ )
119
+
120
+ __all__ = [
121
+ "METRIC_DOMAINS",
122
+ "SCHEMA_VERSION",
123
+ "AxiomReport",
124
+ "BenchmarkConfig",
125
+ "BoundaryReport",
126
+ "ConfigurationError",
127
+ "DatasetRepository",
128
+ "FidelityReport",
129
+ "ManifoldMetrics",
130
+ "MemorisationReport",
131
+ "MemoryEfficientValidator",
132
+ "MetricError",
133
+ "MetricPlugin",
134
+ "NullCalibration",
135
+ "OptimizedDistanceMatrix",
136
+ "OversampleQAError",
137
+ "OversamplingValidator",
138
+ "PydanticValidationConfig",
139
+ "ReferenceSet",
140
+ "RunMetadata",
141
+ "StatisticalBenchmark",
142
+ "TwoSampleTestResult",
143
+ "TypedValidator",
144
+ "ValidationCache",
145
+ "ValidationConfig",
146
+ "ValidationDetails",
147
+ "ValidationError",
148
+ "ValidationMode",
149
+ "ValidationReport",
150
+ "ValidationResult",
151
+ "boundary_violation_rate",
152
+ "braycurtis_distance",
153
+ "calculate_error_rate",
154
+ "canberra_distance",
155
+ "chebyshev_distance",
156
+ "check_metric_axioms",
157
+ "check_model_fairness",
158
+ "cli_main",
159
+ "cluster_based_diagnostics",
160
+ "compute_ranking",
161
+ "confidence_ratio",
162
+ "correlation_distance",
163
+ "create_benchmark_report",
164
+ "cross_match_test",
165
+ "deprecated",
166
+ "distance_matrix",
167
+ "duplication_rate",
168
+ "energy_distance",
169
+ "evaluate_surrogate_models",
170
+ "export_benchmark_results",
171
+ "extract_synthetic_samples",
172
+ "fidelity_report",
173
+ "format_statistical_summary",
174
+ "generate_report",
175
+ "hamming_distance",
176
+ "hassanat_distance",
177
+ "hellinger_distance",
178
+ "jaccard_distance",
179
+ "jensen_shannon_distance",
180
+ "load_standard_datasets",
181
+ "local_density_divergence",
182
+ "mahalanobis_distance",
183
+ "memorisation_report",
184
+ "minkowski_distance",
185
+ "minority_recall_loss",
186
+ "mst_two_sample_test",
187
+ "nn_two_sample_test",
188
+ "noise_sensitivity_diagnostic",
189
+ "null_error_rate",
190
+ "plot_class_balance",
191
+ "plot_distance_histogram",
192
+ "plot_error_boxplot",
193
+ "plot_error_comparison",
194
+ "plot_error_heatmap",
195
+ "plot_error_ranking",
196
+ "plot_noise_sensitivity",
197
+ "plot_sample_distribution",
198
+ "plugin_manager",
199
+ "precision_recall_density_coverage",
200
+ "register_metric",
201
+ "register_validator",
202
+ "registry",
203
+ "run_benchmark",
204
+ "umap_manifold_distance",
205
+ "validate_multiclass_oversampling",
206
+ "validate_oversampling",
207
+ "validation_scorer",
208
+ "validation_session",
209
+ "wasserstein_1d_distance",
210
+ ]
@@ -0,0 +1,131 @@
1
+ """Provenance metadata for benchmark datasets.
2
+
3
+ A benchmark result is only interpretable if you can say where its data came
4
+ from. These helpers build one consistent record for that, so the two catalogs --
5
+ :func:`~oversampleqa.benchmark.load_standard_datasets` and
6
+ :class:`~oversampleqa.advanced_benchmark.DatasetRepository` -- describe their
7
+ datasets the same way rather than each inventing a shape.
8
+
9
+ Every record carries the same six keys:
10
+
11
+ ``source``
12
+ ``"synthetic"``, ``"bundled"`` or ``"OpenML"``. The coarse question of
13
+ whether the data is generated, shipped with scikit-learn, or downloaded.
14
+ ``generator``
15
+ The fully-qualified callable that produced or fetched it.
16
+ ``params``
17
+ Arguments passed, including any ``random_state``. For synthetic data this
18
+ is sufficient to regenerate the dataset exactly.
19
+ ``url``
20
+ Where a human can read about it.
21
+ ``license``
22
+ Terms. "Unknown" is a legitimate value and is better than omitting the key,
23
+ because omission reads as "no restrictions" to a hurried reader.
24
+ ``notes``
25
+ Anything a reader needs in order not to misread the numbers -- applied
26
+ preprocessing, truncation, known caveats.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+
33
+ __all__ = [
34
+ "SKLEARN_LICENSE",
35
+ "bundled_provenance",
36
+ "openml_provenance",
37
+ "synthetic_provenance",
38
+ ]
39
+
40
+ SKLEARN_LICENSE = "BSD-3-Clause (scikit-learn synthetic generator)"
41
+
42
+ _SKLEARN_GENERATORS_URL = (
43
+ "https://scikit-learn.org/stable/datasets/sample_generators.html"
44
+ )
45
+
46
+
47
+ def synthetic_provenance(generator: str, **params: Any) -> dict[str, Any]:
48
+ """Build provenance for a scikit-learn synthetic dataset.
49
+
50
+ Args:
51
+ generator: Fully-qualified name of the generator used.
52
+ **params: Generation parameters, including ``random_state``.
53
+
54
+ Returns:
55
+ A provenance record.
56
+ """
57
+ return {
58
+ "source": "synthetic",
59
+ "generator": generator,
60
+ "params": params,
61
+ "url": _SKLEARN_GENERATORS_URL,
62
+ "license": SKLEARN_LICENSE,
63
+ "notes": (
64
+ "Generated deterministically from the fixed random_state; "
65
+ "not real-world data."
66
+ ),
67
+ }
68
+
69
+
70
+ def bundled_provenance(
71
+ generator: str,
72
+ *,
73
+ url: str,
74
+ license: str,
75
+ notes: str = "",
76
+ **params: Any,
77
+ ) -> dict[str, Any]:
78
+ """Build provenance for a dataset shipped inside scikit-learn.
79
+
80
+ Bundled data is reproducible in the sense that it does not change between
81
+ runs, but it is real-world data with its own citation and terms, so it is
82
+ recorded separately from synthetic data rather than lumped in with it.
83
+
84
+ Args:
85
+ generator: Fully-qualified loader, e.g. ``sklearn.datasets.load_breast_cancer``.
86
+ url: Where the dataset is documented.
87
+ license: Terms of use.
88
+ notes: Caveats a reader needs, such as applied truncation.
89
+ **params: Arguments passed to the loader.
90
+
91
+ Returns:
92
+ A provenance record.
93
+ """
94
+ return {
95
+ "source": "bundled",
96
+ "generator": generator,
97
+ "params": params,
98
+ "url": url,
99
+ "license": license,
100
+ "notes": notes,
101
+ }
102
+
103
+
104
+ def openml_provenance(
105
+ name: str,
106
+ version: int,
107
+ *,
108
+ notes: str = "",
109
+ ) -> dict[str, Any]:
110
+ """Build provenance for a dataset fetched from OpenML.
111
+
112
+ The version is part of the record because it is the only thing standing
113
+ between a "reproducible" benchmark and an upstream dataset being silently
114
+ replaced under it.
115
+
116
+ Args:
117
+ name: OpenML dataset name.
118
+ version: Pinned OpenML version.
119
+ notes: Preprocessing or caveats.
120
+
121
+ Returns:
122
+ A provenance record.
123
+ """
124
+ return {
125
+ "source": "OpenML",
126
+ "generator": "sklearn.datasets.fetch_openml",
127
+ "params": {"name": name, "version": version},
128
+ "url": f"https://www.openml.org/search?type=data&q={name}",
129
+ "license": "Varies per dataset; see the OpenML page.",
130
+ "notes": notes or f"Downloaded from OpenML with the version pinned to {version}.",
131
+ }
@@ -0,0 +1,81 @@
1
+ """Frame rendering shared by the report and export paths.
2
+
3
+ Lives here rather than in ``report`` because ``report`` imports from
4
+ ``benchmark``, so ``benchmark`` importing back from ``report`` would be a
5
+ cycle -- and both need to render a frame as Markdown. Two copies is how the
6
+ ``to_csv(sep="|")`` bug survived in one of them after being fixed in the
7
+ other.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ import pandas as pd
15
+
16
+ __all__ = ["frame_to_html", "frame_to_markdown"]
17
+
18
+
19
+ def frame_to_markdown(frame: pd.DataFrame, *, float_format: str = "{:.4f}") -> str:
20
+ """Render a DataFrame as a GitHub-flavoured Markdown table.
21
+
22
+ Written out rather than delegated to ``DataFrame.to_markdown``, which needs
23
+ ``tabulate``. That is installed here only as a transitive dependency of
24
+ something else, and depending on a package nobody declared is how a working
25
+ install becomes a broken one after an unrelated upgrade.
26
+
27
+ The previous implementation used ``to_csv(sep="|")``, which is not Markdown:
28
+ it has no header separator row and no leading or trailing pipes, so it
29
+ rendered as one run-on paragraph rather than a table.
30
+
31
+ Args:
32
+ frame: Frame to render. The index becomes the first column when it is
33
+ named, since ``compute_ranking`` returns the oversampler there.
34
+ float_format: Format applied to floating-point cells. Raw repr leaks
35
+ values like ``0.21000000000000002`` into a document meant to be read.
36
+
37
+ Returns:
38
+ A Markdown table, or a note when the frame is empty.
39
+ """
40
+ if frame.empty:
41
+ return "_No results._"
42
+
43
+ display = frame.reset_index() if frame.index.name else frame.copy()
44
+
45
+ def render(value: Any) -> str:
46
+ if isinstance(value, float):
47
+ return float_format.format(value)
48
+ return str(value)
49
+
50
+ headers = [str(c) for c in display.columns]
51
+ rows = [[render(v) for v in row] for row in display.itertuples(index=False)]
52
+
53
+ widths = [
54
+ max(len(headers[i]), *(len(r[i]) for r in rows)) if rows else len(headers[i])
55
+ for i in range(len(headers))
56
+ ]
57
+
58
+ def line(cells: list[str]) -> str:
59
+ padded = [c.ljust(w) for c, w in zip(cells, widths, strict=True)]
60
+ return "| " + " | ".join(padded) + " |"
61
+
62
+ separator = "| " + " | ".join("-" * w for w in widths) + " |"
63
+ return "\n".join([line(headers), separator, *(line(r) for r in rows)])
64
+
65
+
66
+ def frame_to_html(frame: pd.DataFrame, *, float_format: str = "{:.4f}") -> str:
67
+ """Render a DataFrame as an HTML table.
68
+
69
+ Args:
70
+ frame: Frame to render. A named index becomes the first column.
71
+ float_format: Format applied to floating-point cells.
72
+
73
+ Returns:
74
+ An HTML table, or a note when the frame is empty.
75
+ """
76
+ if frame.empty:
77
+ return "<p><em>No results.</em></p>"
78
+ display = frame.reset_index() if frame.index.name else frame
79
+ # pandas ships no type information, so to_html is typed Any. str() makes
80
+ # the declared return type honest instead of suppressing the error.
81
+ return str(display.to_html(index=False, float_format=float_format.format))
oversampleqa/_rng.py ADDED
@@ -0,0 +1,83 @@
1
+ """Random-number normalisation shared by every validator.
2
+
3
+ Seeding behaviour lives here so it cannot drift between the validators again.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import numpy as np
9
+
10
+ __all__ = ["RandomStateLike", "as_generator", "spawn_generators"]
11
+
12
+
13
+ RandomStateLike = int | np.random.Generator | np.random.SeedSequence | None
14
+ """Anything accepted as a seed by the validators."""
15
+
16
+
17
+ def as_generator(random_state: RandomStateLike) -> np.random.Generator:
18
+ """Normalise a seed-like value to a :class:`numpy.random.Generator`.
19
+
20
+ Parameters
21
+ ----------
22
+ random_state : int, Generator, SeedSequence or None
23
+ ``None`` draws from fresh entropy, meaning results are not
24
+ reproducible. An ``int`` seeds a new generator. A ``Generator`` is
25
+ returned unchanged, so callers can thread one generator through a whole
26
+ pipeline and keep a single stream.
27
+
28
+ Returns
29
+ -------
30
+ numpy.random.Generator
31
+ """
32
+ if isinstance(random_state, np.random.Generator):
33
+ return random_state
34
+ if isinstance(random_state, np.random.SeedSequence):
35
+ return np.random.default_rng(random_state)
36
+ return np.random.default_rng(random_state)
37
+
38
+
39
+ def spawn_generators(
40
+ random_state: RandomStateLike, n: int
41
+ ) -> list[np.random.Generator]:
42
+ """Return ``n`` independent generators derived from ``random_state``.
43
+
44
+ Uses :class:`numpy.random.SeedSequence` spawning, which guarantees the
45
+ streams are statistically independent.
46
+
47
+ Deriving repeat seeds as ``seed + i`` would **not** be safe: consecutive
48
+ integer seeds produce correlated streams, so repeats built that way share
49
+ structure and the resulting dispersion is understated.
50
+
51
+ Parameters
52
+ ----------
53
+ random_state : int, Generator, SeedSequence or None
54
+ Parent seed.
55
+ n : int
56
+ Number of child generators.
57
+
58
+ Returns
59
+ -------
60
+ list of numpy.random.Generator
61
+ """
62
+ if n < 1:
63
+ raise ValueError(f"n must be at least 1; got {n}")
64
+
65
+ if isinstance(random_state, np.random.SeedSequence):
66
+ parent = random_state
67
+ elif isinstance(random_state, np.random.Generator):
68
+ # Draw a fresh entropy value from the supplied generator so repeated
69
+ # calls on the same generator do not replay the same children.
70
+ parent = np.random.SeedSequence(int(random_state.integers(0, 2**63 - 1)))
71
+ else:
72
+ parent = np.random.SeedSequence(random_state)
73
+
74
+ return [np.random.default_rng(child) for child in parent.spawn(n)]
75
+
76
+
77
+ def integer_seed(rng: np.random.Generator) -> int:
78
+ """Draw an integer seed from ``rng``.
79
+
80
+ For interoperability with scikit-learn estimators, which accept
81
+ ``int``/``RandomState`` but not ``Generator``.
82
+ """
83
+ return int(rng.integers(0, 2**31 - 1))