ml-experiment-framework 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.
- __init__.py +3 -0
- data/__init__.py +6 -0
- data/loader.py +84 -0
- data/profiler.py +182 -0
- data/splitter.py +70 -0
- data/validator.py +106 -0
- detection/__init__.py +5 -0
- detection/decision_engine.py +355 -0
- detection/feature_types.py +284 -0
- detection/problem_type.py +125 -0
- detection/target.py +113 -0
- evaluation/__init__.py +5 -0
- evaluation/error_analysis.py +101 -0
- evaluation/evaluator.py +43 -0
- evaluation/metrics.py +121 -0
- evaluation/plots.py +73 -0
- experiments/__init__.py +5 -0
- experiments/reporter.py +131 -0
- experiments/runner.py +393 -0
- experiments/tracker.py +72 -0
- explainability/__init__.py +3 -0
- explainability/explainer.py +81 -0
- features/__init__.py +12 -0
- features/engineering.py +135 -0
- features/importance.py +45 -0
- features/selection.py +60 -0
- ml_experiment_framework-0.1.0.dist-info/METADATA +273 -0
- ml_experiment_framework-0.1.0.dist-info/RECORD +51 -0
- ml_experiment_framework-0.1.0.dist-info/WHEEL +5 -0
- ml_experiment_framework-0.1.0.dist-info/entry_points.txt +2 -0
- ml_experiment_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- ml_experiment_framework-0.1.0.dist-info/top_level.txt +12 -0
- models/__init__.py +3 -0
- models/registry.py +311 -0
- persistence/__init__.py +3 -0
- persistence/model_store.py +31 -0
- preprocessing/__init__.py +3 -0
- preprocessing/builder.py +158 -0
- preprocessing/categorical.py +39 -0
- preprocessing/numerical.py +33 -0
- preprocessing/target.py +80 -0
- preprocessing/transformers.py +61 -0
- training/__init__.py +13 -0
- training/baseline.py +54 -0
- training/cross_validation.py +141 -0
- training/trainer.py +72 -0
- training/tuning.py +129 -0
- utils/__init__.py +12 -0
- utils/config.py +60 -0
- utils/logging.py +34 -0
- utils/reproducibility.py +39 -0
__init__.py
ADDED
data/__init__.py
ADDED
data/loader.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Data loading with support for CSV and Parquet; extensible registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable, Dict, Optional, Union
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from src.utils.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("data.loader")
|
|
13
|
+
|
|
14
|
+
LoaderFn = Callable[[Path], pd.DataFrame]
|
|
15
|
+
|
|
16
|
+
_LOADERS: Dict[str, LoaderFn] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_loader(extension: str):
|
|
20
|
+
"""Decorator to register a file-format loader."""
|
|
21
|
+
|
|
22
|
+
def decorator(fn: LoaderFn):
|
|
23
|
+
_LOADERS[extension.lower().lstrip(".")] = fn
|
|
24
|
+
return fn
|
|
25
|
+
|
|
26
|
+
return decorator
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@register_loader("csv")
|
|
30
|
+
def _load_csv(path: Path) -> pd.DataFrame:
|
|
31
|
+
return pd.read_csv(path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@register_loader("parquet")
|
|
35
|
+
@register_loader("pq")
|
|
36
|
+
def _load_parquet(path: Path) -> pd.DataFrame:
|
|
37
|
+
return pd.read_parquet(path)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_dataset(
|
|
41
|
+
path: Union[str, Path],
|
|
42
|
+
**kwargs,
|
|
43
|
+
) -> pd.DataFrame:
|
|
44
|
+
"""
|
|
45
|
+
Load a tabular dataset from disk.
|
|
46
|
+
|
|
47
|
+
Supported formats (by extension): csv, parquet.
|
|
48
|
+
Additional loaders can be registered via @register_loader.
|
|
49
|
+
"""
|
|
50
|
+
path = Path(path)
|
|
51
|
+
if not path.exists():
|
|
52
|
+
raise FileNotFoundError(f"Dataset file not found: {path}")
|
|
53
|
+
if not path.is_file():
|
|
54
|
+
raise ValueError(f"Path is not a file: {path}")
|
|
55
|
+
|
|
56
|
+
ext = path.suffix.lower().lstrip(".")
|
|
57
|
+
if ext not in _LOADERS:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Unsupported file format '.{ext}'. "
|
|
60
|
+
f"Supported: {sorted(_LOADERS.keys())}. "
|
|
61
|
+
"Register a new loader with @register_loader if needed."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
logger.info(f"Loading dataset from {path}")
|
|
65
|
+
try:
|
|
66
|
+
df = _LOADERS[ext](path)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise RuntimeError(f"Failed to read dataset {path}: {e}") from e
|
|
69
|
+
|
|
70
|
+
if df is None or len(df) == 0:
|
|
71
|
+
raise ValueError(f"Dataset is empty: {path}")
|
|
72
|
+
|
|
73
|
+
logger.info(f"Dataset shape: {df.shape[0]} rows × {df.shape[1]} columns")
|
|
74
|
+
return df
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_train_test(
|
|
78
|
+
train_path: Union[str, Path],
|
|
79
|
+
test_path: Optional[Union[str, Path]] = None,
|
|
80
|
+
) -> tuple[pd.DataFrame, Optional[pd.DataFrame]]:
|
|
81
|
+
"""Load train and optional external test datasets."""
|
|
82
|
+
train = load_dataset(train_path)
|
|
83
|
+
test = load_dataset(test_path) if test_path else None
|
|
84
|
+
return train, test
|
data/profiler.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""EDA / profiling (exploratory only — no fit that affects final pipeline)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
from src.utils.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger("data.profiler")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _safe_skew(s: pd.Series) -> float:
|
|
17
|
+
try:
|
|
18
|
+
return float(s.dropna().skew())
|
|
19
|
+
except Exception:
|
|
20
|
+
return float("nan")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def profile_dataset(
|
|
24
|
+
df: pd.DataFrame,
|
|
25
|
+
target: Optional[str] = None,
|
|
26
|
+
feature_types: Optional[Dict[str, str]] = None,
|
|
27
|
+
artifacts_dir: Optional[Path] = None,
|
|
28
|
+
max_cat_levels: int = 20,
|
|
29
|
+
) -> Dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Produce a structured EDA profile.
|
|
32
|
+
|
|
33
|
+
Plots are optional (written under artifacts_dir/plots if provided).
|
|
34
|
+
This function must never fit transformers used by the final model.
|
|
35
|
+
"""
|
|
36
|
+
logger.info("Running EDA / profiling...")
|
|
37
|
+
profile: Dict[str, Any] = {
|
|
38
|
+
"overview": {
|
|
39
|
+
"n_rows": int(df.shape[0]),
|
|
40
|
+
"n_cols": int(df.shape[1]),
|
|
41
|
+
"memory_mb": float(df.memory_usage(deep=True).sum() / 1e6),
|
|
42
|
+
"dtypes": {c: str(t) for c, t in df.dtypes.items()},
|
|
43
|
+
},
|
|
44
|
+
"missing": {},
|
|
45
|
+
"numeric": {},
|
|
46
|
+
"categorical": {},
|
|
47
|
+
"target": {},
|
|
48
|
+
"duplicates": int(df.duplicated().sum()),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Missing
|
|
52
|
+
miss = df.isna().sum()
|
|
53
|
+
miss_pct = (miss / len(df) * 100).round(2)
|
|
54
|
+
profile["missing"] = {
|
|
55
|
+
c: {"count": int(miss[c]), "pct": float(miss_pct[c])}
|
|
56
|
+
for c in df.columns
|
|
57
|
+
if miss[c] > 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Infer rough types if not provided
|
|
61
|
+
if feature_types is None:
|
|
62
|
+
feature_types = {}
|
|
63
|
+
for c in df.columns:
|
|
64
|
+
if c == target:
|
|
65
|
+
continue
|
|
66
|
+
if pd.api.types.is_numeric_dtype(df[c]):
|
|
67
|
+
feature_types[c] = "numeric"
|
|
68
|
+
else:
|
|
69
|
+
feature_types[c] = "categorical"
|
|
70
|
+
|
|
71
|
+
numeric_cols = [c for c, t in feature_types.items() if t == "numeric" and c in df.columns]
|
|
72
|
+
cat_cols = [c for c, t in feature_types.items() if t in ("categorical", "boolean") and c in df.columns]
|
|
73
|
+
|
|
74
|
+
for c in numeric_cols:
|
|
75
|
+
s = df[c]
|
|
76
|
+
profile["numeric"][c] = {
|
|
77
|
+
"mean": float(s.mean()) if s.notna().any() else None,
|
|
78
|
+
"median": float(s.median()) if s.notna().any() else None,
|
|
79
|
+
"std": float(s.std()) if s.notna().any() else None,
|
|
80
|
+
"min": float(s.min()) if s.notna().any() else None,
|
|
81
|
+
"max": float(s.max()) if s.notna().any() else None,
|
|
82
|
+
"skew": _safe_skew(s),
|
|
83
|
+
"n_unique": int(s.nunique(dropna=True)),
|
|
84
|
+
"n_missing": int(s.isna().sum()),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for c in cat_cols:
|
|
88
|
+
s = df[c]
|
|
89
|
+
vc = s.value_counts(dropna=False).head(max_cat_levels)
|
|
90
|
+
profile["categorical"][c] = {
|
|
91
|
+
"n_unique": int(s.nunique(dropna=True)),
|
|
92
|
+
"n_missing": int(s.isna().sum()),
|
|
93
|
+
"top_values": {str(k): int(v) for k, v in vc.items()},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if target and target in df.columns:
|
|
97
|
+
y = df[target]
|
|
98
|
+
if pd.api.types.is_numeric_dtype(y) and y.nunique(dropna=True) > 20:
|
|
99
|
+
profile["target"] = {
|
|
100
|
+
"type": "regression",
|
|
101
|
+
"mean": float(y.mean()),
|
|
102
|
+
"median": float(y.median()),
|
|
103
|
+
"std": float(y.std()),
|
|
104
|
+
"min": float(y.min()),
|
|
105
|
+
"max": float(y.max()),
|
|
106
|
+
"skew": _safe_skew(y),
|
|
107
|
+
"n_missing": int(y.isna().sum()),
|
|
108
|
+
}
|
|
109
|
+
else:
|
|
110
|
+
vc = y.value_counts(dropna=False)
|
|
111
|
+
profile["target"] = {
|
|
112
|
+
"type": "classification",
|
|
113
|
+
"n_classes": int(y.nunique(dropna=True)),
|
|
114
|
+
"distribution": {str(k): int(v) for k, v in vc.items()},
|
|
115
|
+
"n_missing": int(y.isna().sum()),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Optional plots
|
|
119
|
+
if artifacts_dir is not None:
|
|
120
|
+
try:
|
|
121
|
+
_write_basic_plots(df, target, numeric_cols, cat_cols, Path(artifacts_dir))
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.warning(f"Could not write EDA plots: {e}")
|
|
124
|
+
|
|
125
|
+
logger.info(
|
|
126
|
+
f"EDA complete — numeric: {len(numeric_cols)}, categorical: {len(cat_cols)}, "
|
|
127
|
+
f"columns with missing: {len(profile['missing'])}"
|
|
128
|
+
)
|
|
129
|
+
return profile
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _write_basic_plots(
|
|
133
|
+
df: pd.DataFrame,
|
|
134
|
+
target: Optional[str],
|
|
135
|
+
numeric_cols: List[str],
|
|
136
|
+
cat_cols: List[str],
|
|
137
|
+
artifacts_dir: Path,
|
|
138
|
+
) -> None:
|
|
139
|
+
import matplotlib
|
|
140
|
+
matplotlib.use("Agg")
|
|
141
|
+
import matplotlib.pyplot as plt
|
|
142
|
+
import seaborn as sns
|
|
143
|
+
|
|
144
|
+
plot_dir = artifacts_dir / "plots"
|
|
145
|
+
plot_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
|
|
147
|
+
# Missing heatmap (sample columns if many)
|
|
148
|
+
miss_cols = [c for c in df.columns if df[c].isna().any()][:30]
|
|
149
|
+
if miss_cols:
|
|
150
|
+
fig, ax = plt.subplots(figsize=(10, 4))
|
|
151
|
+
sns.heatmap(df[miss_cols].isna(), cbar=False, ax=ax, yticklabels=False)
|
|
152
|
+
ax.set_title("Missing values (subset of columns)")
|
|
153
|
+
fig.tight_layout()
|
|
154
|
+
fig.savefig(plot_dir / "missing_heatmap.png", dpi=100)
|
|
155
|
+
plt.close(fig)
|
|
156
|
+
|
|
157
|
+
# Target distribution
|
|
158
|
+
if target and target in df.columns:
|
|
159
|
+
fig, ax = plt.subplots(figsize=(6, 4))
|
|
160
|
+
y = df[target]
|
|
161
|
+
if pd.api.types.is_numeric_dtype(y) and y.nunique() > 20:
|
|
162
|
+
ax.hist(y.dropna(), bins=40, edgecolor="black", alpha=0.7)
|
|
163
|
+
ax.set_title(f"Target distribution: {target}")
|
|
164
|
+
else:
|
|
165
|
+
y.value_counts().plot(kind="bar", ax=ax)
|
|
166
|
+
ax.set_title(f"Target class distribution: {target}")
|
|
167
|
+
fig.tight_layout()
|
|
168
|
+
fig.savefig(plot_dir / "target_distribution.png", dpi=100)
|
|
169
|
+
plt.close(fig)
|
|
170
|
+
|
|
171
|
+
# Numeric correlations (sample)
|
|
172
|
+
num = [c for c in numeric_cols if c != target][:15]
|
|
173
|
+
if len(num) >= 2:
|
|
174
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
175
|
+
corr = df[num].corr(numeric_only=True)
|
|
176
|
+
sns.heatmap(corr, ax=ax, cmap="RdBu_r", center=0, annot=False)
|
|
177
|
+
ax.set_title("Numeric feature correlations (subset)")
|
|
178
|
+
fig.tight_layout()
|
|
179
|
+
fig.savefig(plot_dir / "correlation_heatmap.png", dpi=100)
|
|
180
|
+
plt.close(fig)
|
|
181
|
+
|
|
182
|
+
logger.info(f"EDA plots saved to {plot_dir}")
|
data/splitter.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Train/test split — always before any learned preprocessing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from sklearn.model_selection import train_test_split
|
|
9
|
+
|
|
10
|
+
from src.utils.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("data.splitter")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def split_data(
|
|
16
|
+
df: pd.DataFrame,
|
|
17
|
+
target: str,
|
|
18
|
+
test_size: float = 0.2,
|
|
19
|
+
random_state: int = 42,
|
|
20
|
+
stratify: bool | str = "auto",
|
|
21
|
+
problem_type: Optional[str] = None,
|
|
22
|
+
) -> Tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]:
|
|
23
|
+
"""
|
|
24
|
+
Split into X_train, X_test, y_train, y_test.
|
|
25
|
+
|
|
26
|
+
Stratification is used for classification when appropriate.
|
|
27
|
+
"""
|
|
28
|
+
if target not in df.columns:
|
|
29
|
+
raise ValueError(f"Target '{target}' not in dataframe columns.")
|
|
30
|
+
|
|
31
|
+
# Drop rows with missing target
|
|
32
|
+
mask = df[target].notna()
|
|
33
|
+
n_drop = int((~mask).sum())
|
|
34
|
+
if n_drop > 0:
|
|
35
|
+
logger.warning(f"Dropping {n_drop} rows with missing target before split.")
|
|
36
|
+
df = df.loc[mask].copy()
|
|
37
|
+
|
|
38
|
+
y = df[target]
|
|
39
|
+
X = df.drop(columns=[target])
|
|
40
|
+
|
|
41
|
+
use_stratify = False
|
|
42
|
+
if stratify == "auto":
|
|
43
|
+
if problem_type and "classification" in problem_type:
|
|
44
|
+
# Stratify only if every class has enough samples
|
|
45
|
+
min_count = y.value_counts().min()
|
|
46
|
+
if min_count >= 2 and test_size * len(y) >= y.nunique():
|
|
47
|
+
use_stratify = True
|
|
48
|
+
else:
|
|
49
|
+
logger.warning(
|
|
50
|
+
"Stratification disabled: some classes have too few samples for stratified split."
|
|
51
|
+
)
|
|
52
|
+
use_stratify = use_stratify
|
|
53
|
+
elif stratify is True:
|
|
54
|
+
use_stratify = True
|
|
55
|
+
|
|
56
|
+
stratify_arr = y if use_stratify else None
|
|
57
|
+
|
|
58
|
+
X_train, X_test, y_train, y_test = train_test_split(
|
|
59
|
+
X,
|
|
60
|
+
y,
|
|
61
|
+
test_size=test_size,
|
|
62
|
+
random_state=random_state,
|
|
63
|
+
stratify=stratify_arr,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
logger.info(
|
|
67
|
+
f"Split — train: {len(X_train)} rows, test: {len(X_test)} rows "
|
|
68
|
+
f"(test_size={test_size}, stratify={use_stratify})"
|
|
69
|
+
)
|
|
70
|
+
return X_train, X_test, y_train, y_test
|
data/validator.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Dataset validation before any modeling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from src.utils.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("data.validator")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ValidationReport:
|
|
17
|
+
n_rows: int
|
|
18
|
+
n_cols: int
|
|
19
|
+
n_duplicates: int
|
|
20
|
+
target_present: bool
|
|
21
|
+
target_missing: int
|
|
22
|
+
issues: List[str] = field(default_factory=list)
|
|
23
|
+
warnings: List[str] = field(default_factory=list)
|
|
24
|
+
dtypes: Dict[str, str] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
27
|
+
return {
|
|
28
|
+
"n_rows": self.n_rows,
|
|
29
|
+
"n_cols": self.n_cols,
|
|
30
|
+
"n_duplicates": self.n_duplicates,
|
|
31
|
+
"target_present": self.target_present,
|
|
32
|
+
"target_missing": self.target_missing,
|
|
33
|
+
"issues": self.issues,
|
|
34
|
+
"warnings": self.warnings,
|
|
35
|
+
"dtypes": self.dtypes,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_dataset(
|
|
40
|
+
df: pd.DataFrame,
|
|
41
|
+
target: Optional[str] = None,
|
|
42
|
+
require_target: bool = True,
|
|
43
|
+
) -> ValidationReport:
|
|
44
|
+
"""
|
|
45
|
+
Validate basic integrity of a dataset.
|
|
46
|
+
|
|
47
|
+
Raises ValueError on critical issues (empty, missing target when required).
|
|
48
|
+
"""
|
|
49
|
+
issues: List[str] = []
|
|
50
|
+
warnings: List[str] = []
|
|
51
|
+
|
|
52
|
+
n_rows, n_cols = df.shape
|
|
53
|
+
if n_rows == 0:
|
|
54
|
+
raise ValueError("Dataset has zero rows.")
|
|
55
|
+
if n_cols == 0:
|
|
56
|
+
raise ValueError("Dataset has zero columns.")
|
|
57
|
+
|
|
58
|
+
n_duplicates = int(df.duplicated().sum())
|
|
59
|
+
if n_duplicates > 0:
|
|
60
|
+
warnings.append(f"Found {n_duplicates} duplicate rows ({100 * n_duplicates / n_rows:.1f}%).")
|
|
61
|
+
|
|
62
|
+
target_present = False
|
|
63
|
+
target_missing = 0
|
|
64
|
+
if target is not None:
|
|
65
|
+
if target not in df.columns:
|
|
66
|
+
if require_target:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Target column '{target}' was not found. "
|
|
69
|
+
f"Available columns: {list(df.columns)[:20]}{'...' if len(df.columns) > 20 else ''}"
|
|
70
|
+
)
|
|
71
|
+
issues.append(f"Target column '{target}' not found.")
|
|
72
|
+
else:
|
|
73
|
+
target_present = True
|
|
74
|
+
target_missing = int(df[target].isna().sum())
|
|
75
|
+
if target_missing > 0:
|
|
76
|
+
warnings.append(
|
|
77
|
+
f"Target '{target}' has {target_missing} missing values "
|
|
78
|
+
f"({100 * target_missing / n_rows:.1f}%). Rows with missing target will be dropped."
|
|
79
|
+
)
|
|
80
|
+
n_unique = df[target].nunique(dropna=True)
|
|
81
|
+
if n_unique < 2 and require_target:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
f"Target '{target}' has fewer than 2 unique values (n_unique={n_unique}). "
|
|
84
|
+
"Cannot train a supervised model."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
dtypes = {c: str(df[c].dtype) for c in df.columns}
|
|
88
|
+
|
|
89
|
+
report = ValidationReport(
|
|
90
|
+
n_rows=n_rows,
|
|
91
|
+
n_cols=n_cols,
|
|
92
|
+
n_duplicates=n_duplicates,
|
|
93
|
+
target_present=target_present,
|
|
94
|
+
target_missing=target_missing,
|
|
95
|
+
issues=issues,
|
|
96
|
+
warnings=warnings,
|
|
97
|
+
dtypes=dtypes,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
for w in warnings:
|
|
101
|
+
logger.warning(w)
|
|
102
|
+
for i in issues:
|
|
103
|
+
logger.error(i)
|
|
104
|
+
|
|
105
|
+
logger.info(f"Validation OK — {n_rows} rows, {n_cols} columns, {n_duplicates} duplicates")
|
|
106
|
+
return report
|