ImportanceScore 1.1.2__tar.gz
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.
- importancescore-1.1.2/ImportanceScore/ImportanceScore/__init__.py +1 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/csv_table.py +114 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/custom_editor.py +50 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/data_loader.py +211 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/features.py +155 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/importance_rank.py +522 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/importance_schema.py +161 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/manifest.py +77 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/model.py +264 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/predict_model.py +438 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/project_paths.py +96 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/resources/__init__.py +1 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/text_weight_scoring.py +206 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/train_model.py +249 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/transformations.py +240 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/tune_model.py +124 -0
- importancescore-1.1.2/ImportanceScore/ImportanceScore/weighted_linear_model.py +161 -0
- importancescore-1.1.2/ImportanceScore/tests/text_benchmark.py +202 -0
- importancescore-1.1.2/ImportanceScore.egg-info/PKG-INFO +136 -0
- importancescore-1.1.2/ImportanceScore.egg-info/SOURCES.txt +27 -0
- importancescore-1.1.2/ImportanceScore.egg-info/dependency_links.txt +1 -0
- importancescore-1.1.2/ImportanceScore.egg-info/entry_points.txt +5 -0
- importancescore-1.1.2/ImportanceScore.egg-info/requires.txt +18 -0
- importancescore-1.1.2/ImportanceScore.egg-info/top_level.txt +1 -0
- importancescore-1.1.2/LICENSE +7 -0
- importancescore-1.1.2/PKG-INFO +136 -0
- importancescore-1.1.2/docs/readme.md +92 -0
- importancescore-1.1.2/pyproject.toml +56 -0
- importancescore-1.1.2/setup.cfg +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.1"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import sys
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from PySide6.QtCore import Qt
|
|
7
|
+
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QLineEdit, QMainWindow,
|
|
8
|
+
QPushButton, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, )
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CsvTable(QWidget):
|
|
12
|
+
"""
|
|
13
|
+
QWidget for viewing a CSV data in a table - supports find and sort
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
|
16
|
+
super().__init__(parent)
|
|
17
|
+
main_layout = QVBoxLayout(self)
|
|
18
|
+
self.table_widget = QTableWidget()
|
|
19
|
+
self.table_widget.setSortingEnabled(True)
|
|
20
|
+
self.table_widget.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
|
21
|
+
main_layout.addWidget(self.table_widget)
|
|
22
|
+
|
|
23
|
+
find_layout = QHBoxLayout()
|
|
24
|
+
self.find_input = QLineEdit()
|
|
25
|
+
self.find_input.textChanged.connect(self._reset_search)
|
|
26
|
+
|
|
27
|
+
self.find_button = QPushButton("Find")
|
|
28
|
+
self.find_button.clicked.connect(self._on_find_clicked)
|
|
29
|
+
|
|
30
|
+
find_layout.addWidget(QLabel("Find:"))
|
|
31
|
+
find_layout.addWidget(self.find_input)
|
|
32
|
+
find_layout.addWidget(self.find_button)
|
|
33
|
+
find_layout.addStretch()
|
|
34
|
+
main_layout.addLayout(find_layout)
|
|
35
|
+
|
|
36
|
+
self._search_results = []
|
|
37
|
+
self._current_search_index = -1
|
|
38
|
+
|
|
39
|
+
def clear(self):
|
|
40
|
+
""" Clear table contents """
|
|
41
|
+
self.table_widget.clear()
|
|
42
|
+
|
|
43
|
+
def load(self, path: Path, remove_columns: Optional[List[str]] = None):
|
|
44
|
+
"""Load and displays a CSV file. Remove columns if specified """
|
|
45
|
+
try:
|
|
46
|
+
print(f"Loading {path}")
|
|
47
|
+
self.table_widget.clear()
|
|
48
|
+
|
|
49
|
+
df = pd.read_csv(path)
|
|
50
|
+
if remove_columns:
|
|
51
|
+
df = df.drop(columns=remove_columns, errors='ignore')
|
|
52
|
+
|
|
53
|
+
self.table_widget.setRowCount(df.shape[0])
|
|
54
|
+
self.table_widget.setColumnCount(df.shape[1])
|
|
55
|
+
self.table_widget.setHorizontalHeaderLabels(df.columns)
|
|
56
|
+
|
|
57
|
+
for row_idx, row_data in enumerate(df.values):
|
|
58
|
+
for col_idx, value in enumerate(row_data):
|
|
59
|
+
if isinstance(value, (int, float)):
|
|
60
|
+
if value > 2147483647: # Max C++ int
|
|
61
|
+
item = QTableWidgetItem(str(value))
|
|
62
|
+
else:
|
|
63
|
+
item = QTableWidgetItem(value)
|
|
64
|
+
item.setData(Qt.ItemDataRole.EditRole, value)
|
|
65
|
+
else:
|
|
66
|
+
txt = "" if pd.isna(value) else str(value)
|
|
67
|
+
item = QTableWidgetItem(txt)
|
|
68
|
+
|
|
69
|
+
self.table_widget.setItem(row_idx, col_idx, item)
|
|
70
|
+
|
|
71
|
+
self.table_widget.resizeColumnsToContents()
|
|
72
|
+
print(" Load complete")
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"err {e}")
|
|
76
|
+
self.table_widget.setItem(0, 0, QTableWidgetItem(f"Error loading file"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _reset_search(self):
|
|
80
|
+
"""
|
|
81
|
+
Resets the search state. Called whenever the find text is changed.
|
|
82
|
+
"""
|
|
83
|
+
self.table_widget.clearSelection()
|
|
84
|
+
self._search_results = []
|
|
85
|
+
self._current_search_index = -1
|
|
86
|
+
|
|
87
|
+
def _on_find_clicked(self):
|
|
88
|
+
"""
|
|
89
|
+
Handles a click on the "Find" button.
|
|
90
|
+
- If it's a new search (or text has changed), it finds all occurrences.
|
|
91
|
+
- If it's a subsequent search, it finds the next occurrence.
|
|
92
|
+
"""
|
|
93
|
+
search_text = self.find_input.text()
|
|
94
|
+
|
|
95
|
+
# If the index is -1, it means this is a new search.
|
|
96
|
+
if self._current_search_index == -1:
|
|
97
|
+
self._search_results = self.table_widget.findItems(
|
|
98
|
+
search_text, Qt.MatchFlag.MatchContains
|
|
99
|
+
)
|
|
100
|
+
if not self._search_results:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# If we have results, move to the next one.
|
|
104
|
+
if self._search_results:
|
|
105
|
+
self._current_search_index = (self._current_search_index + 1) % len(self._search_results)
|
|
106
|
+
self._highlight_current_search_result()
|
|
107
|
+
|
|
108
|
+
def _highlight_current_search_result(self):
|
|
109
|
+
"""Scrolls to and selects the current search item."""
|
|
110
|
+
if not self._search_results:
|
|
111
|
+
return
|
|
112
|
+
item = self._search_results[self._current_search_index]
|
|
113
|
+
self.table_widget.setCurrentItem(item)
|
|
114
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# custom_editor.py
|
|
2
|
+
from PySide6.QtGui import QColor, QSyntaxHighlighter, QTextCharFormat, QFont
|
|
3
|
+
from pygments import highlight
|
|
4
|
+
from pygments.lexers import YamlLexer
|
|
5
|
+
from pygments.token import Token
|
|
6
|
+
from pygments.styles import get_style_by_name
|
|
7
|
+
|
|
8
|
+
class PygmentsHighlighter(QSyntaxHighlighter):
|
|
9
|
+
"""A QSyntaxHighlighter that uses the Pygments library for styling."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, parent):
|
|
12
|
+
super().__init__(parent)
|
|
13
|
+
|
|
14
|
+
# The lexer is responsible for breaking text into tokens
|
|
15
|
+
self.lexer = YamlLexer()
|
|
16
|
+
|
|
17
|
+
# 1. Get a Pygments style.
|
|
18
|
+
# 2. Create a dictionary mapping token types to QTextCharFormat objects.
|
|
19
|
+
self.styles = {}
|
|
20
|
+
style = get_style_by_name('dracula')
|
|
21
|
+
for token, s in style:
|
|
22
|
+
q_format = QTextCharFormat()
|
|
23
|
+
if s['color']:
|
|
24
|
+
q_format.setForeground(QColor(f"#{s['color']}"))
|
|
25
|
+
|
|
26
|
+
self.styles[token] = q_format
|
|
27
|
+
|
|
28
|
+
def highlightBlock(self, text: str):
|
|
29
|
+
"""
|
|
30
|
+
This method is called by Qt for each block of text to be highlighted.
|
|
31
|
+
"""
|
|
32
|
+
# Get the tokens from Pygments
|
|
33
|
+
tokens = self.lexer.get_tokens_unprocessed(text)
|
|
34
|
+
|
|
35
|
+
start_index = 0
|
|
36
|
+
for index, token_type, value in tokens:
|
|
37
|
+
# Find the correct style for this token
|
|
38
|
+
# We traverse up the token hierarchy (e.g., from Number.Integer to Number to Token)
|
|
39
|
+
# until we find a style defined.
|
|
40
|
+
current_format = None
|
|
41
|
+
temp_token_type = token_type
|
|
42
|
+
while current_format is None:
|
|
43
|
+
current_format = self.styles.get(temp_token_type)
|
|
44
|
+
if temp_token_type is Token:
|
|
45
|
+
break
|
|
46
|
+
temp_token_type = temp_token_type.parent
|
|
47
|
+
|
|
48
|
+
# Apply the format if one was found
|
|
49
|
+
if current_format:
|
|
50
|
+
self.setFormat(index, len(value), current_format)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# data_loader.py
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from ImportanceScore.model import get_target_column
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DataLoader:
|
|
11
|
+
"""
|
|
12
|
+
Handles loading, validating, and merging of feature and target datasets.
|
|
13
|
+
|
|
14
|
+
This class encapsulates the data loading logic, using a configuration
|
|
15
|
+
dictionary to dynamically handle different data schemas and requirements.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, model_config: dict):
|
|
19
|
+
"""
|
|
20
|
+
Initializes the DataLoader with a model configuration.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
model_config (dict): A dictionary containing model settings, including
|
|
24
|
+
the `id_column` and `regressor_target`.
|
|
25
|
+
"""
|
|
26
|
+
if not isinstance(model_config, dict):
|
|
27
|
+
raise TypeError("model_config must be a dictionary.")
|
|
28
|
+
|
|
29
|
+
self.config = model_config
|
|
30
|
+
self.id_column = self.config['id_column']
|
|
31
|
+
self.name_key = self.config['name_column']
|
|
32
|
+
self.model_name = self.config['model']
|
|
33
|
+
self.target_column = get_target_column(self.config, self.model_name)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _safe_read_csv(path, dtype=None, required_cols=None, label=None):
|
|
37
|
+
"""Internal method to safely read a CSV with validation."""
|
|
38
|
+
try:
|
|
39
|
+
df = pd.read_csv(path, dtype=dtype)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f" ❌ Error loading {label}: {path}. {e}")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
if required_cols:
|
|
45
|
+
missing = [col for col in required_cols if col not in df.columns]
|
|
46
|
+
if missing:
|
|
47
|
+
print(f" ❌ {label or path} is missing required columns: {missing}")
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
return df
|
|
50
|
+
|
|
51
|
+
def _validate_input_files(self, df_features, df_targets):
|
|
52
|
+
"""Internal method to ensure required columns exist in dataframes."""
|
|
53
|
+
required_target_cols = [self.id_column, self.target_column]
|
|
54
|
+
for col in required_target_cols:
|
|
55
|
+
if col not in df_targets.columns:
|
|
56
|
+
raise ValueError(f"Missing '{col}' in targets file.")
|
|
57
|
+
if self.id_column not in df_features.columns:
|
|
58
|
+
raise ValueError(f"Missing '{self.id_column}' in features file.")
|
|
59
|
+
|
|
60
|
+
def load_and_merge_data(self, features_path: str, targets_path: str) -> pd.DataFrame:
|
|
61
|
+
"""
|
|
62
|
+
Loads, validates, and merges feature and target data.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
features_path (str): Path to the features CSV.
|
|
66
|
+
targets_path (str): Path to the targets CSV.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
pd.DataFrame: A merged DataFrame ready for preprocessing.
|
|
70
|
+
"""
|
|
71
|
+
required_final_cols = [self.id_column, self.name_key, self.target_column]
|
|
72
|
+
|
|
73
|
+
df_features = self._safe_read_csv(
|
|
74
|
+
features_path, dtype={self.id_column: str}, required_cols=[self.id_column],
|
|
75
|
+
label="Features CSV"
|
|
76
|
+
)
|
|
77
|
+
df_targets = self._safe_read_csv(
|
|
78
|
+
targets_path, dtype={self.id_column: str},
|
|
79
|
+
required_cols=[self.id_column, self.target_column], label="Targets CSV"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
if self.name_key in df_targets.columns:
|
|
84
|
+
df_targets = df_targets.drop(columns=[self.name_key])
|
|
85
|
+
|
|
86
|
+
self._validate_input_files(df_features, df_targets)
|
|
87
|
+
df = pd.merge(df_features, df_targets, on=self.id_column, how="left")
|
|
88
|
+
|
|
89
|
+
missing = [col for col in required_final_cols if col not in df.columns]
|
|
90
|
+
if missing:
|
|
91
|
+
raise ValueError(f"Missing required columns after merge: {', '.join(missing)}")
|
|
92
|
+
|
|
93
|
+
return df
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Error loading data: {e}")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Schema for Model config
|
|
100
|
+
MODEL_CONFIG_SCHEMA = {
|
|
101
|
+
'config_type': {'type': 'string','required': True,'allowed': ["ModelConfig"] },
|
|
102
|
+
'anomaly_group_key': {'type': 'string', 'required': False},
|
|
103
|
+
'model': {
|
|
104
|
+
'type': 'string', 'required': True,
|
|
105
|
+
'allowed': ['WLM', 'RFR', 'GBT', 'LOGR', 'SVM', 'SVR', 'LR']
|
|
106
|
+
},
|
|
107
|
+
'id_column': {'type': 'string', 'required': True},
|
|
108
|
+
'name_column': {'type': 'string', 'required': True},
|
|
109
|
+
'regressor_target': {'type': 'string', 'required': True},
|
|
110
|
+
'base_score_column': {'type': 'string', 'required': False},
|
|
111
|
+
|
|
112
|
+
'scaling': {
|
|
113
|
+
'type': 'dict', 'required': False, 'schema': {
|
|
114
|
+
'min': {'type': 'number', 'required': True},
|
|
115
|
+
'max': {'type': 'number', 'required': True},
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
'text_weight_columns': {
|
|
120
|
+
'type': 'list', 'schema': {'type': 'string'}, 'required': False, 'default': []
|
|
121
|
+
}, 'one_hot_columns': {
|
|
122
|
+
'type': 'list', 'schema': {'type': 'string'}, 'required': False, 'default': []
|
|
123
|
+
}, 'fillna': {
|
|
124
|
+
'type': 'dict', 'required': False, 'valuesrules': {'type': ['number', 'string', 'boolean']}
|
|
125
|
+
}, 'conditional_adjustment': {
|
|
126
|
+
'type': 'dict', 'required': False, 'default': {}
|
|
127
|
+
}, 'ignore_columns': {
|
|
128
|
+
'type': 'list', 'schema': {'type': 'string'}, 'required': False, 'default': []
|
|
129
|
+
}, 'output_columns': {
|
|
130
|
+
'type': 'list', 'schema': {'type': 'string'}, 'required': True
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
'feature_interactions': {
|
|
134
|
+
'type': 'list', 'required': False, 'schema': {
|
|
135
|
+
'type': 'dict', 'schema': {
|
|
136
|
+
'output_column': {'type': 'string', 'required': True},
|
|
137
|
+
'input_columns': {'type': 'list', 'schema': {'type': 'string'}, 'required': True},
|
|
138
|
+
'method': {'type': 'string', 'allowed': ['logical_or'], 'required': True}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
# clip_outliers
|
|
144
|
+
'clip_outliers': {
|
|
145
|
+
'type': 'dict', 'required': False, 'valuesrules': {
|
|
146
|
+
'type': 'dict', 'schema': {
|
|
147
|
+
'method': {'type': 'string', 'allowed': ['IQR', 'threshold'], 'required': True},
|
|
148
|
+
|
|
149
|
+
# Parameters for the 'IQR' method
|
|
150
|
+
'factor': {'type': 'number', 'required': False, 'default': 1.5},
|
|
151
|
+
|
|
152
|
+
# Parameters for the new 'threshold' method
|
|
153
|
+
'min': {'type': 'number', 'required': False},
|
|
154
|
+
'max': {'type': 'number', 'required': False},
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
# scaler section
|
|
160
|
+
'scaler': {
|
|
161
|
+
'type': 'dict', 'required': False, 'valuesrules': {
|
|
162
|
+
'type': 'string',
|
|
163
|
+
'allowed': ['standard', 'minmax', 'robust', 'power', 'sublinear', 'quantile', 'none']
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
'smoothing': {
|
|
168
|
+
'type': 'dict', 'required': False, 'schema': {
|
|
169
|
+
'method': {'type': 'string', 'allowed': ['sigmoid', 'none'], 'required': True},
|
|
170
|
+
'midpoint': {'type': 'number', 'required': False},
|
|
171
|
+
'steepness': {'type': 'number', 'required': False}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
CLASSIFICATION_SCHEMA = {
|
|
179
|
+
'config_type': {'type': 'string','required': True,'allowed': ["Classification"] },
|
|
180
|
+
'keys': {'type': 'dict', 'required': True},
|
|
181
|
+
'output_tag': {'type': 'string', 'required': True},
|
|
182
|
+
'score_key': {'type': 'string', 'required': True},
|
|
183
|
+
'style': {'type': 'string', 'required': False},
|
|
184
|
+
'features': {'type': 'list', 'required': True},
|
|
185
|
+
'require_name': {
|
|
186
|
+
'type': 'boolean',
|
|
187
|
+
'required': False, # It's an optional setting
|
|
188
|
+
'default': True # A sensible default: usually we want named features
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
'debug_ids': {
|
|
192
|
+
'type': 'list', 'schema': {'type': 'integer'}, # assuming node_row IDs are integers
|
|
193
|
+
'required': False,
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
'enrichment': {
|
|
197
|
+
'type': 'list',
|
|
198
|
+
'required': False,
|
|
199
|
+
'schema': {
|
|
200
|
+
'type': 'dict',
|
|
201
|
+
'schema': {
|
|
202
|
+
'file_suffix': {'type': 'string', 'required': True},
|
|
203
|
+
'columns': {
|
|
204
|
+
'type': 'list',
|
|
205
|
+
'required': True,
|
|
206
|
+
'schema': {'type': 'string'}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# features.py
|
|
2
|
+
"""
|
|
3
|
+
This module provides a robust FeaturePreprocessor class that acts as a
|
|
4
|
+
data-driven orchestrator for the feature preprocessing pipeline.
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Tuple, Dict, Any, List
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from ImportanceScore import transformations
|
|
13
|
+
from ImportanceScore.model import get_target_column
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# --- Module-Level Helper for Logging ---
|
|
17
|
+
def _setup_logger(level) -> logging.Logger:
|
|
18
|
+
"""Initializes and configures the logger for the feature pipeline."""
|
|
19
|
+
logger = logging.getLogger("__file__")
|
|
20
|
+
logger.setLevel(level)
|
|
21
|
+
if not logger.handlers:
|
|
22
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
23
|
+
formatter = logging.Formatter("%(message)s")
|
|
24
|
+
handler.setFormatter(formatter)
|
|
25
|
+
logger.addHandler(handler)
|
|
26
|
+
return logger
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FeaturePreprocessor:
|
|
30
|
+
"""
|
|
31
|
+
Orchestrates the feature preprocessing pipeline by executing a configurable
|
|
32
|
+
sequence of independent transformation strategies.
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, model_config: Dict[str, Any], category: str, model_name: str):
|
|
35
|
+
"""Initializes the FeaturePreprocessor."""
|
|
36
|
+
self.config = model_config
|
|
37
|
+
self.category = category
|
|
38
|
+
self.model_name = model_name
|
|
39
|
+
self.target_col_name = get_target_column(self.config, self.model_name)
|
|
40
|
+
self.logger = _setup_logger(4)
|
|
41
|
+
|
|
42
|
+
# This property will store the names of columns created during one-hot encoding.
|
|
43
|
+
self.one_hot_column_names: List[str] = []
|
|
44
|
+
|
|
45
|
+
self.pipeline_steps = [
|
|
46
|
+
("feature_interactions", transformations.apply_feature_interactions),
|
|
47
|
+
("fillna", lambda df, cfg, log: transformations.apply_fillna(
|
|
48
|
+
df, cfg, [self.target_col_name], log, self.category
|
|
49
|
+
)),
|
|
50
|
+
("text_weight_columns", lambda df, cfg, log: transformations.apply_text_weight_scoring(
|
|
51
|
+
df, {"columns": cfg}, self.category, log
|
|
52
|
+
)),
|
|
53
|
+
("conditional_adjustment", transformations.apply_conditional_adjustment),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def transform(
|
|
57
|
+
self, raw_dataframe: pd.DataFrame, training: bool = True
|
|
58
|
+
) -> Tuple[pd.DataFrame, pd.Series, pd.Series]:
|
|
59
|
+
"""Executes the full preprocessing pipeline in the correct order."""
|
|
60
|
+
self.logger.info("\n🔷Preprocessing Features:\n")
|
|
61
|
+
self.logger.info(f" {len(raw_dataframe)} records.")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
processed_df = raw_dataframe.copy()
|
|
65
|
+
|
|
66
|
+
# --- Step 1: Handle the special case of one-hot encoding first ---
|
|
67
|
+
one_hot_config = {"columns": self.config.get("one_hot_columns", [])}
|
|
68
|
+
processed_df, self.one_hot_column_names = transformations.apply_one_hot_encoding(
|
|
69
|
+
processed_df, one_hot_config, self.logger
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# --- Step 2: Run the rest of the data-driven pipeline ---
|
|
73
|
+
for config_key, transform_func in self.pipeline_steps:
|
|
74
|
+
step_config = self.config.get(config_key, {})
|
|
75
|
+
processed_df = transform_func(processed_df, step_config, self.logger)
|
|
76
|
+
|
|
77
|
+
# --- Step 3: Post-Transformation Orchestration ---
|
|
78
|
+
y, labeled_mask = self._extract_labels(processed_df, training)
|
|
79
|
+
feature_matrix_X = self._prune_columns(processed_df)
|
|
80
|
+
feature_matrix_X = transformations.clip_outliers(
|
|
81
|
+
feature_matrix_X, self.config.get("clip_outliers", {}), self.logger
|
|
82
|
+
)
|
|
83
|
+
feature_matrix_X = transformations.scale_features(
|
|
84
|
+
feature_matrix_X, self.config.get("scaler", {}), self.logger
|
|
85
|
+
)
|
|
86
|
+
self._validate_feature_coverage(feature_matrix_X)
|
|
87
|
+
|
|
88
|
+
return feature_matrix_X, y, labeled_mask
|
|
89
|
+
|
|
90
|
+
except (ValueError, KeyError) as e:
|
|
91
|
+
self.logger.error(f"\n ❌ Preprocessing Error: {e}")
|
|
92
|
+
self.logger.error(" Processing stopped. Please correct the error above and rerun.")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
# --- Orchestration-Level Helper Methods ---
|
|
96
|
+
|
|
97
|
+
def _extract_labels(self, df: pd.DataFrame, training: bool) -> Tuple[pd.Series, pd.Series]:
|
|
98
|
+
"""Extracts the target variable (y) and a mask of labeled rows."""
|
|
99
|
+
if not training:
|
|
100
|
+
return pd.Series([pd.NA] * len(df), dtype="Int64"), pd.Series([False] * len(df))
|
|
101
|
+
|
|
102
|
+
if self.target_col_name not in df.columns:
|
|
103
|
+
raise KeyError(
|
|
104
|
+
f"Label column '{self.target_col_name}' is required for training but is missing."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
dtype = "Int64" if self.config.get("task_type") == "classification" else "float64"
|
|
108
|
+
y = df[self.target_col_name].astype(dtype)
|
|
109
|
+
return y, y.notna()
|
|
110
|
+
|
|
111
|
+
def _prune_columns(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
112
|
+
"""Removes ignored and non-numeric columns to create the feature matrix."""
|
|
113
|
+
self.logger.info("➡️ Pruning columns to create feature matrix...")
|
|
114
|
+
|
|
115
|
+
ignore_cols = self.config.get("ignore_columns", [])
|
|
116
|
+
df.drop(columns=ignore_cols, errors="ignore", inplace=True)
|
|
117
|
+
|
|
118
|
+
numeric_dtypes = ["int64", "float64", "bool"]
|
|
119
|
+
non_numeric_cols = df.select_dtypes(exclude=numeric_dtypes).columns.tolist()
|
|
120
|
+
df.drop(columns=non_numeric_cols, errors="ignore", inplace=True)
|
|
121
|
+
|
|
122
|
+
self.logger.info(f" • Ignored columns dropped: {ignore_cols}")
|
|
123
|
+
self.logger.info(f" • Non-numeric columns dropped: {non_numeric_cols or 'none'}")
|
|
124
|
+
|
|
125
|
+
return df
|
|
126
|
+
|
|
127
|
+
def _validate_feature_coverage(self, X: pd.DataFrame) -> None:
|
|
128
|
+
"""Validates that each feature column has some non-zero coverage."""
|
|
129
|
+
self.logger.info("\n✳️ Feature column coverage:\n")
|
|
130
|
+
for col in X.columns:
|
|
131
|
+
_validate_tag(X, col, threshold=1, logger=self.logger)
|
|
132
|
+
|
|
133
|
+
self.logger.info("\n")
|
|
134
|
+
|
|
135
|
+
# --- Utility Functions ---
|
|
136
|
+
|
|
137
|
+
def _validate_tag(df: pd.DataFrame, column: str, threshold: float = None, logger=None) -> bool:
|
|
138
|
+
"""Ensures a given column has sufficient non-zero coverage."""
|
|
139
|
+
if column not in df.columns:
|
|
140
|
+
logger.info(f"❌ Column '{column}' not found for coverage validation.")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
pct_filled = df[column].fillna(0).ne(0).mean()
|
|
144
|
+
|
|
145
|
+
if threshold is None:
|
|
146
|
+
logger.info(f"↪ Column '{column}': {pct_filled:.0%} filled")
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
is_valid = pct_filled >= (threshold / 100)
|
|
150
|
+
status_emoji = "🆗" if is_valid else "⚠️"
|
|
151
|
+
column_name = f"{column}:"
|
|
152
|
+
logger.info(
|
|
153
|
+
f" {status_emoji} {column_name:<20} {pct_filled:>7.2%} filled"
|
|
154
|
+
)
|
|
155
|
+
return is_valid
|