trusttab 0.1.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.
Files changed (42) hide show
  1. trusttab/__init__.py +5 -0
  2. trusttab/api.py +82 -0
  3. trusttab/calibration/__init__.py +0 -0
  4. trusttab/data/__init__.py +28 -0
  5. trusttab/data/adult.py +117 -0
  6. trusttab/data/german_credit.py +102 -0
  7. trusttab/data/heart_disease.py +229 -0
  8. trusttab/data/preprocessing.py +69 -0
  9. trusttab/evaluation/__init__.py +31 -0
  10. trusttab/evaluation/calibration.py +97 -0
  11. trusttab/evaluation/metrics.py +144 -0
  12. trusttab/evaluation/operating_region.py +85 -0
  13. trusttab/experiments/__init__.py +33 -0
  14. trusttab/experiments/benchmark.py +716 -0
  15. trusttab/experiments/calibration_protocol.py +67 -0
  16. trusttab/experiments/config.py +404 -0
  17. trusttab/experiments/feature_shift_runner.py +151 -0
  18. trusttab/experiments/resumable.py +372 -0
  19. trusttab/experiments/run.py +218 -0
  20. trusttab/experiments/runner.py +173 -0
  21. trusttab/experiments/seeding.py +33 -0
  22. trusttab/experiments/splitting.py +93 -0
  23. trusttab/experiments/tracking.py +556 -0
  24. trusttab/models/__init__.py +10 -0
  25. trusttab/models/model_factory.py +107 -0
  26. trusttab/models/tabpfn_adapter.py +45 -0
  27. trusttab/noise/__init__.py +17 -0
  28. trusttab/noise/feature_shift.py +359 -0
  29. trusttab/noise/label_noise.py +85 -0
  30. trusttab/reporting/__init__.py +5 -0
  31. trusttab/reporting/report.py +1106 -0
  32. trusttab/statistics/__init__.py +39 -0
  33. trusttab/statistics/comparison.py +444 -0
  34. trusttab/statistics/research.py +732 -0
  35. trusttab/visualization/__init__.py +19 -0
  36. trusttab/visualization/reliability.py +272 -0
  37. trusttab-0.1.0.dist-info/METADATA +332 -0
  38. trusttab-0.1.0.dist-info/RECORD +42 -0
  39. trusttab-0.1.0.dist-info/WHEEL +5 -0
  40. trusttab-0.1.0.dist-info/entry_points.txt +2 -0
  41. trusttab-0.1.0.dist-info/licenses/LICENSE +21 -0
  42. trusttab-0.1.0.dist-info/top_level.txt +1 -0
trusttab/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """TrustTab: Reliability-aware stress testing for tabular machine learning."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
trusttab/api.py ADDED
@@ -0,0 +1,82 @@
1
+ """Supported convenience API for common TrustTab workflows.
2
+
3
+ Specialized modules remain importable for advanced use. Names listed here are
4
+ the stable, curated entry points intended for applications and examples.
5
+ """
6
+
7
+ from trusttab.data import (
8
+ build_preprocessor,
9
+ load_adult,
10
+ load_german_credit,
11
+ load_heart_disease,
12
+ )
13
+ from trusttab.evaluation import (
14
+ adaptive_expected_calibration_error,
15
+ calibrate_classifier,
16
+ calibrate_fitted_classifier,
17
+ evaluate_classifier,
18
+ expected_calibration_error,
19
+ log_loss_score,
20
+ maximum_calibration_error,
21
+ )
22
+ from trusttab.experiments import (
23
+ load_experiment_config,
24
+ run_robustness_benchmark,
25
+ run_stress_test,
26
+ )
27
+ from trusttab.experiments.config import expand_experiment_conditions
28
+ from trusttab.models import SUPPORTED_MODELS, build_model
29
+ from trusttab.noise import (
30
+ apply_feature_shifts,
31
+ inject_symmetric_label_noise,
32
+ validate_feature_shifts,
33
+ )
34
+ from trusttab.reporting import generate_robustness_report
35
+ from trusttab.statistics import (
36
+ analyze_friedman_nemenyi,
37
+ calibration_wilcoxon_results,
38
+ compare_degradation_slopes,
39
+ descriptive_summary,
40
+ run_research_analysis,
41
+ )
42
+ from trusttab.visualization import (
43
+ plot_metric_comparison,
44
+ plot_probability_distribution,
45
+ plot_reliability_diagram,
46
+ plot_roc_curve,
47
+ save_figure,
48
+ )
49
+
50
+ __all__ = [
51
+ "SUPPORTED_MODELS",
52
+ "adaptive_expected_calibration_error",
53
+ "analyze_friedman_nemenyi",
54
+ "apply_feature_shifts",
55
+ "build_model",
56
+ "build_preprocessor",
57
+ "calibrate_classifier",
58
+ "calibrate_fitted_classifier",
59
+ "calibration_wilcoxon_results",
60
+ "compare_degradation_slopes",
61
+ "descriptive_summary",
62
+ "evaluate_classifier",
63
+ "expand_experiment_conditions",
64
+ "expected_calibration_error",
65
+ "generate_robustness_report",
66
+ "inject_symmetric_label_noise",
67
+ "load_adult",
68
+ "load_experiment_config",
69
+ "load_german_credit",
70
+ "load_heart_disease",
71
+ "log_loss_score",
72
+ "maximum_calibration_error",
73
+ "plot_metric_comparison",
74
+ "plot_probability_distribution",
75
+ "plot_reliability_diagram",
76
+ "plot_roc_curve",
77
+ "run_research_analysis",
78
+ "run_robustness_benchmark",
79
+ "run_stress_test",
80
+ "save_figure",
81
+ "validate_feature_shifts",
82
+ ]
File without changes
@@ -0,0 +1,28 @@
1
+ """Dataset loading utilities for TrustTab."""
2
+
3
+ from trusttab.data.preprocessing import build_preprocessor
4
+
5
+ from trusttab.data.german_credit import (
6
+ load_german_credit,
7
+ validate_german_credit_data,
8
+ )
9
+
10
+ from trusttab.data.adult import (
11
+ load_adult,
12
+ validate_adult_data,
13
+ )
14
+
15
+ from trusttab.data.heart_disease import (
16
+ load_heart_disease,
17
+ validate_heart_disease_data,
18
+ )
19
+
20
+ __all__ = [
21
+ "load_german_credit",
22
+ "validate_german_credit_data",
23
+ "load_adult",
24
+ "validate_adult_data",
25
+ "load_heart_disease",
26
+ "validate_heart_disease_data",
27
+ "build_preprocessor",
28
+ ]
trusttab/data/adult.py ADDED
@@ -0,0 +1,117 @@
1
+
2
+ """Adult Census Income dataset loading and validation."""
3
+
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ from sklearn.datasets import fetch_openml
8
+
9
+
10
+ OPENML_DATA_ID = 1590
11
+ EXPECTED_ROWS = 48842
12
+ EXPECTED_FEATURES = 14
13
+
14
+ TARGET_MAPPING = {
15
+ "<=50k": 0,
16
+ ">50k": 1,
17
+ }
18
+
19
+
20
+ def validate_adult_data(
21
+ features: pd.DataFrame,
22
+ target: pd.Series,
23
+ ) -> None:
24
+ """Validate the structure and contents of the Adult dataset."""
25
+
26
+ if not isinstance(features, pd.DataFrame):
27
+ raise TypeError("Features must be a pandas DataFrame.")
28
+
29
+ if not isinstance(target, pd.Series):
30
+ raise TypeError("Target must be a pandas Series.")
31
+
32
+ if features.empty:
33
+ raise ValueError("The feature dataset is empty.")
34
+
35
+ if len(features) != len(target):
36
+ raise ValueError("Features and target have different row counts.")
37
+
38
+ if features.shape[0] != EXPECTED_ROWS:
39
+ raise ValueError(
40
+ f"Expected {EXPECTED_ROWS} rows, got {features.shape[0]}."
41
+ )
42
+
43
+ if features.shape[1] != EXPECTED_FEATURES:
44
+ raise ValueError(
45
+ f"Expected {EXPECTED_FEATURES} features, "
46
+ f"got {features.shape[1]}."
47
+ )
48
+
49
+ if features.columns.has_duplicates:
50
+ raise ValueError("Feature names must be unique.")
51
+
52
+ if target.isnull().any():
53
+ raise ValueError("Target contains missing values.")
54
+
55
+ observed_labels = (
56
+ set(target.astype(str).str.strip().str.lower().unique())
57
+ )
58
+
59
+ if observed_labels != set(TARGET_MAPPING):
60
+ raise ValueError(
61
+ "Unexpected target labels. "
62
+ f"Expected {set(TARGET_MAPPING)}, got {observed_labels}."
63
+ )
64
+
65
+
66
+ def load_adult(
67
+ data_home: str | Path | None = None,
68
+ ) -> tuple[pd.DataFrame, pd.Series]:
69
+ """
70
+ Download and load the Adult Census Income dataset from OpenML.
71
+
72
+ Returns
73
+ -------
74
+ X:
75
+ DataFrame containing 14 input features. Missing feature
76
+ values are preserved for downstream preprocessing.
77
+ y:
78
+ Integer target Series: <=50K=0, >50K=1.
79
+ """
80
+
81
+ dataset = fetch_openml(
82
+ data_id=OPENML_DATA_ID,
83
+ as_frame=True,
84
+ data_home=data_home,
85
+ )
86
+
87
+ features = dataset.data.copy()
88
+ raw_target = dataset.target.copy()
89
+
90
+ # Convert question-mark missing-value markers to actual NaN values.
91
+ features = features.replace(
92
+ r"^\s*\?\s*$",
93
+ float("nan"),
94
+ regex=True,
95
+ )
96
+
97
+ # Remove surrounding whitespace from categorical values.
98
+ for column in features.select_dtypes(
99
+ include=["object", "category"]
100
+ ).columns:
101
+ features[column] = features[column].map(
102
+ lambda value: value.strip() if isinstance(value, str) else value
103
+ )
104
+
105
+ validate_adult_data(features, raw_target)
106
+
107
+ target = (
108
+ raw_target.astype(str)
109
+ .str.strip()
110
+ .str.lower()
111
+ .map(TARGET_MAPPING)
112
+ .astype("int64")
113
+ )
114
+
115
+ target.name = "target"
116
+
117
+ return features, target
@@ -0,0 +1,102 @@
1
+
2
+ """German Credit dataset loading and validation."""
3
+
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ from sklearn.datasets import fetch_openml
8
+
9
+
10
+ OPENML_DATA_ID = 31
11
+ EXPECTED_ROWS = 1000
12
+ EXPECTED_FEATURES = 20
13
+
14
+ TARGET_MAPPING = {
15
+ "good": 0,
16
+ "bad": 1,
17
+ }
18
+
19
+
20
+ def validate_german_credit_data(
21
+ features: pd.DataFrame,
22
+ target: pd.Series,
23
+ ) -> None:
24
+ """Validate the structure and contents of the German Credit dataset."""
25
+
26
+ if not isinstance(features, pd.DataFrame):
27
+ raise TypeError("Features must be a pandas DataFrame.")
28
+
29
+ if not isinstance(target, pd.Series):
30
+ raise TypeError("Target must be a pandas Series.")
31
+
32
+ if features.empty:
33
+ raise ValueError("The feature dataset is empty.")
34
+
35
+ if len(features) != len(target):
36
+ raise ValueError("Features and target have different row counts.")
37
+
38
+ if features.shape[0] != EXPECTED_ROWS:
39
+ raise ValueError(
40
+ f"Expected {EXPECTED_ROWS} rows, got {features.shape[0]}."
41
+ )
42
+
43
+ if features.shape[1] != EXPECTED_FEATURES:
44
+ raise ValueError(
45
+ f"Expected {EXPECTED_FEATURES} features, "
46
+ f"got {features.shape[1]}."
47
+ )
48
+
49
+ if features.columns.has_duplicates:
50
+ raise ValueError("Feature names must be unique.")
51
+
52
+ if features.isnull().any().any():
53
+ raise ValueError("Features contain missing values.")
54
+
55
+ if target.isnull().any():
56
+ raise ValueError("Target contains missing values.")
57
+
58
+ observed_labels = set(target.astype(str).str.strip().str.lower().unique())
59
+
60
+ if observed_labels != set(TARGET_MAPPING):
61
+ raise ValueError(
62
+ "Unexpected target labels. "
63
+ f"Expected {set(TARGET_MAPPING)}, got {observed_labels}."
64
+ )
65
+
66
+
67
+ def load_german_credit(
68
+ data_home: str | Path | None = None,
69
+ ) -> tuple[pd.DataFrame, pd.Series]:
70
+ """
71
+ Download and load the German Credit dataset from OpenML.
72
+
73
+ Returns
74
+ -------
75
+ X:
76
+ DataFrame containing 20 input features.
77
+ y:
78
+ Integer target Series: good=0, bad=1.
79
+ """
80
+
81
+ dataset = fetch_openml(
82
+ data_id=OPENML_DATA_ID,
83
+ as_frame=True,
84
+ data_home=data_home,
85
+ )
86
+
87
+ features = dataset.data.copy()
88
+ raw_target = dataset.target.copy()
89
+
90
+ validate_german_credit_data(features, raw_target)
91
+
92
+ target = (
93
+ raw_target.astype(str)
94
+ .str.strip()
95
+ .str.lower()
96
+ .map(TARGET_MAPPING)
97
+ .astype("int64")
98
+ )
99
+
100
+ target.name = "target"
101
+
102
+ return features, target
@@ -0,0 +1,229 @@
1
+ """UCI Heart Disease processed Cleveland dataset loader.
2
+
3
+ Source: UCI Machine Learning Repository, dataset 45, processed Cleveland
4
+ subset only. Citation: Janosi, A., Steinbrunn, W., Pfisterer, M., & Detrano,
5
+ R. (1989). *Heart Disease*. https://doi.org/10.24432/C52P4X. The dataset is
6
+ licensed CC BY 4.0; see https://creativecommons.org/licenses/by/4.0/.
7
+
8
+ The source ``num`` target is retained as ``y.attrs['original_num']`` before
9
+ binary mapping (0 -> 0, 1-4 -> 1). Feature missing values are preserved.
10
+ """
11
+
12
+ from io import BytesIO
13
+ from pathlib import Path
14
+ from urllib.request import urlopen
15
+ from zipfile import ZipFile
16
+
17
+ import numpy as np
18
+ import pandas as pd
19
+
20
+ UCI_DATA_ID = 45
21
+ DATASET_URL = "https://archive.ics.uci.edu/static/public/45/heart+disease.zip"
22
+ EXPECTED_ROWS = 303
23
+ EXPECTED_FEATURES = 13
24
+ CACHE_FILENAME = "heart_disease_uci45_cleveland.csv"
25
+
26
+ FEATURE_COLUMNS = (
27
+ "age",
28
+ "sex",
29
+ "cp",
30
+ "trestbps",
31
+ "chol",
32
+ "fbs",
33
+ "restecg",
34
+ "thalach",
35
+ "exang",
36
+ "oldpeak",
37
+ "slope",
38
+ "ca",
39
+ "thal",
40
+ )
41
+ TARGET_COLUMN = "num"
42
+ CATEGORICAL_FEATURES = (
43
+ "sex",
44
+ "cp",
45
+ "fbs",
46
+ "restecg",
47
+ "exang",
48
+ "slope",
49
+ "ca",
50
+ "thal",
51
+ )
52
+ MISSING_VALUE_FEATURES = ("ca", "thal")
53
+ TARGET_MAPPING = {0: 0, 1: 1, 2: 1, 3: 1, 4: 1}
54
+ EXPECTED_NUM_COUNTS = {0: 164, 1: 55, 2: 36, 3: 35, 4: 13}
55
+
56
+
57
+ def _read_processed_cleveland_archive(archive: bytes) -> pd.DataFrame:
58
+ """Read only UCI's processed Cleveland member from its dataset archive."""
59
+
60
+ with ZipFile(BytesIO(archive)) as zipped:
61
+ matches = [
62
+ name
63
+ for name in zipped.namelist()
64
+ if Path(name).name.lower() == "processed.cleveland.data"
65
+ ]
66
+ if len(matches) != 1:
67
+ raise ValueError(
68
+ "UCI dataset archive must contain exactly one "
69
+ "processed.cleveland.data file."
70
+ )
71
+ with zipped.open(matches[0]) as source_file:
72
+ frame = pd.read_csv(
73
+ source_file,
74
+ header=None,
75
+ names=[*FEATURE_COLUMNS, TARGET_COLUMN],
76
+ na_values=["?"],
77
+ skipinitialspace=True,
78
+ )
79
+ return frame
80
+
81
+
82
+ def _download_processed_cleveland() -> pd.DataFrame:
83
+ """Download UCI dataset 45 and parse only the processed Cleveland file."""
84
+
85
+ with urlopen(DATASET_URL, timeout=30) as response:
86
+ return _read_processed_cleveland_archive(response.read())
87
+
88
+
89
+ def validate_heart_disease_data(
90
+ features: pd.DataFrame,
91
+ original_num: pd.Series,
92
+ ) -> None:
93
+ """Validate processed Cleveland rows, schema, missingness, and ``num``.
94
+
95
+ The UCI source has missing values in ``ca`` and ``thal``. They are
96
+ intentionally preserved for downstream train-only imputation; rows are
97
+ never dropped and no imputation occurs in this loader.
98
+ """
99
+
100
+ if not isinstance(features, pd.DataFrame):
101
+ raise TypeError("Features must be a pandas DataFrame.")
102
+ if not isinstance(original_num, pd.Series):
103
+ raise TypeError("Original num target must be a pandas Series.")
104
+ if features.empty:
105
+ raise ValueError("The feature dataset is empty.")
106
+ if len(features) != len(original_num):
107
+ raise ValueError("Features and target have different row counts.")
108
+ if features.shape[0] != EXPECTED_ROWS:
109
+ raise ValueError(f"Expected {EXPECTED_ROWS} rows, got {features.shape[0]}.")
110
+ if features.shape[1] != EXPECTED_FEATURES:
111
+ raise ValueError(
112
+ f"Expected {EXPECTED_FEATURES} features, got {features.shape[1]}."
113
+ )
114
+ if tuple(features.columns) != FEATURE_COLUMNS:
115
+ raise ValueError("Feature columns do not match the processed Cleveland schema.")
116
+ if features.columns.has_duplicates:
117
+ raise ValueError("Feature names must be unique.")
118
+ if original_num.isna().any():
119
+ raise ValueError("Original num target contains missing values.")
120
+
121
+ raw_values = pd.to_numeric(original_num, errors="coerce").to_numpy(dtype=float)
122
+ if not np.isfinite(raw_values).all():
123
+ raise ValueError("Original num target must contain finite numeric values.")
124
+ if not np.equal(raw_values, np.floor(raw_values)).all():
125
+ raise ValueError("Original num target values must be integers 0 through 4.")
126
+ observed_targets = set(raw_values.astype(int))
127
+ if not observed_targets.issubset(TARGET_MAPPING):
128
+ raise ValueError(
129
+ "Unexpected original num target values. Expected a subset of "
130
+ f"{set(TARGET_MAPPING)}, got {observed_targets}."
131
+ )
132
+ observed_counts = pd.Series(raw_values.astype(int)).value_counts().sort_index()
133
+ if observed_counts.to_dict() != EXPECTED_NUM_COUNTS:
134
+ raise ValueError(
135
+ "Unexpected processed Cleveland num class distribution. Expected "
136
+ f"{EXPECTED_NUM_COUNTS}, got {observed_counts.to_dict()}."
137
+ )
138
+
139
+ allowed_missing = set(MISSING_VALUE_FEATURES)
140
+ missing_columns = set(features.columns[features.isna().any()])
141
+ if missing_columns.difference(allowed_missing):
142
+ raise ValueError(
143
+ "Unexpected missing feature values in columns: "
144
+ f"{sorted(missing_columns.difference(allowed_missing))}."
145
+ )
146
+
147
+ for column in features.columns:
148
+ converted = pd.to_numeric(features[column], errors="coerce")
149
+ invalid_values = features[column].notna() & converted.isna()
150
+ if invalid_values.any():
151
+ raise ValueError(f"Feature '{column}' contains non-numeric values.")
152
+ if not np.isfinite(converted.dropna().to_numpy(dtype=float)).all():
153
+ raise ValueError(f"Feature '{column}' contains non-finite values.")
154
+
155
+
156
+ def _normalize_features(features: pd.DataFrame) -> pd.DataFrame:
157
+ """Set stable numeric and categorical dtypes without filling missing data."""
158
+
159
+ normalized = features.loc[:, list(FEATURE_COLUMNS)].copy()
160
+ for column in FEATURE_COLUMNS:
161
+ normalized[column] = pd.to_numeric(normalized[column], errors="raise")
162
+ for column in CATEGORICAL_FEATURES:
163
+ codes = normalized[column].astype("Int64").astype(object)
164
+ normalized[column] = codes.where(codes.notna(), np.nan)
165
+ return normalized
166
+
167
+
168
+ def load_heart_disease(
169
+ data_home: str | Path | None = None,
170
+ ) -> tuple[pd.DataFrame, pd.Series]:
171
+ """Load UCI dataset 45's processed Cleveland subset.
172
+
173
+ Only ``processed.cleveland.data`` is read from the UCI archive; the
174
+ Cleveland, Hungarian, Switzerland, and VA datasets are never combined.
175
+ With ``data_home``, the original 13 feature columns and multiclass ``num``
176
+ are cached as CSV and reused on later calls. ``?`` source markers become
177
+ ``NaN``. The 303 rows are retained and feature missingness is left for the
178
+ experiment pipeline to impute after splitting.
179
+
180
+ The returned target is binary: original ``num`` 0 maps to 0 and values
181
+ 1, 2, 3, or 4 map to 1. The unmodified numeric target is also preserved in
182
+ ``y.attrs['original_num']``. UCI citation and attribution: Janosi et al.
183
+ (1989), *Heart Disease*, DOI: https://doi.org/10.24432/C52P4X, CC BY 4.0.
184
+
185
+ Parameters
186
+ ----------
187
+ data_home:
188
+ Optional cache directory. If omitted, the UCI archive is fetched for
189
+ each call and no local dataset file is written.
190
+
191
+ Returns
192
+ -------
193
+ X:
194
+ DataFrame with the 13 processed Cleveland features in documented
195
+ order. Categorical codes use object dtype with integer values; numeric
196
+ measurements remain numeric. Missing values are preserved as NaN.
197
+ y:
198
+ Integer binary target Series named ``target`` with original ``num``
199
+ available in its ``attrs`` metadata.
200
+ """
201
+
202
+ cache_path = Path(data_home) / CACHE_FILENAME if data_home is not None else None
203
+ if cache_path is not None and cache_path.exists():
204
+ raw_data = pd.read_csv(cache_path, na_values=["?"])
205
+ missing_columns = set([*FEATURE_COLUMNS, TARGET_COLUMN]).difference(
206
+ raw_data.columns
207
+ )
208
+ if missing_columns:
209
+ raise ValueError(
210
+ f"Cached Cleveland data is missing columns: {sorted(missing_columns)}."
211
+ )
212
+ features = raw_data.loc[:, list(FEATURE_COLUMNS)].copy()
213
+ original_num = raw_data[TARGET_COLUMN].copy()
214
+ else:
215
+ raw_data = _download_processed_cleveland()
216
+ if cache_path is not None:
217
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
218
+ raw_data.to_csv(cache_path, index=False)
219
+ features = raw_data.loc[:, list(FEATURE_COLUMNS)].copy()
220
+ original_num = raw_data[TARGET_COLUMN].copy()
221
+
222
+ validate_heart_disease_data(features, original_num)
223
+ normalized_features = _normalize_features(features)
224
+ numeric_num = pd.to_numeric(original_num, errors="raise").astype("int64")
225
+ target = numeric_num.map(TARGET_MAPPING).astype("int64")
226
+ target.name = "target"
227
+ target.attrs["original_num"] = numeric_num.copy()
228
+
229
+ return normalized_features, target
@@ -0,0 +1,69 @@
1
+ """Leakage-safe preprocessing utilities for tabular datasets."""
2
+
3
+ import pandas as pd
4
+ from sklearn.compose import ColumnTransformer
5
+ from sklearn.impute import SimpleImputer
6
+ from sklearn.pipeline import Pipeline
7
+ from sklearn.preprocessing import OneHotEncoder, StandardScaler
8
+
9
+
10
+ def build_preprocessor(
11
+ features: pd.DataFrame, *, scale_numeric: bool = True
12
+ ) -> ColumnTransformer:
13
+ """
14
+ Build an unfitted preprocessing transformer for tabular features.
15
+
16
+ Numeric columns:
17
+ Median imputation, optionally followed by standard scaling.
18
+
19
+ Categorical columns:
20
+ Most-frequent imputation followed by one-hot encoding.
21
+
22
+ The returned transformer must be fitted only on training data.
23
+ """
24
+
25
+ if not isinstance(features, pd.DataFrame):
26
+ raise TypeError("Features must be a pandas DataFrame.")
27
+
28
+ if features.empty:
29
+ raise ValueError("Features must not be empty.")
30
+
31
+ if features.columns.has_duplicates:
32
+ raise ValueError("Feature names must be unique.")
33
+
34
+ numeric_columns = features.select_dtypes(include=["number"]).columns.tolist()
35
+
36
+ categorical_columns = [
37
+ column for column in features.columns if column not in numeric_columns
38
+ ]
39
+
40
+ if not numeric_columns and not categorical_columns:
41
+ raise ValueError("No usable feature columns found.")
42
+
43
+ transformers = []
44
+
45
+ if numeric_columns:
46
+ numeric_steps = [("imputer", SimpleImputer(strategy="median"))]
47
+ if scale_numeric:
48
+ numeric_steps.append(("scaler", StandardScaler()))
49
+ numeric_pipeline = Pipeline(steps=numeric_steps)
50
+
51
+ transformers.append(("numeric", numeric_pipeline, numeric_columns))
52
+
53
+ if categorical_columns:
54
+ categorical_pipeline = Pipeline(
55
+ steps=[
56
+ ("imputer", SimpleImputer(strategy="most_frequent")),
57
+ (
58
+ "onehot",
59
+ OneHotEncoder(handle_unknown="ignore"),
60
+ ),
61
+ ]
62
+ )
63
+
64
+ transformers.append(("categorical", categorical_pipeline, categorical_columns))
65
+
66
+ return ColumnTransformer(
67
+ transformers=transformers,
68
+ remainder="drop",
69
+ )
@@ -0,0 +1,31 @@
1
+ """Evaluation utilities for TrustTab."""
2
+
3
+ from trusttab.evaluation.calibration import (
4
+ calibrate_classifier,
5
+ calibrate_fitted_classifier,
6
+ get_calibration_curve,
7
+ )
8
+ from trusttab.evaluation.metrics import (
9
+ adaptive_expected_calibration_error,
10
+ evaluate_classifier,
11
+ expected_calibration_error,
12
+ log_loss_score,
13
+ maximum_calibration_error,
14
+ )
15
+ from trusttab.evaluation.operating_region import (
16
+ OperatingPoint,
17
+ analyze_operating_region,
18
+ )
19
+
20
+ __all__ = [
21
+ "OperatingPoint",
22
+ "adaptive_expected_calibration_error",
23
+ "analyze_operating_region",
24
+ "calibrate_classifier",
25
+ "calibrate_fitted_classifier",
26
+ "evaluate_classifier",
27
+ "expected_calibration_error",
28
+ "get_calibration_curve",
29
+ "log_loss_score",
30
+ "maximum_calibration_error",
31
+ ]