ins-pricing 0.5.0__py3-none-any.whl → 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.
Files changed (43) hide show
  1. ins_pricing/cli/BayesOpt_entry.py +15 -5
  2. ins_pricing/cli/BayesOpt_incremental.py +43 -10
  3. ins_pricing/cli/Explain_Run.py +16 -5
  4. ins_pricing/cli/Explain_entry.py +29 -8
  5. ins_pricing/cli/Pricing_Run.py +16 -5
  6. ins_pricing/cli/bayesopt_entry_runner.py +45 -12
  7. ins_pricing/cli/utils/bootstrap.py +23 -0
  8. ins_pricing/cli/utils/cli_config.py +34 -15
  9. ins_pricing/cli/utils/import_resolver.py +14 -14
  10. ins_pricing/cli/utils/notebook_utils.py +120 -106
  11. ins_pricing/cli/watchdog_run.py +15 -5
  12. ins_pricing/frontend/app.py +132 -61
  13. ins_pricing/frontend/config_builder.py +33 -0
  14. ins_pricing/frontend/example_config.json +11 -0
  15. ins_pricing/frontend/runner.py +340 -388
  16. ins_pricing/modelling/README.md +1 -1
  17. ins_pricing/modelling/bayesopt/README.md +29 -11
  18. ins_pricing/modelling/bayesopt/config_components.py +12 -0
  19. ins_pricing/modelling/bayesopt/config_preprocess.py +50 -13
  20. ins_pricing/modelling/bayesopt/core.py +47 -19
  21. ins_pricing/modelling/bayesopt/model_plotting_mixin.py +20 -14
  22. ins_pricing/modelling/bayesopt/models/model_ft_components.py +349 -342
  23. ins_pricing/modelling/bayesopt/models/model_ft_trainer.py +11 -5
  24. ins_pricing/modelling/bayesopt/models/model_gnn.py +20 -14
  25. ins_pricing/modelling/bayesopt/models/model_resn.py +9 -3
  26. ins_pricing/modelling/bayesopt/trainers/trainer_base.py +62 -50
  27. ins_pricing/modelling/bayesopt/trainers/trainer_ft.py +61 -53
  28. ins_pricing/modelling/bayesopt/trainers/trainer_glm.py +9 -3
  29. ins_pricing/modelling/bayesopt/trainers/trainer_gnn.py +40 -32
  30. ins_pricing/modelling/bayesopt/trainers/trainer_resn.py +36 -24
  31. ins_pricing/modelling/bayesopt/trainers/trainer_xgb.py +240 -37
  32. ins_pricing/modelling/bayesopt/utils/distributed_utils.py +193 -186
  33. ins_pricing/modelling/bayesopt/utils/torch_trainer_mixin.py +23 -10
  34. ins_pricing/pricing/factors.py +67 -56
  35. ins_pricing/setup.py +1 -1
  36. ins_pricing/utils/__init__.py +7 -6
  37. ins_pricing/utils/device.py +45 -24
  38. ins_pricing/utils/logging.py +34 -1
  39. ins_pricing/utils/profiling.py +8 -4
  40. {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.1.dist-info}/METADATA +182 -182
  41. {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.1.dist-info}/RECORD +43 -42
  42. {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.1.dist-info}/WHEEL +0 -0
  43. {ins_pricing-0.5.0.dist-info → ins_pricing-0.5.1.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ This directory contains reusable training tooling and frameworks centered on Bay
4
4
 
5
5
  ## Key locations
6
6
 
7
- - `core/bayesopt/` - core training/tuning package
7
+ - `bayesopt/` - core training/tuning package
8
8
  - `explain/` - explainability helpers
9
9
  - `plotting/` - plotting utilities
10
10
  - `ins_pricing/cli/` - CLI entry points
@@ -46,14 +46,32 @@ python ins_pricing/cli/BayesOpt_entry.py --config-json config_template.json
46
46
  - `embedding`: FT trains with labels but exports embeddings (`pred_<prefix>_*`).
47
47
  - `unsupervised_embedding`: FT trains without labels and exports embeddings.
48
48
 
49
- ## Output layout
50
-
51
- `output_dir/` contains:
52
- - `plot/` plots and diagnostics
53
- - `Results/` metrics, params, and snapshots
54
- - `model/` saved models
55
-
56
- ## Notes
57
-
58
- - Relative paths in config are resolved from the config file directory.
59
- - For multi-GPU, use `torchrun` and set `runner.nproc_per_node` in config.
49
+ ## Output layout
50
+
51
+ `output_dir/` contains:
52
+ - `plot/` plots and diagnostics
53
+ - `Results/` metrics, params, and snapshots
54
+ - `model/` saved models
55
+
56
+ ## XGBoost GPU tips
57
+
58
+ - Use `xgb_gpu_id` to select a specific GPU on multi-GPU Linux systems.
59
+ - Per-fold GPU cleanup is disabled by default to avoid long idle gaps caused by CUDA sync.
60
+ - If you need to reclaim memory between folds, set `xgb_cleanup_per_fold=true`.
61
+ - If you still need a full device sync, set `xgb_cleanup_synchronize=true` (slower).
62
+ - `xgb_use_dmatrix=true` switches XGBoost to `xgb.train` + DMatrix/QuantileDMatrix for better throughput.
63
+ - External-memory DMatrix (file-backed) is disabled; pass in-memory arrays/dataframes.
64
+
65
+ ## Torch model cleanup
66
+
67
+ To reduce CPU↔GPU thrash, fold-level cleanup for FT/ResNet/GNN is off by default.
68
+ Enable if you see memory pressure:
69
+ - `ft_cleanup_per_fold`, `ft_cleanup_synchronize`
70
+ - `resn_cleanup_per_fold`, `resn_cleanup_synchronize`
71
+ - `gnn_cleanup_per_fold`, `gnn_cleanup_synchronize`
72
+ - `optuna_cleanup_synchronize` controls whether trial-level cleanup syncs CUDA (default false)
73
+
74
+ ## Notes
75
+
76
+ - Relative paths in config are resolved from the config file directory.
77
+ - For multi-GPU, use `torchrun` and set `runner.nproc_per_node` in config.
@@ -178,10 +178,18 @@ class XGBoostConfig:
178
178
  Attributes:
179
179
  max_depth_max: Maximum tree depth for hyperparameter tuning
180
180
  n_estimators_max: Maximum number of estimators for tuning
181
+ gpu_id: GPU device id for XGBoost (None = default)
182
+ cleanup_per_fold: Whether to cleanup GPU memory after each fold
183
+ cleanup_synchronize: Whether to synchronize CUDA during cleanup
184
+ use_dmatrix: Whether to use xgb.train with DMatrix/QuantileDMatrix
181
185
  """
182
186
 
183
187
  max_depth_max: int = 25
184
188
  n_estimators_max: int = 500
189
+ gpu_id: Optional[int] = None
190
+ cleanup_per_fold: bool = False
191
+ cleanup_synchronize: bool = False
192
+ use_dmatrix: bool = True
185
193
 
186
194
  @classmethod
187
195
  def from_flat_dict(cls, d: Dict[str, Any]) -> "XGBoostConfig":
@@ -189,6 +197,10 @@ class XGBoostConfig:
189
197
  return cls(
190
198
  max_depth_max=int(d.get("xgb_max_depth_max", 25)),
191
199
  n_estimators_max=int(d.get("xgb_n_estimators_max", 500)),
200
+ gpu_id=d.get("xgb_gpu_id"),
201
+ cleanup_per_fold=bool(d.get("xgb_cleanup_per_fold", False)),
202
+ cleanup_synchronize=bool(d.get("xgb_cleanup_synchronize", False)),
203
+ use_dmatrix=bool(d.get("xgb_use_dmatrix", True)),
192
204
  )
193
205
 
194
206
 
@@ -14,6 +14,13 @@ from sklearn.preprocessing import StandardScaler
14
14
  from ins_pricing.utils.io import IOUtils
15
15
  from ins_pricing.utils.losses import normalize_loss_name
16
16
  from ins_pricing.exceptions import ConfigurationError, DataValidationError
17
+ from ins_pricing.utils import get_logger, log_print
18
+
19
+ _logger = get_logger("ins_pricing.modelling.bayesopt.config_preprocess")
20
+
21
+
22
+ def _log(*args, **kwargs) -> None:
23
+ log_print(_logger, *args, **kwargs)
17
24
 
18
25
  # NOTE: Some CSV exports may contain invisible BOM characters or leading/trailing
19
26
  # spaces in column names. Pandas requires exact matches, so we normalize a few
@@ -86,9 +93,20 @@ class BayesOptConfig:
86
93
  prop_test: Proportion of data for validation (0.0-1.0)
87
94
  rand_seed: Random seed for reproducibility
88
95
  epochs: Number of training epochs
89
- use_gpu: Whether to use GPU acceleration
90
- xgb_max_depth_max: Maximum tree depth for XGBoost tuning
91
- xgb_n_estimators_max: Maximum estimators for XGBoost tuning
96
+ use_gpu: Whether to use GPU acceleration
97
+ xgb_max_depth_max: Maximum tree depth for XGBoost tuning
98
+ xgb_n_estimators_max: Maximum estimators for XGBoost tuning
99
+ xgb_gpu_id: GPU device id for XGBoost (None = default)
100
+ xgb_cleanup_per_fold: Whether to cleanup GPU memory after each XGBoost fold
101
+ xgb_cleanup_synchronize: Whether to synchronize CUDA during XGBoost cleanup
102
+ xgb_use_dmatrix: Whether to use xgb.train with DMatrix/QuantileDMatrix
103
+ ft_cleanup_per_fold: Whether to cleanup GPU memory after each FT fold
104
+ ft_cleanup_synchronize: Whether to synchronize CUDA during FT cleanup
105
+ resn_cleanup_per_fold: Whether to cleanup GPU memory after each ResNet fold
106
+ resn_cleanup_synchronize: Whether to synchronize CUDA during ResNet cleanup
107
+ gnn_cleanup_per_fold: Whether to cleanup GPU memory after each GNN fold
108
+ gnn_cleanup_synchronize: Whether to synchronize CUDA during GNN cleanup
109
+ optuna_cleanup_synchronize: Whether to synchronize CUDA during Optuna cleanup
92
110
  use_resn_data_parallel: Use DataParallel for ResNet
93
111
  use_ft_data_parallel: Use DataParallel for FT-Transformer
94
112
  use_resn_ddp: Use DDP for ResNet
@@ -128,9 +146,20 @@ class BayesOptConfig:
128
146
  epochs: int = 100
129
147
  use_gpu: bool = True
130
148
 
131
- # XGBoost settings
132
- xgb_max_depth_max: int = 25
133
- xgb_n_estimators_max: int = 500
149
+ # XGBoost settings
150
+ xgb_max_depth_max: int = 25
151
+ xgb_n_estimators_max: int = 500
152
+ xgb_gpu_id: Optional[int] = None
153
+ xgb_cleanup_per_fold: bool = False
154
+ xgb_cleanup_synchronize: bool = False
155
+ xgb_use_dmatrix: bool = True
156
+ ft_cleanup_per_fold: bool = False
157
+ ft_cleanup_synchronize: bool = False
158
+ resn_cleanup_per_fold: bool = False
159
+ resn_cleanup_synchronize: bool = False
160
+ gnn_cleanup_per_fold: bool = False
161
+ gnn_cleanup_synchronize: bool = False
162
+ optuna_cleanup_synchronize: bool = False
134
163
 
135
164
  # Distributed training settings
136
165
  use_resn_data_parallel: bool = False
@@ -244,10 +273,18 @@ class BayesOptConfig:
244
273
  errors.append(
245
274
  f"xgb_max_depth_max must be >= 1, got {self.xgb_max_depth_max}"
246
275
  )
247
- if self.xgb_n_estimators_max < 1:
248
- errors.append(
249
- f"xgb_n_estimators_max must be >= 1, got {self.xgb_n_estimators_max}"
250
- )
276
+ if self.xgb_n_estimators_max < 1:
277
+ errors.append(
278
+ f"xgb_n_estimators_max must be >= 1, got {self.xgb_n_estimators_max}"
279
+ )
280
+ if self.xgb_gpu_id is not None:
281
+ try:
282
+ gpu_id = int(self.xgb_gpu_id)
283
+ except (TypeError, ValueError):
284
+ errors.append(f"xgb_gpu_id must be an integer, got {self.xgb_gpu_id!r}")
285
+ else:
286
+ if gpu_id < 0:
287
+ errors.append(f"xgb_gpu_id must be >= 0, got {gpu_id}")
251
288
 
252
289
  # Validate distributed training: can't use both DataParallel and DDP
253
290
  if self.use_resn_data_parallel and self.use_resn_ddp:
@@ -363,7 +400,7 @@ class VersionManager:
363
400
  IOUtils.ensure_parent_dir(str(path))
364
401
  with open(path, "w", encoding="utf-8") as f:
365
402
  json.dump(payload, f, ensure_ascii=False, indent=2, default=str)
366
- print(f"[Version] Saved snapshot: {path}")
403
+ _log(f"[Version] Saved snapshot: {path}")
367
404
  return str(path)
368
405
 
369
406
  def load_latest(self, tag: str) -> Optional[Dict[str, Any]]:
@@ -377,7 +414,7 @@ class VersionManager:
377
414
  try:
378
415
  return json.loads(path.read_text(encoding="utf-8"))
379
416
  except Exception as exc:
380
- print(f"[Version] Failed to load snapshot {path}: {exc}")
417
+ _log(f"[Version] Failed to load snapshot {path}: {exc}")
381
418
  return None
382
419
 
383
420
 
@@ -469,7 +506,7 @@ class DatasetPreprocessor:
469
506
 
470
507
  build_oht = bool(getattr(cfg, "build_oht", True))
471
508
  if not build_oht:
472
- print("[Preprocess] build_oht=False; skip one-hot features.", flush=True)
509
+ _log("[Preprocess] build_oht=False; skip one-hot features.", flush=True)
473
510
  self.train_oht_data = None
474
511
  self.test_oht_data = None
475
512
  self.train_oht_scl_data = None
@@ -16,7 +16,7 @@ from ins_pricing.modelling.bayesopt.model_explain_mixin import BayesOptExplainMi
16
16
  from ins_pricing.modelling.bayesopt.model_plotting_mixin import BayesOptPlottingMixin
17
17
  from ins_pricing.modelling.bayesopt.models import GraphNeuralNetSklearn
18
18
  from ins_pricing.modelling.bayesopt.trainers import FTTrainer, GLMTrainer, GNNTrainer, ResNetTrainer, XGBTrainer
19
- from ins_pricing.utils import EPS, infer_factor_and_cate_list, set_global_seed
19
+ from ins_pricing.utils import EPS, infer_factor_and_cate_list, set_global_seed, get_logger, log_print
20
20
  from ins_pricing.utils.io import IOUtils
21
21
  from ins_pricing.utils.losses import (
22
22
  infer_loss_name_from_model_name,
@@ -25,6 +25,12 @@ from ins_pricing.utils.losses import (
25
25
  resolve_xgb_objective,
26
26
  )
27
27
 
28
+ _logger = get_logger("ins_pricing.modelling.bayesopt.core")
29
+
30
+
31
+ def _log(*args, **kwargs) -> None:
32
+ log_print(_logger, *args, **kwargs)
33
+
28
34
 
29
35
  class _CVSplitter:
30
36
  """Wrapper to carry optional groups or time order for CV splits."""
@@ -108,11 +114,22 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
108
114
  region_effect_alpha: Optional[float] = None,
109
115
  geo_feature_nmes: Optional[List[str]] = None,
110
116
  geo_token_hidden_dim: Optional[int] = None,
111
- geo_token_layers: Optional[int] = None,
112
- geo_token_dropout: Optional[float] = None,
113
- geo_token_k_neighbors: Optional[int] = None,
114
- geo_token_learning_rate: Optional[float] = None,
115
- geo_token_epochs: Optional[int] = None):
117
+ geo_token_layers: Optional[int] = None,
118
+ geo_token_dropout: Optional[float] = None,
119
+ geo_token_k_neighbors: Optional[int] = None,
120
+ geo_token_learning_rate: Optional[float] = None,
121
+ geo_token_epochs: Optional[int] = None,
122
+ xgb_gpu_id: Optional[int] = None,
123
+ xgb_cleanup_per_fold: bool = False,
124
+ xgb_cleanup_synchronize: bool = False,
125
+ xgb_use_dmatrix: bool = True,
126
+ ft_cleanup_per_fold: bool = False,
127
+ ft_cleanup_synchronize: bool = False,
128
+ resn_cleanup_per_fold: bool = False,
129
+ resn_cleanup_synchronize: bool = False,
130
+ gnn_cleanup_per_fold: bool = False,
131
+ gnn_cleanup_synchronize: bool = False,
132
+ optuna_cleanup_synchronize: bool = False):
116
133
  """Orchestrate BayesOpt training across multiple trainers.
117
134
 
118
135
  Args:
@@ -229,8 +246,19 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
229
246
  rand_seed=rand_seed,
230
247
  epochs=epochs,
231
248
  use_gpu=use_gpu,
232
- xgb_max_depth_max=int(xgb_max_depth_max),
233
- xgb_n_estimators_max=int(xgb_n_estimators_max),
249
+ xgb_max_depth_max=int(xgb_max_depth_max),
250
+ xgb_n_estimators_max=int(xgb_n_estimators_max),
251
+ xgb_gpu_id=int(xgb_gpu_id) if xgb_gpu_id is not None else None,
252
+ xgb_cleanup_per_fold=bool(xgb_cleanup_per_fold),
253
+ xgb_cleanup_synchronize=bool(xgb_cleanup_synchronize),
254
+ xgb_use_dmatrix=bool(xgb_use_dmatrix),
255
+ ft_cleanup_per_fold=bool(ft_cleanup_per_fold),
256
+ ft_cleanup_synchronize=bool(ft_cleanup_synchronize),
257
+ resn_cleanup_per_fold=bool(resn_cleanup_per_fold),
258
+ resn_cleanup_synchronize=bool(resn_cleanup_synchronize),
259
+ gnn_cleanup_per_fold=bool(gnn_cleanup_per_fold),
260
+ gnn_cleanup_synchronize=bool(gnn_cleanup_synchronize),
261
+ optuna_cleanup_synchronize=bool(optuna_cleanup_synchronize),
234
262
  use_resn_data_parallel=use_resn_data_parallel,
235
263
  use_ft_data_parallel=use_ft_data_parallel,
236
264
  use_resn_ddp=use_resn_ddp,
@@ -542,7 +570,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
542
570
  test_embed, index=self.test_data.index, columns=cols)
543
571
  return train_tokens, test_tokens, cols, geo_gnn
544
572
  except Exception as exc:
545
- print(f"[GeoToken] Generation failed: {exc}")
573
+ _log(f"[GeoToken] Generation failed: {exc}")
546
574
  return None
547
575
 
548
576
  def _prepare_geo_tokens(self) -> None:
@@ -553,7 +581,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
553
581
  gnn_trainer.prepare_geo_tokens(force=False) # type: ignore[attr-defined]
554
582
  return
555
583
  except Exception as exc:
556
- print(f"[GeoToken] GNNTrainer generation failed: {exc}")
584
+ _log(f"[GeoToken] GNNTrainer generation failed: {exc}")
557
585
 
558
586
  result = self._build_geo_tokens()
559
587
  if result is None:
@@ -563,7 +591,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
563
591
  self.test_geo_tokens = test_tokens
564
592
  self.geo_token_cols = cols
565
593
  self.geo_gnn_model = geo_gnn
566
- print(f"[GeoToken] Generated {len(cols)}-dim geo tokens; injecting into FT.")
594
+ _log(f"[GeoToken] Generated {len(cols)}-dim geo tokens; injecting into FT.")
567
595
 
568
596
  def _add_region_effect(self) -> None:
569
597
  """Partial pooling over province/city to create a smoothed region_effect feature."""
@@ -573,7 +601,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
573
601
  return
574
602
  for col in [prov_col, city_col]:
575
603
  if col not in self.train_data.columns:
576
- print(f"[RegionEffect] Missing column {col}; skipped.")
604
+ _log(f"[RegionEffect] Missing column {col}; skipped.")
577
605
  return
578
606
 
579
607
  def safe_mean(df: pd.DataFrame) -> float:
@@ -691,7 +719,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
691
719
  if best_params_file and not trainer.best_params:
692
720
  trainer.best_params = IOUtils.load_params_file(best_params_file)
693
721
  trainer.best_trial = None
694
- print(
722
+ _log(
695
723
  f"[Optuna][{trainer.label}] Loaded best_params from {best_params_file}; skip tuning."
696
724
  )
697
725
 
@@ -705,7 +733,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
705
733
  trainer.best_trial = None
706
734
  trainer.study_name = payload.get(
707
735
  "study_name") if isinstance(payload, dict) else None
708
- print(
736
+ _log(
709
737
  f"[Optuna][{trainer.label}] Reusing best_params from versions snapshot.")
710
738
  return
711
739
 
@@ -716,7 +744,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
716
744
  try:
717
745
  trainer.best_params = IOUtils.load_params_file(params_path)
718
746
  trainer.best_trial = None
719
- print(
747
+ _log(
720
748
  f"[Optuna][{trainer.label}] Reusing best_params from {params_path}.")
721
749
  except ValueError:
722
750
  # Legacy compatibility: ignore empty files and continue tuning.
@@ -725,7 +753,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
725
753
  # Generic optimization entry point.
726
754
  def optimize_model(self, model_key: str, max_evals: int = 100):
727
755
  if model_key not in self.trainers:
728
- print(f"Warning: Unknown model key: {model_key}")
756
+ _log(f"Warning: Unknown model key: {model_key}")
729
757
  return
730
758
 
731
759
  trainer = self._require_trainer(model_key)
@@ -774,7 +802,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
774
802
  elif hasattr(trainer, "ensemble_predict"):
775
803
  trainer.ensemble_predict(k)
776
804
  else:
777
- print(
805
+ _log(
778
806
  f"[Ensemble] Trainer '{model_key}' does not support ensemble prediction.",
779
807
  flush=True,
780
808
  )
@@ -945,7 +973,7 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
945
973
  self.trainers[key].save()
946
974
  else:
947
975
  if model_name: # Only warn when the user specifies a model name.
948
- print(f"[save_model] Warning: Unknown model key {key}")
976
+ _log(f"[save_model] Warning: Unknown model key {key}")
949
977
 
950
978
  def load_model(self, model_name=None):
951
979
  keys = [model_name] if model_name else self.trainers.keys()
@@ -962,4 +990,4 @@ class BayesOptModel(BayesOptPlottingMixin, BayesOptExplainMixin):
962
990
  setattr(self, f"{key}_load", trainer.model)
963
991
  else:
964
992
  if model_name:
965
- print(f"[load_model] Warning: Unknown model key {key}")
993
+ _log(f"[load_model] Warning: Unknown model key {key}")
@@ -16,7 +16,13 @@ except Exception as exc: # pragma: no cover - optional dependency
16
16
  import numpy as np
17
17
  import pandas as pd
18
18
 
19
- from ins_pricing.utils import EPS
19
+ from ins_pricing.utils import EPS, get_logger, log_print
20
+
21
+ _logger = get_logger("ins_pricing.modelling.bayesopt.model_plotting_mixin")
22
+
23
+
24
+ def _log(*args, **kwargs) -> None:
25
+ log_print(_logger, *args, **kwargs)
20
26
 
21
27
  try:
22
28
  from ins_pricing.modelling.plotting import curves as plot_curves
@@ -36,9 +42,9 @@ except Exception: # pragma: no cover - optional for legacy imports
36
42
 
37
43
  def _plot_skip(label: str) -> None:
38
44
  if _MPL_IMPORT_ERROR is not None:
39
- print(f"[Plot] Skip {label}: matplotlib unavailable ({_MPL_IMPORT_ERROR}).", flush=True)
45
+ _log(f"[Plot] Skip {label}: matplotlib unavailable ({_MPL_IMPORT_ERROR}).", flush=True)
40
46
  else:
41
- print(f"[Plot] Skip {label}: matplotlib unavailable.", flush=True)
47
+ _log(f"[Plot] Skip {label}: matplotlib unavailable.", flush=True)
42
48
 
43
49
 
44
50
  class BayesOptPlottingMixin:
@@ -54,7 +60,7 @@ class BayesOptPlottingMixin:
54
60
  _plot_skip("oneway plot")
55
61
  return
56
62
  if pred_col is not None and pred_col not in self.train_data.columns:
57
- print(
63
+ _log(
58
64
  f"[Oneway] Missing prediction column '{pred_col}'; skip predicted line.",
59
65
  flush=True,
60
66
  )
@@ -146,7 +152,7 @@ class BayesOptPlottingMixin:
146
152
  return
147
153
 
148
154
  if "w_act" not in self.train_data.columns:
149
- print("[Oneway] Missing w_act column; skip plotting.", flush=True)
155
+ _log("[Oneway] Missing w_act column; skip plotting.", flush=True)
150
156
  return
151
157
 
152
158
  for c in self.factor_nmes:
@@ -220,7 +226,7 @@ class BayesOptPlottingMixin:
220
226
  ('Lift Chart on Test Data', self.test_data),
221
227
  ]:
222
228
  if 'w_act' not in data.columns or data['w_act'].isna().all():
223
- print(
229
+ _log(
224
230
  f"[Lift] Missing labels for {title}; skip.",
225
231
  flush=True,
226
232
  )
@@ -228,7 +234,7 @@ class BayesOptPlottingMixin:
228
234
  datasets.append((title, data))
229
235
 
230
236
  if not datasets:
231
- print("[Lift] No labeled data available; skip plotting.", flush=True)
237
+ _log("[Lift] No labeled data available; skip plotting.", flush=True)
232
238
  return
233
239
 
234
240
  if plot_curves is None:
@@ -250,7 +256,7 @@ class BayesOptPlottingMixin:
250
256
  denom = np.maximum(data[self.weight_nme].values, EPS)
251
257
  pred_vals = data[w_pred_col].values / denom
252
258
  if pred_vals is None:
253
- print(
259
+ _log(
254
260
  f"[Lift] Missing prediction columns in {title}; skip.",
255
261
  flush=True,
256
262
  )
@@ -313,7 +319,7 @@ class BayesOptPlottingMixin:
313
319
  for data_name, data in [('Train Data', self.train_data),
314
320
  ('Test Data', self.test_data)]:
315
321
  if 'w_act' not in data.columns or data['w_act'].isna().all():
316
- print(
322
+ _log(
317
323
  f"[Double Lift] Missing labels for {data_name}; skip.",
318
324
  flush=True,
319
325
  )
@@ -321,7 +327,7 @@ class BayesOptPlottingMixin:
321
327
  datasets.append((data_name, data))
322
328
 
323
329
  if not datasets:
324
- print("[Double Lift] No labeled data available; skip plotting.", flush=True)
330
+ _log("[Double Lift] No labeled data available; skip plotting.", flush=True)
325
331
  return
326
332
 
327
333
  if plot_curves is None:
@@ -358,7 +364,7 @@ class BayesOptPlottingMixin:
358
364
  pred2 = data[w_pred2_col].values / np.maximum(weight_vals, EPS)
359
365
 
360
366
  if pred1 is None or pred2 is None:
361
- print(
367
+ _log(
362
368
  f"Warning: missing pred_{name1}/pred_{name2} or w_pred columns in {data_name}. Skip plot.")
363
369
  continue
364
370
 
@@ -395,7 +401,7 @@ class BayesOptPlottingMixin:
395
401
  _plot_skip("conversion lift plot")
396
402
  return
397
403
  if not self.binary_resp_nme:
398
- print("Error: `binary_resp_nme` not provided at BayesOptModel init; cannot plot conversion lift.")
404
+ _log("Error: `binary_resp_nme` not provided at BayesOptModel init; cannot plot conversion lift.")
399
405
  return
400
406
 
401
407
  if plot_curves is None:
@@ -407,7 +413,7 @@ class BayesOptPlottingMixin:
407
413
 
408
414
  for ax, (data_name, data) in zip(axes, datasets.items()):
409
415
  if model_pred_col not in data.columns:
410
- print(f"Warning: missing prediction column '{model_pred_col}' in {data_name}. Skip plot.")
416
+ _log(f"Warning: missing prediction column '{model_pred_col}' in {data_name}. Skip plot.")
411
417
  continue
412
418
 
413
419
  # Sort by model prediction and compute bins.
@@ -463,7 +469,7 @@ class BayesOptPlottingMixin:
463
469
 
464
470
  for ax, (data_name, data) in zip(axes, datasets.items()):
465
471
  if model_pred_col not in data.columns:
466
- print(f"Warning: missing prediction column '{model_pred_col}' in {data_name}. Skip plot.")
472
+ _log(f"Warning: missing prediction column '{model_pred_col}' in {data_name}. Skip plot.")
467
473
  continue
468
474
 
469
475
  plot_curves.plot_conversion_lift(