wrapevofs 0.2.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.
wrapevofs/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """Public API for wrapevofs."""
2
+
3
+ from wrapevofs._version import (
4
+ ARTIFACT_SCHEMA_VERSION,
5
+ CONFIG_SCHEMA_VERSION,
6
+ __version__,
7
+ )
8
+
9
+ from wrapevofs.config import (
10
+ BorutaRFWrapperConfig,
11
+ FirstStageConfig,
12
+ GAConfig,
13
+ LockingConfig,
14
+ PipelineConfig,
15
+ PreprocessingConfig,
16
+ RFECVConfig,
17
+ ScoringConfig,
18
+ SplitConfig,
19
+ SVML1WrapperConfig,
20
+ XGBoostWrapperConfig,
21
+ )
22
+ from wrapevofs.pipeline import WrapEvoPipeline, PipelineResult, PreparedData
23
+ from wrapevofs.locking import (
24
+ LockingCandidate,
25
+ LockingResult,
26
+ lock_representative_run,
27
+ score_candidate_feature_sets,
28
+ )
29
+ from wrapevofs.preprocessing import TabularPreprocessor
30
+ from wrapevofs.validation import validate_locking_artifact_directory
31
+ from wrapevofs.selectors.genetic_rf import GASolution, GeneticRFResult, run_genetic_rf
32
+ from wrapevofs.split import parse_split_ratio, train_test_split_frame
33
+
34
+ __all__ = [
35
+ "ARTIFACT_SCHEMA_VERSION",
36
+ "BorutaRFWrapperConfig",
37
+ "CONFIG_SCHEMA_VERSION",
38
+ "FirstStageConfig",
39
+ "GAConfig",
40
+ "LockingConfig",
41
+ "LockingCandidate",
42
+ "LockingResult",
43
+ "GASolution",
44
+ "WrapEvoPipeline",
45
+ "GeneticRFResult",
46
+ "PipelineConfig",
47
+ "PipelineResult",
48
+ "PreparedData",
49
+ "PreprocessingConfig",
50
+ "RFECVConfig",
51
+ "ScoringConfig",
52
+ "SVML1WrapperConfig",
53
+ "SplitConfig",
54
+ "TabularPreprocessor",
55
+ "XGBoostWrapperConfig",
56
+ "parse_split_ratio",
57
+ "train_test_split_frame",
58
+ "validate_locking_artifact_directory",
59
+ "run_genetic_rf",
60
+ "lock_representative_run",
61
+ "score_candidate_feature_sets",
62
+ "__version__",
63
+ ]
wrapevofs/_scoring.py ADDED
@@ -0,0 +1,35 @@
1
+ """Scoring helpers shared by wrapper and RFECV selectors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+
9
+
10
+ def unique_classes(y: Any) -> np.ndarray:
11
+ """Return sorted unique class labels from a pandas or numpy target vector."""
12
+
13
+ values = y.to_numpy() if hasattr(y, "to_numpy") else y
14
+ return np.unique(np.asarray(values))
15
+
16
+
17
+ def infer_problem_type(y: Any) -> str:
18
+ """Infer whether a classification target is binary or multiclass."""
19
+
20
+ classes = unique_classes(y)
21
+ return "binary" if len(classes) <= 2 else "multiclass"
22
+
23
+
24
+ def resolve_auto_scoring(
25
+ scoring: str,
26
+ y: Any,
27
+ *,
28
+ binary: str,
29
+ multiclass: str,
30
+ ) -> str:
31
+ """Resolve ``scoring='auto'`` from the observed number of target classes."""
32
+
33
+ if scoring != "auto":
34
+ return scoring
35
+ return binary if infer_problem_type(y) == "binary" else multiclass
wrapevofs/_version.py ADDED
@@ -0,0 +1,5 @@
1
+ """Single-source package and artifact version constants."""
2
+
3
+ __version__ = "0.2.0"
4
+ ARTIFACT_SCHEMA_VERSION = "2.1"
5
+ CONFIG_SCHEMA_VERSION = "1.0"
wrapevofs/artifacts.py ADDED
@@ -0,0 +1,193 @@
1
+ """Artifact writing helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, is_dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+
13
+ from wrapevofs.pipeline import PipelineResult, PreparedData
14
+ from wrapevofs._version import ARTIFACT_SCHEMA_VERSION, __version__
15
+
16
+
17
+ def _json_default(value: Any) -> Any:
18
+ if is_dataclass(value):
19
+ return asdict(value)
20
+ if hasattr(value, "tolist"):
21
+ return value.tolist()
22
+ return str(value)
23
+
24
+
25
+ def save_prepared_data(prepared: PreparedData, output_dir: str | Path) -> None:
26
+ output = Path(output_dir)
27
+ output.mkdir(parents=True, exist_ok=True)
28
+ prepared.X_train.to_csv(output / "X_train.csv", index=False)
29
+ prepared.X_test.to_csv(output / "X_test.csv", index=False)
30
+ prepared.y_train.to_frame("target").to_csv(output / "y_train.csv", index=False)
31
+ prepared.y_test.to_frame("target").to_csv(output / "y_test.csv", index=False)
32
+ with (output / "preprocessing_report.json").open("w", encoding="utf-8") as handle:
33
+ json.dump(asdict(prepared.preprocessor.report), handle, indent=2)
34
+
35
+
36
+ def save_pipeline_result(result: PipelineResult, output_dir: str | Path) -> None:
37
+ output = Path(output_dir)
38
+ output.mkdir(parents=True, exist_ok=True)
39
+ with (output / "software_metadata.json").open("w", encoding="utf-8") as handle:
40
+ json.dump(
41
+ {
42
+ "software_name": "wrapevofs",
43
+ "software_version": __version__,
44
+ "artifact_schema_version": ARTIFACT_SCHEMA_VERSION,
45
+ },
46
+ handle,
47
+ indent=2,
48
+ )
49
+ save_prepared_data(result.prepared, output / "preprocessed")
50
+ for name, selection in result.first_stage.items():
51
+ method_dir = output / "first_stage" / name
52
+ method_dir.mkdir(parents=True, exist_ok=True)
53
+ selection.feature_table.to_csv(method_dir / "feature_table.csv", index=False)
54
+ pd.DataFrame({"feature": selection.selected_features}).to_csv(
55
+ method_dir / "selected_features.csv",
56
+ index=False,
57
+ )
58
+ np.save(
59
+ method_dir / "selected_features.npy",
60
+ np.asarray(selection.selected_features, dtype=str),
61
+ )
62
+ with (method_dir / "metadata.json").open("w", encoding="utf-8") as handle:
63
+ json.dump(selection.metadata, handle, indent=2, default=_json_default)
64
+
65
+ for name, target in result.rfecv_targets.items():
66
+ method_dir = output / "rfecv" / name
67
+ method_dir.mkdir(parents=True, exist_ok=True)
68
+ target.score_table.to_csv(method_dir / "score_table.csv", index=False)
69
+ summary = {
70
+ "target_k": target.target_k,
71
+ "target_score": target.target_score,
72
+ "global_best_k": target.global_best_k,
73
+ "global_best_score": target.global_best_score,
74
+ "selected_features_at_target": target.selected_features_at_target,
75
+ "metadata": target.metadata,
76
+ }
77
+ np.save(
78
+ method_dir / "target_features.npy",
79
+ np.asarray(target.selected_features_at_target, dtype=str),
80
+ )
81
+ np.save(
82
+ method_dir / "global_best_features.npy",
83
+ np.asarray(target.selected_features_at_global_best, dtype=str),
84
+ )
85
+ with (method_dir / "summary.json").open("w", encoding="utf-8") as handle:
86
+ json.dump(summary, handle, indent=2, default=_json_default)
87
+
88
+ final_feature_sets: dict[str, list[Any]] = {}
89
+ best_feature_sets: dict[str, list[str]] = {}
90
+ for name, ga_result in result.ga_results.items():
91
+ method_dir = output / "ga" / name
92
+ method_dir.mkdir(parents=True, exist_ok=True)
93
+ rows = []
94
+ for solution in ga_result.top_solutions:
95
+ rows.append(
96
+ {
97
+ "rank": solution.rank,
98
+ "run_id": solution.run_id,
99
+ "score": solution.score,
100
+ "base_score": solution.base_score,
101
+ "n_features": solution.n_features,
102
+ "selected_features": "|".join(solution.selected_features),
103
+ "raw_objective": solution.raw_objective,
104
+ "legacy_truncated_fitness": solution.legacy_truncated_fitness,
105
+ "target_deviation": solution.target_deviation,
106
+ "penalty_amount": solution.penalty_amount,
107
+ "stable_mask_hash": solution.stable_mask_hash,
108
+ }
109
+ )
110
+ pd.DataFrame(rows).to_csv(method_dir / "top_solutions.csv", index=False)
111
+ if not ga_result.history.empty:
112
+ ga_result.history.to_csv(method_dir / "history.csv", index=False)
113
+ top_feature_sets = np.asarray(
114
+ [np.asarray(item.selected_features, dtype=str) for item in ga_result.top_solutions],
115
+ dtype=object,
116
+ )
117
+ top_masks = np.asarray([item.mask for item in ga_result.top_solutions], dtype=bool)
118
+ np.save(method_dir / "top_feature_sets.npy", top_feature_sets, allow_pickle=True)
119
+ np.save(method_dir / "top_masks.npy", top_masks)
120
+ np.save(
121
+ method_dir / "scores.npy",
122
+ np.asarray([item.score for item in ga_result.top_solutions], dtype=float),
123
+ )
124
+ if ga_result.top_solutions:
125
+ np.save(
126
+ method_dir / "best_feature_set.npy",
127
+ np.asarray(ga_result.best_solution.selected_features, dtype=str),
128
+ )
129
+ np.save(method_dir / "best_mask.npy", ga_result.best_solution.mask)
130
+ best_feature_sets[name] = ga_result.best_solution.selected_features
131
+ final_feature_sets[name] = [
132
+ solution.selected_features for solution in ga_result.top_solutions
133
+ ]
134
+ summary = {
135
+ "name": ga_result.name,
136
+ "target_k": ga_result.target_k,
137
+ "best_score": ga_result.best_solution.score if ga_result.top_solutions else None,
138
+ "best_n_features": (
139
+ ga_result.best_solution.n_features if ga_result.top_solutions else None
140
+ ),
141
+ "metadata": ga_result.metadata,
142
+ "warnings": ga_result.warnings,
143
+ }
144
+ with (method_dir / "summary.json").open("w", encoding="utf-8") as handle:
145
+ json.dump(summary, handle, indent=2, default=_json_default)
146
+
147
+ locked_feature_sets: dict[str, list[str]] = {}
148
+ for name, locking_result in result.locking_results.items():
149
+ method_dir = output / "locking" / name
150
+ method_dir.mkdir(parents=True, exist_ok=True)
151
+ locking_result.candidate_audit.to_csv(
152
+ method_dir / "locking_candidate_audit.csv",
153
+ index=False,
154
+ )
155
+ locking_result.pairwise_jaccard.to_csv(
156
+ method_dir / "pairwise_jaccard.csv",
157
+ index=False,
158
+ )
159
+ pd.DataFrame({"feature": locking_result.selected_features}).to_csv(
160
+ method_dir / "selected_features.csv",
161
+ index=False,
162
+ )
163
+ np.save(
164
+ method_dir / "selected_features.npy",
165
+ np.asarray(locking_result.selected_features, dtype=str),
166
+ )
167
+ with (method_dir / "summary.json").open("w", encoding="utf-8") as handle:
168
+ json.dump(locking_result.metadata, handle, indent=2, default=_json_default)
169
+ locked_feature_sets[name] = list(locking_result.selected_features)
170
+
171
+ if locked_feature_sets:
172
+ final_feature_sets = locked_feature_sets
173
+ np.save(output / "locked_feature_sets.npy", locked_feature_sets, allow_pickle=True)
174
+ elif not final_feature_sets:
175
+ if result.rfecv_targets:
176
+ final_feature_sets = {
177
+ name: target.selected_features_at_target
178
+ for name, target in result.rfecv_targets.items()
179
+ }
180
+ else:
181
+ final_feature_sets = {
182
+ name: selection.selected_features
183
+ for name, selection in result.first_stage.items()
184
+ }
185
+ if best_feature_sets:
186
+ np.save(output / "best_feature_sets.npy", best_feature_sets, allow_pickle=True)
187
+ np.save(output / "final_feature_sets.npy", final_feature_sets, allow_pickle=True)
188
+
189
+ exported_warnings = list(result.warnings)
190
+ for name, ga_result in result.ga_results.items():
191
+ exported_warnings.extend(f"{name}: {message}" for message in ga_result.warnings)
192
+ with (output / "warnings.json").open("w", encoding="utf-8") as handle:
193
+ json.dump(exported_warnings, handle, indent=2)
wrapevofs/cli.py ADDED
@@ -0,0 +1,158 @@
1
+ """Command-line entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ import pandas as pd
9
+
10
+ from wrapevofs.artifacts import save_pipeline_result
11
+ from wrapevofs.config import PipelineConfig
12
+ from wrapevofs.pipeline import WrapEvoPipeline
13
+ from wrapevofs._version import __version__
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(prog="wrapevofs")
18
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
19
+ subparsers = parser.add_subparsers(dest="command", required=True)
20
+
21
+ run = subparsers.add_parser("run", help="Run preprocessing through RFECV target discovery.")
22
+ run.add_argument("--csv", required=True, help="Input CSV containing features and target.")
23
+ run.add_argument("--target", required=True, help="Target column name.")
24
+ run.add_argument("--out", required=True, help="Output directory.")
25
+ run.add_argument("--config", help="YAML config path.")
26
+ run.add_argument("--ratio", help="Override split ratio, for example 7:3, 6:4, 8:2.")
27
+ run.add_argument("--impute", choices=["median", "mean", "zero"], help="Override imputation strategy.")
28
+ run.add_argument(
29
+ "--rfecv-max-features",
30
+ type=int,
31
+ help="Override RFECV compact cap for all wrapper branches.",
32
+ )
33
+ run.add_argument("--xgb-rfecv-max", type=int, help="RFECV compact cap for the xgboost branch.")
34
+ run.add_argument("--svm-rfecv-max", type=int, help="RFECV compact cap for the svm_l1 branch.")
35
+ run.add_argument("--boruta-rfecv-max", type=int, help="RFECV compact cap for the boruta_rf branch.")
36
+ run.add_argument("--methods", help="Comma-separated methods: svm_l1,xgboost,boruta_rf.")
37
+ run.add_argument("--drop-columns", help="Comma-separated non-feature columns to drop.")
38
+ run.add_argument(
39
+ "--ga-backend",
40
+ choices=["auto", "cpu", "gpu"],
41
+ help="Override GA Random Forest evaluator backend.",
42
+ )
43
+ run.add_argument(
44
+ "--ga-fitness-mode",
45
+ choices=["legacy_zero_truncated_linear", "untruncated_shifted_linear"],
46
+ help="GA ranking objective; archived configurations use the legacy mode.",
47
+ )
48
+ run.add_argument(
49
+ "--locking-strategy",
50
+ choices=["top_k_jaccard_medoid", "regret_constrained_medoid"],
51
+ help="Enable development-only representative-run locking with this strategy.",
52
+ )
53
+ run.add_argument(
54
+ "--locking-tolerance-mode",
55
+ choices=["absolute", "relative", "best_run_se_scaled", "one_se"],
56
+ help="Regret eligibility mode for regret-constrained locking.",
57
+ )
58
+ run.add_argument("--regret-tolerance", type=float, help="Development-CV regret tolerance.")
59
+ run.add_argument(
60
+ "--minimum-pool-size",
61
+ type=int,
62
+ help="Compatibility option; strict regret-constrained locking requires 1.",
63
+ )
64
+ run.add_argument(
65
+ "--unified-metric",
66
+ choices=["accuracy", "balanced_accuracy", "roc_auc", "macro_ovr_auroc"],
67
+ help="Apply one compatible metric across RFECV, GA, and locking.",
68
+ )
69
+ run.add_argument("--run-ga", action="store_true", help="Continue from RFECV target discovery into GA-RF.")
70
+ run.add_argument(
71
+ "--resume",
72
+ action="store_true",
73
+ help=(
74
+ "Resume GA state from each branch checkpoint directory. Fails on "
75
+ "missing, corrupt, version-, configuration-, feature-, or input-mismatched state."
76
+ ),
77
+ )
78
+ return parser
79
+
80
+
81
+ def main(argv: list[str] | None = None) -> int:
82
+ args = build_parser().parse_args(argv)
83
+ if args.command == "run":
84
+ config = PipelineConfig.from_yaml(args.config) if args.config else PipelineConfig()
85
+ if args.ratio:
86
+ config.split.ratio = args.ratio
87
+ if args.impute:
88
+ config.preprocessing.impute_strategy = args.impute
89
+ if args.rfecv_max_features is not None:
90
+ config.rfecv.max_features_to_consider = args.rfecv_max_features
91
+ for method in config.rfecv.method_max_features_to_consider:
92
+ config.rfecv.method_max_features_to_consider[method] = args.rfecv_max_features
93
+ if args.xgb_rfecv_max is not None:
94
+ config.rfecv.method_max_features_to_consider["xgboost"] = args.xgb_rfecv_max
95
+ if args.svm_rfecv_max is not None:
96
+ config.rfecv.method_max_features_to_consider["svm_l1"] = args.svm_rfecv_max
97
+ if args.boruta_rfecv_max is not None:
98
+ config.rfecv.method_max_features_to_consider["boruta_rf"] = args.boruta_rfecv_max
99
+ if args.ga_backend:
100
+ config.ga.backend = args.ga_backend
101
+ if args.ga_fitness_mode:
102
+ config.ga.fitness_mode = args.ga_fitness_mode
103
+ if args.locking_strategy:
104
+ config.locking.enabled = True
105
+ config.locking.strategy = args.locking_strategy
106
+ if args.locking_tolerance_mode:
107
+ config.locking.tolerance_mode = args.locking_tolerance_mode
108
+ if args.regret_tolerance is not None:
109
+ config.locking.regret_tolerance = args.regret_tolerance
110
+ if args.minimum_pool_size is not None:
111
+ config.locking.minimum_pool_size = args.minimum_pool_size
112
+ if args.unified_metric:
113
+ config.scoring.unified_metric = args.unified_metric
114
+ if args.resume:
115
+ if not args.run_ga:
116
+ raise SystemExit("--resume requires --run-ga.")
117
+ config.ga.resume_from_checkpoint = True
118
+ methods = args.methods.split(",") if args.methods else None
119
+ drop_columns = args.drop_columns.split(",") if args.drop_columns else None
120
+
121
+ df = pd.read_csv(args.csv)
122
+ output_dir = Path(args.out)
123
+ if args.run_ga:
124
+ config.ga.checkpoint_dir = str(output_dir)
125
+ pipeline = WrapEvoPipeline(config)
126
+ if args.run_ga:
127
+ result = pipeline.run_until_rfecv(
128
+ df=df,
129
+ target_column=args.target,
130
+ drop_columns=drop_columns,
131
+ methods=methods,
132
+ )
133
+ save_pipeline_result(result, output_dir)
134
+ result.ga_results = pipeline.run_ga(
135
+ result.prepared,
136
+ result.first_stage,
137
+ result.rfecv_targets,
138
+ )
139
+ result.locking_results = pipeline.run_locking(
140
+ result.prepared,
141
+ result.first_stage,
142
+ result.ga_results,
143
+ )
144
+ result.warnings = list(pipeline.warnings)
145
+ else:
146
+ result = pipeline.run_until_rfecv(
147
+ df=df,
148
+ target_column=args.target,
149
+ drop_columns=drop_columns,
150
+ methods=methods,
151
+ )
152
+ save_pipeline_result(result, output_dir)
153
+ return 0
154
+ return 1
155
+
156
+
157
+ if __name__ == "__main__":
158
+ raise SystemExit(main())