dragon-ml-toolbox 3.10.2__py3-none-any.whl → 3.12.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.

Potentially problematic release.


This version of dragon-ml-toolbox might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dragon-ml-toolbox
3
- Version: 3.10.2
3
+ Version: 3.12.0
4
4
  Summary: A collection of tools for data science and machine learning projects.
5
5
  Author-email: Karl Loza <luigiloza@gmail.com>
6
6
  License-Expression: MIT
@@ -1,7 +1,7 @@
1
- dragon_ml_toolbox-3.10.2.dist-info/licenses/LICENSE,sha256=2uUFNy7D0TLgHim1K5s3DIJ4q_KvxEXVilnU20cWliY,1066
2
- dragon_ml_toolbox-3.10.2.dist-info/licenses/LICENSE-THIRD-PARTY.md,sha256=lY4_rJPnLnMu7YBQaY-_iz1JRDcLdQzNCyeLAF1glJY,1837
1
+ dragon_ml_toolbox-3.12.0.dist-info/licenses/LICENSE,sha256=2uUFNy7D0TLgHim1K5s3DIJ4q_KvxEXVilnU20cWliY,1066
2
+ dragon_ml_toolbox-3.12.0.dist-info/licenses/LICENSE-THIRD-PARTY.md,sha256=lY4_rJPnLnMu7YBQaY-_iz1JRDcLdQzNCyeLAF1glJY,1837
3
3
  ml_tools/ETL_engineering.py,sha256=yeZsW_7zRvEcuMZbM4E2GV1dxwBoWIeJAcFFk2AK0fY,39502
4
- ml_tools/GUI_tools.py,sha256=bsav1gBo8Pj6bnGo72Bd5RmlZljzVyZrQLLHf6ZZdjM,21500
4
+ ml_tools/GUI_tools.py,sha256=VonZEizPS0ncm8HWU-ik-SgcXKryJU8eSG7NN0QN9cc,42222
5
5
  ml_tools/MICE_imputation.py,sha256=rYqvwQDVtoAJJ0agXWoGzoZEHedWiA6QzcEKEIkiZ08,11388
6
6
  ml_tools/ML_callbacks.py,sha256=g_9nSzoA22UJOQZCPKeDz-Ayh0ECFZLzRd6rZ8SokrE,13080
7
7
  ml_tools/ML_evaluation.py,sha256=oiDV6HItQloUUKCUpltV-2pogubWLBieGpc-VUwosAQ,10106
@@ -14,13 +14,13 @@ ml_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  ml_tools/_pytorch_models.py,sha256=bpWZsrSwCvHJQkR6UfoPpElsMv9AvmiNErNHC8NYB_I,10132
15
15
  ml_tools/data_exploration.py,sha256=M7bn2q5XN9zJZJGAmMMFSFFZh8LGzC2arFelrXw3N6Q,25241
16
16
  ml_tools/datasetmaster.py,sha256=S3PKHNQZ9cyAOck8xQltVLZhaD1gFLfgHFL-aRjz4JU,30077
17
- ml_tools/ensemble_learning.py,sha256=0Ld6jwVRthG-IgtEKw68Hh1K5G-Jx1Sk5MdXnmvKL9M,45663
17
+ ml_tools/ensemble_learning.py,sha256=D-9IbOKtCvyAB-LbPu3sdSRtdp0RZIcQEZcyMnarHmQ,45758
18
18
  ml_tools/handle_excel.py,sha256=lwds7rDLlGSCWiWGI7xNg-Z7kxAepogp0lstSFa0590,12949
19
- ml_tools/keys.py,sha256=pwn7IkqQ00r1rspq7vV2fHJVSg5TTOQ_cPTqM-PewGE,536
19
+ ml_tools/keys.py,sha256=A3mLrtLZrxL27whAs2F1GPqZ1KzJpxBp6QbhxY5ioPI,636
20
20
  ml_tools/logger.py,sha256=UkbiU9ihBhw9VKyn3rZzisdClWV94EBV6B09_D0iUU0,6026
21
21
  ml_tools/path_manager.py,sha256=OCpESgdftbi6mOxetDMIaHhazt4N-W8pJx11X3-yNOs,8305
22
22
  ml_tools/utilities.py,sha256=FW97hMTLLxjDR1so-C-_yDm_iz2z_YfirRXjG_IwSLo,22843
23
- dragon_ml_toolbox-3.10.2.dist-info/METADATA,sha256=kBiUOEZa1iZ9jl-VzV71G5vhr-qg4-WcmRoyH_fTFgA,3274
24
- dragon_ml_toolbox-3.10.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- dragon_ml_toolbox-3.10.2.dist-info/top_level.txt,sha256=wm-oxax3ciyez6VoO4zsFd-gSok2VipYXnbg3TH9PtU,9
26
- dragon_ml_toolbox-3.10.2.dist-info/RECORD,,
23
+ dragon_ml_toolbox-3.12.0.dist-info/METADATA,sha256=JD5pg6MBVM3stGknoD2vwec1pKgykEwNVtRmanRV2sw,3274
24
+ dragon_ml_toolbox-3.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ dragon_ml_toolbox-3.12.0.dist-info/top_level.txt,sha256=wm-oxax3ciyez6VoO4zsFd-gSok2VipYXnbg3TH9PtU,9
26
+ dragon_ml_toolbox-3.12.0.dist-info/RECORD,,
ml_tools/GUI_tools.py CHANGED
@@ -3,19 +3,18 @@ from pathlib import Path
3
3
  import traceback
4
4
  import FreeSimpleGUI as sg
5
5
  from functools import wraps
6
- from typing import Any, Dict, Tuple, List, Literal, Union, Any, Optional
6
+ from typing import Any, Dict, Tuple, List, Literal, Union, Optional, Callable
7
7
  from .utilities import _script_info
8
8
  import numpy as np
9
9
  from .logger import _LOGGER
10
- from abc import ABC, abstractmethod
11
10
 
12
11
 
13
12
  __all__ = [
14
13
  "ConfigManager",
15
14
  "GUIFactory",
16
15
  "catch_exceptions",
17
- "BaseFeatureHandler",
18
- "update_target_fields"
16
+ "FeatureMaster",
17
+ "GUIHandler"
19
18
  ]
20
19
 
21
20
  # --- Configuration Management ---
@@ -105,11 +104,13 @@ class ConfigManager:
105
104
  'max_size': ''
106
105
  }
107
106
  config['Layout'] = {
108
- '; Default size for continuous input boxes (width,height in characters).': '',
107
+ '; Default size for continuous input boxes (width,height in characters/rows).': '',
109
108
  'input_size_cont': '16,1',
110
- '; Default size for combo/binary boxes (width,height in characters).': '',
109
+ '; Default size for combo/binary boxes (width,height in characters/rows).': '',
111
110
  'input_size_binary': '14,1',
112
- '; Default size for buttons (width,height in characters).': '',
111
+ '; Size for multiselect listboxes (width,height in characters/rows).': '',
112
+ 'input_size_multi': '14,4',
113
+ '; Default size for buttons (width,height in characters/rows).': '',
113
114
  'button_size': '15,2'
114
115
  }
115
116
  config['Fonts'] = {
@@ -208,7 +209,7 @@ class GUIFactory:
208
209
  # --- General-Purpose Layout Generators ---
209
210
  def generate_continuous_layout(
210
211
  self,
211
- data_dict: Dict[str, Optional[Tuple[Union[int,float], Union[int,float]]]],
212
+ data_dict: Dict[str, Union[Tuple[Union[int,float,None], Union[int,float,None]],List[Union[int,float,None]]]],
212
213
  is_target: bool = False,
213
214
  layout_mode: Literal["grid", "row"] = 'grid',
214
215
  features_per_column: int = 4
@@ -232,9 +233,9 @@ class GUIFactory:
232
233
  columns = []
233
234
  for name, value in data_dict.items():
234
235
  if value is None:
235
- val_min, val_max = None, None
236
- if not is_target:
237
- raise ValueError(f"Feature '{name}' was assigned a 'None' value. It is not defined as a target.")
236
+ raise ValueError(f"Feature '{name}' was assigned a 'None' value.")
237
+ elif len(value) != 2:
238
+ raise ValueError(f"Feature '{name}' must provide exactly 2 values.")
238
239
  else:
239
240
  val_min, val_max = value
240
241
  key = name
@@ -268,7 +269,7 @@ class GUIFactory:
268
269
 
269
270
  def generate_combo_layout(
270
271
  self,
271
- data_dict: Dict[str, List[Any]],
272
+ data_dict: Dict[str, Union[List[Any],Tuple[Any,...]]],
272
273
  layout_mode: Literal["grid", "row"] = 'grid',
273
274
  features_per_column: int = 4
274
275
  ) -> List[List[sg.Column]]:
@@ -304,6 +305,57 @@ class GUIFactory:
304
305
 
305
306
  # Default to 'grid' layout
306
307
  return [columns[i:i + features_per_column] for i in range(0, len(columns), features_per_column)]
308
+
309
+ def generate_multiselect_layout(
310
+ self,
311
+ data_dict: Dict[str, Union[List[Any], Tuple[Any, ...]]],
312
+ layout_mode: Literal["grid", "row"] = 'grid',
313
+ features_per_column: int = 4
314
+ ) -> List[List[sg.Column]]:
315
+ """
316
+ Generates a layout for features using Listbox elements for multiple selections.
317
+
318
+ This allows the user to select zero or more options from a list without
319
+ being able to input custom text.
320
+
321
+ Args:
322
+ data_dict (dict): Keys are feature names, values are lists of options.
323
+ layout_mode (str): 'grid' for a multi-row grid layout, or 'row' for a single horizontal row.
324
+ features_per_column (int): Number of features per column when `layout_mode` is 'grid'.
325
+
326
+ Returns:
327
+ A list of lists of sg.Column elements, ready to be used in a window layout.
328
+ """
329
+ cfg = self.config
330
+ bg_color = sg.theme_background_color()
331
+ label_font = (cfg.fonts.font_family, cfg.fonts.label_size, cfg.fonts.label_style) # type: ignore
332
+
333
+ columns = []
334
+ for name, values in data_dict.items():
335
+ label = sg.Text(name, font=label_font, background_color=bg_color, key=f"_text_{name}")
336
+
337
+ # Use sg.Listbox for multiple selections.
338
+ element = sg.Listbox(
339
+ values,
340
+ key=name,
341
+ select_mode=sg.LISTBOX_SELECT_MODE_MULTIPLE,
342
+ size=cfg.layout.input_size_multi, # type: ignore
343
+ no_scrollbar=False
344
+ )
345
+ # -------------------
346
+
347
+ layout = [[label], [element]]
348
+ # Add a small spacer for consistent vertical alignment.
349
+ layout.append([sg.Text(" ", font=(cfg.fonts.font_family, 2), background_color=bg_color)]) # type: ignore
350
+
351
+ # Each feature is wrapped in a Column element for proper alignment.
352
+ columns.append(sg.Column(layout, background_color=bg_color))
353
+
354
+ if layout_mode == 'row':
355
+ return [columns] # A single row containing all columns
356
+
357
+ # Default to 'grid' layout
358
+ return [columns[i:i + features_per_column] for i in range(0, len(columns), features_per_column)]
307
359
 
308
360
  # --- Window Creation ---
309
361
  def create_window(self, title: str, layout: List[List[sg.Element]], **kwargs) -> sg.Window:
@@ -357,175 +409,564 @@ def catch_exceptions(show_popup: bool = True):
357
409
  return decorator
358
410
 
359
411
 
360
- # --- Inference Helper ---
361
- class BaseFeatureHandler(ABC):
412
+ # --- Feature Handler ---
413
+ class FeatureMaster:
362
414
  """
363
- An abstract base class that defines the template for preparing a model input feature vector to perform inference, from GUI inputs.
364
-
365
- A subclass must implement the `gui_input_map` property and the `process_categorical` method.
415
+ Manages and organizes feature definitions for a machine learning model.
416
+
417
+ This class serves as a centralized registry for all features and targets
418
+ used by a model. It is designed to bridge the gap between a user-facing
419
+ application (like a GUI) and the underlying model's data representation.
420
+
421
+ It takes various types of features (continuous, binary, one-hot encoded,
422
+ categorical) and targets, processing them into two key formats:
423
+ 1. A mapping from a user-friendly "GUI name" to the corresponding "model name"
424
+ used in the dataset or model training.
425
+ 2. A structure containing the acceptable values or ranges for each feature,
426
+ suitable for populating GUI elements like sliders, dropdowns, or checkboxes.
427
+
428
+ By separating the GUI representation from the model's internal logic, this
429
+ class simplifies the process of building user interfaces for model interaction
430
+ and ensures that user input is correctly formatted. At least one type of
431
+ feature must be provided upon initialization.
432
+
433
+ Properties are available to access the processed mappings and GUI-ready values
434
+ for each feature type.
366
435
  """
367
- def __init__(self, expected_columns_in_order: list[str]):
436
+ def __init__(self,
437
+ targets: Dict[str, str],
438
+ continuous_features: Optional[Dict[str, Tuple[str, float, float]]] = None,
439
+ binary_features: Optional[Dict[str, str]] = None,
440
+ multi_binary_features: Optional[Dict[str, Dict[str, str]]] = None,
441
+ one_hot_features: Optional[Dict[str, Dict[str, str]]] = None,
442
+ categorical_features: Optional[List[Tuple[str, str, Dict[str, int]]]] = None) -> None:
368
443
  """
369
- Validates and stores the feature names in the order the model expects.
370
-
444
+ Initializes the FeatureMaster instance by processing feature and target definitions.
445
+
446
+ This constructor creates internal mappings to translate between GUI-friendly names and model-specific feature names. It also
447
+ prepares data structures needed to populate UI components.
448
+
371
449
  Args:
372
- expected_columns_in_order (List[str]): A list of strings with the feature names in the correct order.
450
+ targets (Dict[str, str]):
451
+ A dictionary defining the model's target variables.
452
+ - **key** (str): The name to be displayed in the GUI.
453
+ - **value** (str): The corresponding column name in the model's dataset.
454
+
455
+ continuous_features (Dict[str, Tuple[str, float, float]]):
456
+ A dictionary for continuous numerical features.
457
+ - **key** (str): The name to be displayed in the GUI (e.g., for a slider).
458
+ - **value** (Tuple[str, float, float]): A tuple containing:
459
+ - `[0]` (str): The model's internal feature name.
460
+ - `[1]` (float): The minimum allowed value (inclusive).
461
+ - `[2]` (float): The maximum allowed value (inclusive).
462
+
463
+ binary_features (Dict[str, str]):
464
+ A dictionary for binary (True/False) features.
465
+ - **key** (str): The name to be displayed in the GUI (e.g., for a checkbox).
466
+ - **value** (str): The model's internal feature name.
467
+
468
+ multi_binary_features (Dict[str, Dict[str, str]]):
469
+ A dictionary for features where multiple binary-like options can be
470
+ selected at once (e.g., from a multi-select listbox).
471
+ - **key** (str): The name for the group to be displayed in the GUI.
472
+ - **value** (Dict[str, str]): A nested dictionary where:
473
+ - key (str): The user-selectable option.
474
+ - value (str): The corresponding model's internal feature name.
475
+
476
+ one_hot_features (Dict[str, Dict[str, str]]):
477
+ A dictionary for features that will be one-hot encoded from a single
478
+ categorical input.
479
+ - **key** (str): The name for the group to be displayed in the GUI (e.g.,
480
+ for a dropdown menu).
481
+ - **value** (Dict[str, str]): A nested dictionary where:
482
+ - key (str): The user-selectable option (e.g., 'Category A').
483
+ - value (str): The corresponding model column name.
484
+
485
+ categorical_features (List[Tuple[str, str, Dict[str, int]]]):
486
+ A list for ordinal or label-encoded categorical features.
487
+ - **Each element is a tuple** containing:
488
+ - `[0]` (str): The name to be displayed in the GUI (e.g., for a
489
+ dropdown menu).
490
+ - `[1]` (str): The model's internal feature name.
491
+ - `[2]` (Dict[str, int]): A dictionary mapping the user-selectable
492
+ options to their corresponding integer values.
373
493
  """
374
- # --- Validation Logic ---
375
- if not isinstance(expected_columns_in_order, list):
376
- raise TypeError("Input 'expected_columns_in_order' must be a list.")
494
+ # Validation
495
+ if continuous_features is None and binary_features is None and one_hot_features is None and categorical_features is None and multi_binary_features is None:
496
+ raise ValueError("No features provided.")
497
+
498
+ # Targets
499
+ self._targets_values = self._handle_targets(targets)
500
+ self._targets_mapping = targets
501
+
502
+ # continuous features
503
+ if continuous_features is not None:
504
+ self._continuous_values, self._continuous_mapping = self._handle_continuous_features(continuous_features)
505
+ self.has_continuous = True
506
+ else:
507
+ self._continuous_values, self._continuous_mapping = None, None
508
+ self.has_continuous = False
377
509
 
378
- if not all(isinstance(col, str) for col in expected_columns_in_order):
379
- raise TypeError("All elements in the 'expected_columns_in_order' list must be strings.")
380
- # -----------------------
510
+ # binary features
511
+ if binary_features is not None:
512
+ self._binary_values = self._handle_binary_features(binary_features)
513
+ self._binary_mapping = binary_features
514
+ self.has_binary = True
515
+ else:
516
+ self._binary_values, self._binary_mapping = None, None
517
+ self.has_binary = False
518
+
519
+ # multi-binary features
520
+ if multi_binary_features is not None:
521
+ self._multi_binary_values = self._handle_multi_binary_features(multi_binary_features)
522
+ self._multi_binary_mapping = multi_binary_features
523
+ self.has_multi_binary = True
524
+ else:
525
+ self._multi_binary_values, self._multi_binary_mapping = None, None
526
+ self.has_multi_binary = False
527
+
528
+ # one-hot features
529
+ if one_hot_features is not None:
530
+ self._one_hot_values = self._handle_one_hot_features(one_hot_features)
531
+ self._one_hot_mapping = one_hot_features
532
+ self.has_one_hot = True
533
+ else:
534
+ self._one_hot_values, self._one_hot_mapping = None, None
535
+ self.has_one_hot = False
536
+
537
+ # categorical features
538
+ if categorical_features is not None:
539
+ self._categorical_values, self._categorical_mapping = self._handle_categorical_features(categorical_features)
540
+ self.has_categorical = True
541
+ else:
542
+ self._categorical_values, self._categorical_mapping = None, None
543
+ self.has_categorical = False
544
+
545
+ # all features attribute
546
+ self._all_features = self._get_all_gui_features()
547
+
548
+ def _handle_targets(self, targets: Dict[str, str]):
549
+ # Make dictionary GUI name: range values
550
+ gui_values: dict[str, tuple[None,None]] = {gui_key: (None, None) for gui_key in targets.keys()}
551
+ # Map GUI name to Model name (same as input)
552
+ return gui_values
381
553
 
382
- self._model_feature_order = expected_columns_in_order
554
+ def _handle_continuous_features(self, continuous_features: Dict[str, Tuple[str, float, float]]):
555
+ # Make dictionary GUI name: range values
556
+ gui_values: dict[str, tuple[float,float]] = {gui_key: (tuple_values[1], tuple_values[2]) for gui_key, tuple_values in continuous_features.items()}
557
+ # Map GUI name to Model name
558
+ gui_to_model: dict[str,str] = {gui_key: tuple_values[0] for gui_key, tuple_values in continuous_features.items()}
559
+ return gui_values, gui_to_model
560
+
561
+ def _handle_binary_features(self, binary_features: Dict[str, str]):
562
+ # Make dictionary GUI name: range values
563
+ gui_values: dict[str, tuple[Literal["False"],Literal["True"]]] = {gui_key: ("False", "True") for gui_key in binary_features.keys()}
564
+ # Map GUI name to Model name (same as input)
565
+ return gui_values
566
+
567
+ def _handle_multi_binary_features(self, multi_binary_features: Dict[str, Dict[str, str]]):
568
+ # Make dictionary GUI name: range values
569
+ gui_values: dict[str, tuple[str,...]] = {
570
+ gui_key: tuple(nested_dict.keys())
571
+ for gui_key, nested_dict in multi_binary_features.items()}
572
+ # Map GUI name to Model name and preserve internal mapping (same as input)
573
+ return gui_values
574
+
575
+ def _handle_one_hot_features(self, one_hot_features: Dict[str, Dict[str,str]]):
576
+ # Make dictionary GUI name: range values
577
+ gui_values: dict[str, tuple[str,...]] = {gui_key: tuple(nested_dict.keys()) for gui_key, nested_dict in one_hot_features.items()}
578
+ # Map GUI name to Model name and preserve internal mapping (same as input)
579
+ return gui_values
383
580
 
581
+ def _handle_categorical_features(self, categorical_features: List[Tuple[str, str, Dict[str, int]]]):
582
+ # Make dictionary GUI name: range values
583
+ gui_values: dict[str, tuple[str,...]] = {gui_key: tuple(gui_options.keys()) for gui_key, _, gui_options in categorical_features}
584
+ # Map GUI name to Model name and preserve internal mapping
585
+ gui_to_model: dict[str, tuple[str, dict[str, int]]] = {gui_key: (model_key, gui_options) for gui_key, model_key, gui_options in categorical_features}
586
+ return gui_values, gui_to_model
587
+
588
+ def _get_all_gui_features(self) -> dict[str,Any]:
589
+ all_dict: dict[str,Any] = dict()
590
+ # Add all feature GUI keys
591
+ if self._continuous_mapping is not None:
592
+ all_dict.update(self._continuous_mapping)
593
+ if self._binary_mapping is not None:
594
+ all_dict.update(self._binary_mapping)
595
+ if self._multi_binary_mapping is not None:
596
+ all_dict.update(self._multi_binary_mapping)
597
+ if self._one_hot_mapping is not None:
598
+ all_dict.update(self._one_hot_mapping)
599
+ if self._categorical_mapping is not None:
600
+ all_dict.update(self._categorical_mapping)
601
+ return all_dict
602
+
384
603
  @property
385
- @abstractmethod
386
- def gui_input_map(self) -> Dict[str, Literal["continuous","categorical"]]:
604
+ def all_features(self):
387
605
  """
388
- Must be implemented by the subclass.
389
-
390
- Should return a dictionary mapping each GUI input name to its type ('continuous' or 'categorical').
606
+ A merged dictionary of all feature mappings.
391
607
 
392
- _Example:_
393
- ```python
394
- {
395
- 'Temperature': 'continuous',
396
- 'Material Type': 'categorical'
397
- }
398
- ```
608
+ The value type varies based on the feature type (str, dict, or tuple).
609
+
610
+ Structure:
611
+ Dict[str, Any]
399
612
  """
400
- pass
613
+ return self._all_features
401
614
 
402
615
  @property
403
- @abstractmethod
404
- def map_gui_to_real(self) -> Dict[str,str]:
616
+ def targets(self):
405
617
  """
406
- Must be implemented by the subclass.
407
-
408
- Should return a dictionary mapping each GUI continuous feature name to its expected model feature name.
618
+ The mapping for target variables from GUI name to model name.
409
619
 
410
- _Example:_
411
- ```python
412
- {
413
- 'Temperature (K)': 'temperature_k',
414
- 'Pressure (Pa)': 'pressure_pa'
415
- }
416
- ```
620
+ Structure:
621
+ Dict[str, str]
622
+ """
623
+ return self._targets_mapping
624
+
625
+ @property
626
+ def targets_gui(self):
627
+ """
628
+ The GUI value structure for targets.
629
+
630
+ Structure:
631
+ Dict[str, Tuple[None, None]]
632
+ """
633
+ return self._targets_values
634
+
635
+ @property
636
+ def continuous(self):
637
+ """
638
+ The mapping for continuous features from GUI name to model name.
639
+
640
+ Structure:
641
+ Dict[str, str]
642
+ """
643
+ if self._continuous_mapping is not None:
644
+ return self._continuous_mapping
645
+
646
+ @property
647
+ def continuous_gui(self):
648
+ """
649
+ The GUI value ranges (min, max) for continuous features.
650
+
651
+ Structure:
652
+ Dict[str, Tuple[float, float]]
653
+ """
654
+ if self._continuous_values is not None:
655
+ return self._continuous_values
656
+
657
+ @property
658
+ def binary(self):
659
+ """
660
+ The mapping for binary features from GUI name to model name.
661
+
662
+ Structure:
663
+ Dict[str, str]
664
+ """
665
+ if self._binary_mapping is not None:
666
+ return self._binary_mapping
667
+
668
+ @property
669
+ def binary_gui(self):
670
+ """
671
+ The GUI options ('False', 'True') for binary features.
672
+
673
+ Structure:
674
+ Dict[str, Tuple['False', 'True']]
675
+ """
676
+ if self._binary_values is not None:
677
+ return self._binary_values
678
+
679
+ @property
680
+ def multi_binary(self):
417
681
  """
418
- pass
682
+ The mapping for multi-binary features.
683
+
684
+ Structure:
685
+ {"GUI NAME": {"GUI OPTION 1": "model_column"}}
686
+ """
687
+ if self._multi_binary_mapping is not None:
688
+ return self._multi_binary_mapping
689
+
690
+ @property
691
+ def multi_binary_gui(self):
692
+ """
693
+ The GUI options for multi-binary feature groups.
694
+
695
+ Structure:
696
+ Dict[str, Tuple[str, ...]]
697
+ """
698
+ if self._multi_binary_values is not None:
699
+ return self._multi_binary_values
419
700
 
420
- @abstractmethod
421
- def process_categorical(self, gui_feature_name: str, chosen_value: Any) -> Dict[str, float]:
701
+ @property
702
+ def one_hot(self):
703
+ """
704
+ The mapping for one-hot encoded features.
705
+
706
+ {"GUI NAME": {"GUI OPTION 1": "model_column"}}
707
+
708
+ Structure:
709
+ Dict[str, Dict[str, str]]
422
710
  """
423
- Must be implemented by the subclass.
711
+ if self._one_hot_mapping is not None:
712
+ return self._one_hot_mapping
713
+
714
+ @property
715
+ def one_hot_gui(self):
716
+ """
717
+ The GUI options for one-hot encoded feature groups.
718
+
719
+ Structure:
720
+ Dict[str, Tuple[str, ...]]
721
+ """
722
+ if self._one_hot_values is not None:
723
+ return self._one_hot_values
424
724
 
425
- Should take a GUI categorical feature name and its chosen value, and return a dictionary mapping the one-hot-encoded/binary real feature names to their
426
- float values (as expected by the inference model).
725
+ @property
726
+ def categorical(self):
727
+ """
728
+ The mapping for categorical features.
427
729
 
428
- _Example:_
429
- ```python
430
- # GUI input: "Material Type"
431
- # GUI values: "Steel", "Aluminum", "Titanium"
432
- {
433
- "is_steel": 0,
434
- "is_aluminum": 1,
435
- "is_titanium": 0,
436
- }
437
- ```
730
+ {"GUI NAME": ("model_column", {"GUI OPTION 1": column_value})}
731
+
732
+ Structure:
733
+ Dict[str, Tuple[str, Dict[str, int]]]
734
+ """
735
+ if self._categorical_mapping is not None:
736
+ return self._categorical_mapping
737
+
738
+ @property
739
+ def categorical_gui(self):
438
740
  """
439
- pass
741
+ The GUI options for categorical features.
742
+
743
+ Structure:
744
+ Dict[str, Tuple[str, ...]]
745
+ """
746
+ if self._categorical_values is not None:
747
+ return self._categorical_values
748
+
749
+
750
+ # --- GUI-Model API ---
751
+ class GUIHandler:
752
+ """
753
+ Translates data between a GUI and a machine learning model.
440
754
 
441
- def _process_continuous(self, gui_feature_name: str, chosen_value: Any) -> Tuple[str, float]:
755
+ This class acts as the primary interface between a user-facing application
756
+ (FreeSimpleGUI) and the model's expected data format. It uses a `FeatureMaster` instance to correctly process
757
+ and encode user inputs.
758
+
759
+ Its main responsibilities are:
760
+ 1. To take raw values from GUI elements and, using the definitions from
761
+ `FeatureMaster`, convert them into a single, ordered `numpy.ndarray`
762
+ that can be fed directly into a model for inference.
763
+ 2. To take the results of a model's inference and update the
764
+ corresponding target fields in the GUI to display the prediction.
765
+
766
+ This handler ensures a clean separation of concerns, where the GUI is
767
+ only responsible for presentation, and the model sees correctly formatted numerical data.
768
+ """
769
+ def __init__(self, feature_handler: FeatureMaster, model_expected_features: list[str]) -> None:
442
770
  """
443
- Maps GUI names to model expected names and casts the value to float.
771
+ Initializes the GUIHandler.
772
+
773
+ Args:
774
+ feature_handler (FeatureMaster):
775
+ An initialized instance of the `FeatureMaster` class. This object
776
+ contains all the necessary mappings and definitions for the model's
777
+ features and targets.
778
+ model_expected_features (list[str]):
779
+ A list of strings specifying the exact names of the features the
780
+ machine learning model expects in its input vector. The **order**
781
+ of features in this list is critical, as it dictates the final
782
+ column order of the output numpy array.
783
+
784
+ Raises:
785
+ TypeError: If `model_expected_features` is not a list or if any of its elements are not strings.
786
+ """
787
+ if not isinstance(model_expected_features, list):
788
+ raise TypeError("Input 'model_expected_features' must be a list.")
789
+ if not all(isinstance(col, str) for col in model_expected_features):
790
+ raise TypeError("All elements in the 'model_expected_features' must be strings.")
444
791
 
445
- Should not be overridden by subclasses.
792
+ # Model expected features
793
+ self.model_expected_features = tuple(model_expected_features)
794
+ # Feature master instance
795
+ self.master = feature_handler
796
+
797
+ def _process_continuous(self, gui_feature: str, chosen_value: Any) -> Tuple[str,float]:
798
+ """
799
+ Maps GUI name to model expected name and casts the value to float.
446
800
  """
447
801
  try:
448
- real_name = self.map_gui_to_real[gui_feature_name]
802
+ model_name = self.master.continuous[gui_feature] # type: ignore
449
803
  float_value = float(chosen_value)
450
804
  except KeyError as e:
451
- _LOGGER.error(f"No matching name for '{gui_feature_name}'. Check the 'map_gui_to_real' implementation.")
805
+ _LOGGER.error(f"No matching name for '{gui_feature}' defined as continuous.")
452
806
  raise e
453
807
  except (ValueError, TypeError) as e2:
454
- _LOGGER.error(f"Invalid number conversion for '{chosen_value}' of '{gui_feature_name}'.")
808
+ _LOGGER.error(f"Invalid number conversion for '{chosen_value}' of '{gui_feature}'.")
455
809
  raise e2
456
810
  else:
457
- return real_name, float_value
811
+ return model_name, float_value
812
+
813
+ def _process_binary(self, gui_feature: str, chosen_value: str) -> Tuple[str,int]:
814
+ """
815
+ Maps GUI name to model expected name and casts the value to binary (0,1).
816
+ """
817
+ try:
818
+ model_name = self.master.binary[gui_feature] # type: ignore
819
+ binary_mapping_keys = self.master.binary_gui[gui_feature] # type: ignore
820
+ except KeyError as e:
821
+ _LOGGER.error(f"No matching name for '{gui_feature}' defined as binary.")
822
+ raise e
823
+ else:
824
+ mapping_dict = {
825
+ binary_mapping_keys[0]: 0,
826
+ binary_mapping_keys[1]: 1
827
+ }
828
+ result = mapping_dict[chosen_value]
829
+ return model_name, result
458
830
 
459
- def __call__(self, window_values: Dict[str, Any]) -> np.ndarray:
831
+ def _process_multi_binary(self, gui_feature: str, chosen_values: list[str]) -> dict[str, int]:
832
+ """
833
+ Maps GUI names to model expected names and casts values to multi-binary encoding.
834
+
835
+ For a given feature group, this sets all selected options to 1 and all
836
+ unselected options to 0.
837
+ """
838
+ try:
839
+ # Get the mapping for the group
840
+ multi_binary_mapping = self.master.multi_binary[gui_feature] # type: ignore
841
+ except KeyError as e:
842
+ _LOGGER.error(f"No matching name for '{gui_feature}' defined as multi-binary.")
843
+ raise e
844
+ else:
845
+ # Start with all possible features for this group set to 0 (unselected)
846
+ results = {model_key: 0 for model_key in multi_binary_mapping.values()}
847
+ # Set the features for the chosen options to 1
848
+ for chosen_option in chosen_values:
849
+ model_name = multi_binary_mapping[chosen_option]
850
+ results[model_name] = 1
851
+
852
+ return results
853
+
854
+ def _process_one_hot(self, gui_feature: str, chosen_value: str) -> Dict[str,int]:
855
+ """
856
+ Maps GUI names to model expected names and casts values to one-hot encoding.
460
857
  """
461
- Performs the full vector preparation, returning a 1D numpy array.
858
+ try:
859
+ one_hot_mapping = self.master.one_hot[gui_feature] # type: ignore
860
+ except KeyError as e:
861
+ _LOGGER.error(f"No matching name for '{gui_feature}' defined as one-hot.")
862
+ raise e
863
+ else:
864
+ mapped_chosen_value = one_hot_mapping[chosen_value]
865
+ # base results mapped to 0
866
+ results = {model_key: 0 for model_key in one_hot_mapping.values()}
867
+ # update chosen value
868
+ results[mapped_chosen_value] = 1
869
+ return results
462
870
 
463
- Should not be overridden by subclasses.
871
+ def _process_categorical(self, gui_feature: str, chosen_value: str) -> Tuple[str,int]:
464
872
  """
465
- # Stage 1: Process GUI inputs into a dictionary
466
- processed_features: Dict[str, float] = {}
467
- for gui_name, feature_type in self.gui_input_map.items():
468
- chosen_value = window_values.get(gui_name)
873
+ Maps GUI name to model expected name and casts the value to a categorical number.
874
+ """
875
+ try:
876
+ categorical_tuple = self.master.categorical[gui_feature] # type: ignore
877
+ except KeyError as e:
878
+ _LOGGER.error(f"No matching name for '{gui_feature}' defined as categorical.")
879
+ raise e
880
+ else:
881
+ model_name = categorical_tuple[0]
882
+ categorical_mapping = categorical_tuple[1]
883
+ result = categorical_mapping[chosen_value]
884
+ return model_name, result
885
+
886
+ def update_target_fields(self, window: sg.Window, inference_results: Dict[str, Any]):
887
+ """
888
+ Updates the GUI's target fields with inference results.
889
+
890
+ Args:
891
+ window (sg.Window): The application's window object.
892
+ inference_results (dict): A dictionary where keys are target names (as used by the model) and values are the predicted results to update.
893
+ """
894
+ # Target values to update
895
+ gui_targets_values = {gui_key: inference_results[model_key] for gui_key, model_key in self.master.targets.items()}
896
+
897
+ # Update window
898
+ for gui_key, result in gui_targets_values.items():
899
+ # Format numbers to 2 decimal places, leave other types as-is
900
+ display_value = f"{result:.2f}" if isinstance(result, (int, float)) else result
901
+ window[gui_key].update(display_value) # type: ignore
469
902
 
903
+ def _call_subprocess(self, window_values: Dict[str,Any], master_feature: Dict[str,str], processor: Callable) -> Dict[str, Union[float,int]]:
904
+ processed_features_subset: Dict[str, Union[float,int]] = dict()
905
+
906
+ for gui_name in master_feature.keys():
907
+ chosen_value = window_values.get(gui_name)
470
908
  # value validation
471
909
  if chosen_value is None or str(chosen_value) == '':
472
910
  raise ValueError(f"GUI input '{gui_name}' is missing a value.")
911
+ # process value
912
+ raw_result = processor(gui_name, chosen_value)
913
+ if isinstance(raw_result, tuple):
914
+ model_name, result = raw_result
915
+ processed_features_subset[model_name] = result
916
+ elif isinstance(raw_result, dict):
917
+ processed_features_subset.update(raw_result)
918
+ else:
919
+ raise TypeError(f"Processor returned an unrecognized type: {type(raw_result)}")
920
+
921
+ return processed_features_subset
473
922
 
474
- # process continuous
475
- if feature_type == 'continuous':
476
- mapped_name, float_value = self._process_continuous(gui_name, chosen_value)
477
- processed_features[mapped_name] = float_value
923
+ def process_features(self, window_values: Dict[str, Any]) -> np.ndarray:
924
+ """
925
+ Translates GUI values to a model-expected input array, returning a 1D numpy array.
926
+ """
927
+ # Stage 1: Process GUI inputs into a dictionary
928
+ processed_features: Dict[str, Union[float,int]] = {}
929
+
930
+ if self.master.has_continuous:
931
+ processed_subset = self._call_subprocess(window_values=window_values,
932
+ master_feature=self.master.continuous, # type: ignore
933
+ processor=self._process_continuous)
934
+ processed_features.update(processed_subset)
935
+
936
+ if self.master.has_binary:
937
+ processed_subset = self._call_subprocess(window_values=window_values,
938
+ master_feature=self.master.binary, # type: ignore
939
+ processor=self._process_binary)
940
+ processed_features.update(processed_subset)
478
941
 
479
- # process categorical
480
- elif feature_type == 'categorical':
481
- feature_dict = self.process_categorical(gui_name, chosen_value)
482
- processed_features.update(feature_dict)
942
+ if self.master.has_multi_binary:
943
+ processed_subset = self._call_subprocess(window_values=window_values,
944
+ master_feature=self.master.multi_binary, # type: ignore
945
+ processor=self._process_multi_binary)
946
+ processed_features.update(processed_subset)
947
+
948
+ if self.master.has_one_hot:
949
+ processed_subset = self._call_subprocess(window_values=window_values,
950
+ master_feature=self.master.one_hot, # type: ignore
951
+ processor=self._process_one_hot)
952
+ processed_features.update(processed_subset)
953
+
954
+ if self.master.has_categorical:
955
+ processed_subset = self._call_subprocess(window_values=window_values,
956
+ master_feature=self.master.categorical, # type: ignore
957
+ processor=self._process_categorical)
958
+ processed_features.update(processed_subset)
483
959
 
484
960
  # Stage 2: Assemble the final vector using the model's required order
485
- final_vector: List[float] = []
961
+ final_vector: List[float] = list()
486
962
 
487
963
  try:
488
- for feature_name in self._model_feature_order:
964
+ for feature_name in self.model_expected_features:
489
965
  final_vector.append(processed_features[feature_name])
490
966
  except KeyError as e:
491
- raise RuntimeError(
492
- f"Configuration Error: Implemented methods failed to generate "
493
- f"the required model feature: '{e}'"
494
- f"Check the gui_input_map and process_categorical logic."
495
- )
496
-
967
+ raise RuntimeError(f"Configuration Error: Implemented methods failed to generate the required model feature: '{e}'")
968
+
497
969
  return np.array(final_vector, dtype=np.float32)
498
970
 
499
-
500
- def update_target_fields(window: sg.Window, results_dict: Dict[str, Any], map_model_to_gui: Optional[Dict[str,str]]):
501
- """
502
- Updates the GUI's target fields with inference results.
503
-
504
- Args:
505
- window (sg.Window): The application's window object.
506
- results_dict (dict): A dictionary where keys are target names (as expected by the GUI) and values are the predicted results to update.
507
- map_model_to_gui (dict | None): Map `results_dict.keys()` from model target names to GUI target names, if gui names were customized.
508
- """
509
- if map_model_to_gui is not None:
510
- # Validation
511
- if len(map_model_to_gui) != len(results_dict):
512
- _LOGGER.error(f"Expected a mapping for {len(results_dict)} targets, but received {len(map_model_to_gui)} target map names.")
513
- raise ValueError
514
-
515
- # new dictionary with GUI keys and corresponding result values
516
- display_dict = {
517
- gui_key: results_dict[model_key]
518
- for model_key, gui_key in map_model_to_gui.items()
519
- }
520
- else:
521
- # If no map is provided, use given result keys
522
- display_dict = results_dict
523
-
524
- for key, result in display_dict.items():
525
- # Format numbers to 2 decimal places, leave other types as-is
526
- display_value = f"{result:.2f}" if isinstance(result, (int, float)) else result
527
- window[key].update(display_value) # type: ignore
528
-
529
-
530
971
  def info():
531
972
  _script_info(__all__)
@@ -1026,7 +1026,8 @@ class InferenceHandler:
1026
1026
  else: # Classification
1027
1027
  label = model.predict(features)[0]
1028
1028
  probabilities = model.predict_proba(features)[0]
1029
- results[target_name] = {"label": label, "probabilities": probabilities}
1029
+ results[target_name] = {ModelSaveKeys.CLASSIFICATION_LABEL: label,
1030
+ ModelSaveKeys.CLASSIFICATION_PROBABILITIES: probabilities}
1030
1031
 
1031
1032
  if self.verbose:
1032
1033
  _LOGGER.info("✅ Inference process complete.")
ml_tools/keys.py CHANGED
@@ -17,9 +17,12 @@ class LogKeys:
17
17
  class ModelSaveKeys:
18
18
  """
19
19
  Used internally for ensemble_learning module.
20
-
21
- Keys used for serializing a trained model metadata.
22
20
  """
21
+ # Serializing a trained model metadata.
23
22
  MODEL = "model"
24
23
  FEATURES = "feature_names"
25
24
  TARGET = "target_name"
25
+
26
+ # Classification keys
27
+ CLASSIFICATION_LABEL = "label"
28
+ CLASSIFICATION_PROBABILITIES = "probabilities"