tanml 0.1.6__py3-none-any.whl → 0.1.7__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.

Potentially problematic release.


This version of tanml might be problematic. Click here for more details.

Files changed (49) hide show
  1. tanml/__init__.py +1 -1
  2. tanml/check_runners/cleaning_repro_runner.py +2 -2
  3. tanml/check_runners/correlation_runner.py +49 -12
  4. tanml/check_runners/explainability_runner.py +12 -22
  5. tanml/check_runners/logistic_stats_runner.py +196 -17
  6. tanml/check_runners/performance_runner.py +82 -26
  7. tanml/check_runners/raw_data_runner.py +29 -14
  8. tanml/check_runners/regression_metrics_runner.py +195 -0
  9. tanml/check_runners/stress_test_runner.py +23 -6
  10. tanml/check_runners/vif_runner.py +33 -27
  11. tanml/checks/correlation.py +241 -41
  12. tanml/checks/explainability/shap_check.py +261 -29
  13. tanml/checks/logit_stats.py +186 -54
  14. tanml/checks/performance_classification.py +305 -0
  15. tanml/checks/raw_data.py +58 -23
  16. tanml/checks/regression_metrics.py +167 -0
  17. tanml/checks/stress_test.py +157 -53
  18. tanml/cli/main.py +99 -27
  19. tanml/engine/check_agent_registry.py +20 -10
  20. tanml/engine/core_engine_agent.py +199 -37
  21. tanml/models/registry.py +329 -0
  22. tanml/report/report_builder.py +1180 -147
  23. tanml/report/templates/report_template_cls.docx +0 -0
  24. tanml/report/templates/report_template_reg.docx +0 -0
  25. tanml/ui/app.py +1205 -0
  26. tanml/utils/data_loader.py +105 -15
  27. tanml-0.1.7.dist-info/METADATA +164 -0
  28. tanml-0.1.7.dist-info/RECORD +54 -0
  29. tanml/cli/arg_parser.py +0 -31
  30. tanml/cli/init_cmd.py +0 -8
  31. tanml/cli/validate_cmd.py +0 -7
  32. tanml/config_templates/rules_multiple_models_datasets.yaml +0 -144
  33. tanml/config_templates/rules_one_dataset_segment_column.yaml +0 -140
  34. tanml/config_templates/rules_one_model_one_dataset.yaml +0 -143
  35. tanml/engine/segmentation_agent.py +0 -118
  36. tanml/engine/validation_agent.py +0 -91
  37. tanml/report/templates/report_template.docx +0 -0
  38. tanml/utils/model_loader.py +0 -35
  39. tanml/utils/r_loader.py +0 -30
  40. tanml/utils/sas_loader.py +0 -50
  41. tanml/utils/yaml_generator.py +0 -34
  42. tanml/utils/yaml_loader.py +0 -5
  43. tanml/validate.py +0 -209
  44. tanml-0.1.6.dist-info/METADATA +0 -317
  45. tanml-0.1.6.dist-info/RECORD +0 -62
  46. {tanml-0.1.6.dist-info → tanml-0.1.7.dist-info}/WHEEL +0 -0
  47. {tanml-0.1.6.dist-info → tanml-0.1.7.dist-info}/entry_points.txt +0 -0
  48. {tanml-0.1.6.dist-info → tanml-0.1.7.dist-info}/licenses/LICENSE +0 -0
  49. {tanml-0.1.6.dist-info → tanml-0.1.7.dist-info}/top_level.txt +0 -0
@@ -1,64 +1,168 @@
1
- from sklearn.metrics import roc_auc_score, accuracy_score
1
+ from __future__ import annotations
2
+ from typing import Any, Dict, List, Tuple
2
3
  import numpy as np
3
4
  import pandas as pd
5
+ from pandas.api.types import is_bool_dtype, is_numeric_dtype
6
+
7
+ from sklearn.metrics import (
8
+ accuracy_score, roc_auc_score,
9
+ mean_squared_error, r2_score
10
+ )
11
+
12
+ def _infer_task_type(model, y) -> str:
13
+ # 1) model hint
14
+ if hasattr(model, "_estimator_type"):
15
+ if model._estimator_type == "classifier":
16
+ return "classification"
17
+ if model._estimator_type == "regressor":
18
+ return "regression"
19
+ # 2) label-based fallback
20
+ try:
21
+ s = pd.Series(y).dropna()
22
+ if pd.api.types.is_numeric_dtype(s):
23
+ return "classification" if s.nunique() <= 10 else "regression"
24
+ return "classification"
25
+ except Exception:
26
+ return "classification"
27
+
28
+ def _scores_for_classification(model, X) -> np.ndarray:
29
+ # Prefer probabilities
30
+ if hasattr(model, "predict_proba"):
31
+ p = model.predict_proba(X)
32
+ return p[:, 1] if p.ndim == 2 and p.shape[1] > 1 else np.ravel(p)
33
+ # Fall back to decision scores
34
+ if hasattr(model, "decision_function"):
35
+ return np.ravel(model.decision_function(X))
36
+ # Last resort: hard predictions (will be used directly for acc; AUC may be NaN)
37
+ return np.ravel(model.predict(X))
38
+
39
+ def _bin_pred_from_score(score: np.ndarray) -> np.ndarray:
40
+ # If looks like probability in [0,1], threshold at 0.5; else at 0.0
41
+ if np.all(np.isfinite(score)):
42
+ smin, smax = float(np.min(score)), float(np.max(score))
43
+ if 0.0 <= smin <= 1.0 and 0.0 <= smax <= 1.0:
44
+ return (score >= 0.5).astype(int)
45
+ return (score >= 0.0).astype(int)
46
+ # fallback
47
+ return (score >= 0.5).astype(int)
48
+
49
+ def _cls_metrics(y_true, y_score, y_pred) -> Tuple[float, float]:
50
+ acc = float(accuracy_score(y_true, y_pred))
51
+ try:
52
+ auc = float(roc_auc_score(y_true, y_score)) if len(np.unique(y_true)) > 1 else np.nan
53
+ except Exception:
54
+ auc = np.nan
55
+ return acc, auc
56
+
57
+ def _reg_metrics(y_true, y_pred) -> Tuple[float, float]:
58
+ rmse = float(np.sqrt(mean_squared_error(y_true, y_pred)))
59
+ r2 = float(r2_score(y_true, y_pred))
60
+ return rmse, r2
4
61
 
5
62
  class StressTestCheck:
6
- def __init__(self, model, X, y, epsilon=0.01, perturb_fraction=0.2):
63
+ """
64
+ Task-aware stress test:
65
+ - Classification: accuracy, auc, delta_accuracy, delta_auc
66
+ - Regression: rmse, r2, delta_rmse, delta_r2
67
+
68
+ For each numeric feature, perturb a random subset of rows by (1 ± epsilon).
69
+ """
70
+
71
+ def __init__(self, model, X, y, epsilon: float = 0.01, perturb_fraction: float = 0.2, random_state: int = 42):
7
72
  self.model = model
8
- self.X = X.copy()
9
- self.y = y
10
- self.epsilon = epsilon
11
- self.perturb_fraction = perturb_fraction
73
+ self.X = pd.DataFrame(X, columns=getattr(X, "columns", None))
74
+ self.y = np.asarray(y)
75
+ self.epsilon = float(epsilon)
76
+ self.perturb_fraction = float(perturb_fraction)
77
+ self.rng = np.random.default_rng(int(random_state))
78
+
79
+ # 🔧 Cast ALL numeric columns to float once to avoid int64→float assignment warnings
80
+ num_cols = [c for c in self.X.columns if is_numeric_dtype(self.X[c]) and not is_bool_dtype(self.X[c])]
81
+ if num_cols:
82
+ self.X[num_cols] = self.X[num_cols].astype("float64")
83
+
84
+ def _numeric_cols(self) -> List[str]:
85
+ return [c for c in self.X.columns if is_numeric_dtype(self.X[c]) and not is_bool_dtype(self.X[c])]
86
+
87
+ def _perturb_scaled(self, X: pd.DataFrame, col: str, sign: int) -> pd.DataFrame:
88
+ """Scale a random subset of column 'col' by (1 + sign*epsilon)."""
89
+ Xp = X.copy(deep=True)
90
+ if Xp.empty:
91
+ return Xp
92
+ n = len(Xp)
93
+ k = max(1, int(self.perturb_fraction * n))
94
+ idx = self.rng.choice(Xp.index, size=k, replace=False)
95
+ factor = 1.0 + sign * self.epsilon
96
+
97
+ # Use a float numpy view for assignment — no dtype warnings
98
+ vals = Xp.loc[idx, col].to_numpy(dtype="float64", copy=False)
99
+ Xp.loc[idx, col] = vals * float(factor)
100
+ return Xp
12
101
 
13
102
  def run(self):
14
- np.random.seed(42)
15
- results = []
16
-
17
- # Compute baseline metrics
18
- try:
19
- base_proba = self.model.predict_proba(self.X)[:, 1]
20
- base_pred = (base_proba >= 0.5).astype(int)
21
- base_auc = roc_auc_score(self.y, base_proba)
22
- base_acc = accuracy_score(self.y, base_pred)
23
- except Exception as e:
24
- print(f"⚠️ Error computing baseline metrics: {e}")
25
- return []
26
-
27
- # Perturb each numeric feature
28
- for col in self.X.columns:
29
- if not pd.api.types.is_numeric_dtype(self.X[col]):
30
- continue # skip non-numeric features
103
+ task_type = _infer_task_type(self.model, self.y)
104
+ results: List[Dict[str, Any]] = []
31
105
 
106
+ # ---------- Baseline ----------
107
+ if task_type == "regression":
108
+ y_pred_base = np.ravel(self.model.predict(self.X))
109
+ rmse_base, r2_base = _reg_metrics(self.y, y_pred_base)
110
+ else:
111
+ y_score_base = _scores_for_classification(self.model, self.X)
112
+ # If scores are probs/decision, bin properly; else use model.predict
32
113
  try:
33
- n_perturb = int(self.perturb_fraction * len(self.X))
34
- idx = np.random.choice(self.X.index, size=n_perturb, replace=False)
35
-
36
- X_perturbed = self.X.copy()
37
- X_perturbed.loc[idx, col] += self.epsilon
38
-
39
- perturbed_proba = self.model.predict_proba(X_perturbed)[:, 1]
40
- perturbed_pred = (perturbed_proba >= 0.5).astype(int)
41
-
42
- pert_auc = roc_auc_score(self.y, perturbed_proba)
43
- pert_acc = accuracy_score(self.y, perturbed_pred)
44
-
45
- results.append({
46
- "feature": col,
47
- "perturbation": f"±{round(self.epsilon * 100, 2)}%",
48
- "accuracy": round(pert_acc, 4),
49
- "auc": round(pert_auc, 4),
50
- "delta_accuracy": round(pert_acc - base_acc, 4),
51
- "delta_auc": round(pert_auc - base_auc, 4),
52
- })
53
-
54
- except Exception as e:
55
- results.append({
56
- "feature": col,
57
- "perturbation": f"±{round(self.epsilon * 100, 2)}%",
58
- "accuracy": "error",
59
- "auc": "error",
60
- "delta_accuracy": f"Error: {e}",
61
- "delta_auc": f"Error: {e}",
62
- })
114
+ y_pred_base = _bin_pred_from_score(y_score_base)
115
+ except Exception:
116
+ y_pred_base = np.ravel(self.model.predict(self.X))
117
+ acc_base, auc_base = _cls_metrics(self.y, y_score_base, y_pred_base)
118
+
119
+ # ---------- Per-feature perturbations ----------
120
+ for col in self._numeric_cols():
121
+ for sign, lab in [(+1, f"+{round(self.epsilon * 100, 2)}%"),
122
+ (-1, f"-{round(self.epsilon * 100, 2)}%")]:
123
+ try:
124
+ Xp = self._perturb_scaled(self.X, col, sign)
125
+
126
+ if task_type == "regression":
127
+ y_pred_p = np.ravel(self.model.predict(Xp))
128
+ rmse_p, r2_p = _reg_metrics(self.y, y_pred_p)
129
+ results.append({
130
+ "feature": col,
131
+ "perturbation": lab,
132
+ "rmse": round(rmse_p, 4),
133
+ "r2": round(r2_p, 4),
134
+ "delta_rmse": round(rmse_p - rmse_base, 4),
135
+ "delta_r2": round(r2_p - r2_base, 4),
136
+ })
137
+ else:
138
+ y_score_p = _scores_for_classification(self.model, Xp)
139
+ try:
140
+ y_pred_p = _bin_pred_from_score(y_score_p)
141
+ except Exception:
142
+ y_pred_p = np.ravel(self.model.predict(Xp))
143
+ acc_p, auc_p = _cls_metrics(self.y, y_score_p, y_pred_p)
144
+ results.append({
145
+ "feature": col,
146
+ "perturbation": lab,
147
+ "accuracy": round(acc_p, 4),
148
+ "auc": round(auc_p, 4) if auc_p == auc_p else np.nan,
149
+ "delta_accuracy": round(acc_p - acc_base, 4),
150
+ "delta_auc": round((auc_p - auc_base), 4) if (auc_base == auc_base and auc_p == auc_p) else np.nan,
151
+ })
152
+
153
+ # Robust error row in either mode
154
+ except Exception as e:
155
+ if task_type == "regression":
156
+ results.append({
157
+ "feature": col, "perturbation": lab,
158
+ "rmse": "error", "r2": "error",
159
+ "delta_rmse": f"Error: {e}", "delta_r2": f"Error: {e}",
160
+ })
161
+ else:
162
+ results.append({
163
+ "feature": col, "perturbation": lab,
164
+ "accuracy": "error", "auc": "error",
165
+ "delta_accuracy": f"Error: {e}", "delta_auc": f"Error: {e}",
166
+ })
63
167
 
64
168
  return pd.DataFrame(results)
tanml/cli/main.py CHANGED
@@ -1,27 +1,99 @@
1
- import argparse
2
- from tanml.cli.validate_cmd import run_validate
3
- from tanml.cli.init_cmd import run_init
4
-
5
- def main():
6
- parser = argparse.ArgumentParser(prog="tanml")
7
- subparsers = parser.add_subparsers(dest="command")
8
-
9
- # tanml validate --rules path.yaml
10
- validate_parser = subparsers.add_parser("validate", help="Run model validation")
11
- validate_parser.add_argument("--rules", required=True, help="Path to rules/config YAML")
12
-
13
- # tanml init --scenario B
14
- init_parser = subparsers.add_parser("init", help="Initialize rules YAML template")
15
- init_parser.add_argument("--scenario", choices=["A", "B", "C"], required=True, help="Scenario type")
16
- init_parser.add_argument("--overwrite", action="store_true", help="Overwrite existing rules.yaml if it exists")
17
- init_parser.add_argument("--output", default="rules.yaml", help="Path where rules.yaml should be saved (default: rules.yaml)")
18
-
19
- args = parser.parse_args()
20
-
21
- if args.command == "validate":
22
- run_validate(args.rules)
23
- elif args.command == "init":
24
- run_init(args.scenario, dest_path=args.output, overwrite=args.overwrite)
25
-
26
- else:
27
- parser.print_help()
1
+ # tanml/cli/main.py
2
+ from __future__ import annotations
3
+ import sys, os, subprocess, argparse, importlib.util
4
+
5
+ def _parse_args(argv):
6
+ p = argparse.ArgumentParser(prog="tanml ui", add_help=False)
7
+ p.add_argument("--public", action="store_true", help="Bind on 0.0.0.0 for LAN access")
8
+ p.add_argument("--headless", action="store_true", help="Run without opening a browser")
9
+ p.add_argument("--port", type=int, help="Port to serve on (default 8501)")
10
+ p.add_argument("--max-mb", type=int, help="Max upload/message size in MB (default 1024)")
11
+ p.add_argument("--no-telemetry", action="store_true", help="Disable Streamlit usage stats")
12
+ p.add_argument("--address", type=str, help="Explicit bind address (overrides --public)")
13
+ p.add_argument("-h", "--help", action="store_true", help="Show help")
14
+ args, _ = p.parse_known_args(argv)
15
+ if args.help:
16
+ p.print_help()
17
+ sys.exit(0)
18
+ return args
19
+
20
+ def _env_bool(name, default=False):
21
+ v = os.environ.get(name)
22
+ if v is None:
23
+ return default
24
+ return str(v).strip().lower() in {"1", "true", "yes", "on"}
25
+
26
+ def _module_file(modname: str) -> str:
27
+ """Return module file path WITHOUT importing it (avoids early st.* calls)."""
28
+ spec = importlib.util.find_spec(modname)
29
+ if spec is None or not spec.origin:
30
+ print(f"Could not locate module: {modname}", file=sys.stderr)
31
+ sys.exit(1)
32
+ return os.path.abspath(spec.origin)
33
+
34
+ def _launch_ui(argv):
35
+ # ---- Resolve app path WITHOUT importing tanml.ui.app ----
36
+ app_path = _module_file("tanml.ui.app")
37
+
38
+ # ---- Resolve config: CLI > ENV > defaults ----
39
+ args = _parse_args(argv)
40
+
41
+ default_address = "127.0.0.1"
42
+ env_address = os.environ.get("TANML_SERVER_ADDRESS")
43
+ address = args.address or ("0.0.0.0" if args.public else (env_address or default_address))
44
+
45
+ default_headless = _env_bool("TANML_HEADLESS", False)
46
+ headless = args.headless or default_headless
47
+
48
+ default_port = int(os.environ.get("TANML_PORT", "8501"))
49
+ port = args.port if args.port is not None else default_port
50
+
51
+ default_max_mb = int(os.environ.get("TANML_MAX_MB", "1024"))
52
+ max_mb = args.max_mb if args.max_mb is not None else default_max_mb
53
+
54
+ default_no_telemetry = _env_bool("TANML_NO_TELEMETRY", True) # default OFF
55
+ no_telemetry = args.no_telemetry or default_no_telemetry
56
+
57
+ # ---- Environment for the child process (the Streamlit runner)
58
+ env = os.environ.copy()
59
+ env.setdefault("STREAMLIT_SERVER_MAX_UPLOAD_SIZE", str(max_mb))
60
+ env.setdefault("STREAMLIT_SERVER_MAX_MESSAGE_SIZE", str(max_mb))
61
+ if no_telemetry:
62
+ env["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
63
+
64
+ # Optional: reduce auto-reruns in production (cuts stale-media churn)
65
+ env.setdefault("STREAMLIT_SERVER_FILE_WATCHER_TYPE", "none")
66
+
67
+ # ---- Hand off to the official runner (prevents ScriptRunContext warnings)
68
+ cmd = [
69
+ sys.executable, "-m", "streamlit", "run", app_path,
70
+ f"--server.port={port}",
71
+ f"--server.address={address}",
72
+ ]
73
+ # Keep flags too (these mirror the env, fine to be redundant)
74
+ cmd += [
75
+ f"--server.maxUploadSize={max_mb}",
76
+ f"--server.maxMessageSize={max_mb}",
77
+ ]
78
+ if headless:
79
+ cmd.append("--server.headless=true")
80
+ if no_telemetry:
81
+ cmd.append("--browser.gatherUsageStats=false")
82
+
83
+ return subprocess.call(cmd, env=env)
84
+
85
+ def main():
86
+ argv = sys.argv[1:]
87
+ if not argv or argv[0] in {"-h", "--help"}:
88
+ print(
89
+ "Usage:\n"
90
+ " tanml ui [--public] [--headless] [--port N] [--max-mb N] [--no-telemetry]\n"
91
+ "Env vars:\n"
92
+ " TANML_SERVER_ADDRESS, TANML_HEADLESS, TANML_PORT, TANML_MAX_MB, TANML_NO_TELEMETRY\n"
93
+ )
94
+ sys.exit(0)
95
+ if argv[0] == "ui":
96
+ sys.exit(_launch_ui(argv[1:]))
97
+ else:
98
+ print(f"Unknown command: {argv[0]}\nTry: tanml ui --help")
99
+ sys.exit(2)
@@ -1,4 +1,4 @@
1
- from tanml.check_runners.performance_runner import run_performance_check
1
+ #from tanml.check_runners.performance_runner import run_performance_check
2
2
  from tanml.check_runners.data_quality_runner import run_data_quality_check
3
3
  from tanml.check_runners.stress_test_runner import run_stress_test_check
4
4
  from tanml.check_runners.input_cluster_runner import run_input_cluster_check
@@ -12,6 +12,10 @@ from tanml.check_runners.explainability_runner import run_shap_check
12
12
  from tanml.check_runners.vif_runner import VIFCheckRunner
13
13
  from tanml.check_runners.rule_engine_runner import RuleEngineCheckRunner
14
14
 
15
+ from tanml.check_runners.regression_metrics_runner import RegressionMetricsCheckRunner
16
+
17
+ from tanml.check_runners.performance_runner import PerformanceCheckRunner
18
+
15
19
 
16
20
  # Wrapper for InputClusterCheck to inject expected_features from model
17
21
  def input_cluster_wrapper(model, X_train, X_test, y_train, y_test, rule_config, cleaned_df, *args, **kwargs):
@@ -25,18 +29,24 @@ def input_cluster_wrapper(model, X_train, X_test, y_train, y_test, rule_config,
25
29
  )
26
30
 
27
31
  CHECK_RUNNER_REGISTRY = {
28
- "PerformanceCheck": run_performance_check,
32
+ #"PerformanceCheck": run_performance_check,
33
+ "RawDataCheck": run_raw_data_check,
29
34
  "DataQualityCheck": run_data_quality_check,
30
- "StressTestCheck": run_stress_test_check,
31
- "InputClusterCheck": input_cluster_wrapper,
35
+ "EDACheck": EDACheckRunner,
36
+ "CorrelationCheck": CorrelationCheckRunner,
37
+ "VIFCheck": VIFCheckRunner,
38
+ "InputClusterCheck": input_cluster_wrapper,
39
+ "ModelMetaCheck": ModelMetaCheckRunner,
40
+ "PerformanceCheck": PerformanceCheckRunner,
41
+ "RegressionMetricsCheck": RegressionMetricsCheckRunner,
32
42
  "LogisticStatsCheck": run_logistic_stats_check,
33
- "RawDataCheck": run_raw_data_check,
43
+ "StressTestCheck": run_stress_test_check,
44
+
45
+
34
46
  #"CleaningReproCheck": run_cleaning_repro_check,
35
- "ModelMetaCheck": ModelMetaCheckRunner,
36
- "CorrelationCheck": CorrelationCheckRunner,
37
- "EDACheck": EDACheckRunner,
47
+
38
48
  "SHAPCheck": run_shap_check,
39
- "VIFCheck": VIFCheckRunner,
40
- "RuleEngineCheck": RuleEngineCheckRunner,
49
+
50
+ "RuleEngineCheck": RuleEngineCheckRunner,
41
51
 
42
52
  }