classifier-toolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- classifier_toolkit/eda/__init__.py +19 -0
- classifier_toolkit/eda/bivariate_analysis.py +413 -0
- classifier_toolkit/eda/eda_toolkit.py +549 -0
- classifier_toolkit/eda/feature_engineering.py +1310 -0
- classifier_toolkit/eda/first_glance.py +253 -0
- classifier_toolkit/eda/univariate_analysis.py +778 -0
- classifier_toolkit/eda/visualizations.py +1248 -0
- classifier_toolkit/eda/warnings/__init__.py +4 -0
- classifier_toolkit/eda/warnings/automated_warnings.py +20 -0
- classifier_toolkit/eda/warnings/default_warnings.py +286 -0
- classifier_toolkit/feature_selection/__init__.py +33 -0
- classifier_toolkit/feature_selection/base.py +182 -0
- classifier_toolkit/feature_selection/embedded_methods/__init__.py +7 -0
- classifier_toolkit/feature_selection/embedded_methods/elastic_net.py +178 -0
- classifier_toolkit/feature_selection/meta_selector.py +329 -0
- classifier_toolkit/feature_selection/utils/__init__.py +17 -0
- classifier_toolkit/feature_selection/utils/plottings.py +65 -0
- classifier_toolkit/feature_selection/utils/scoring.py +86 -0
- classifier_toolkit/feature_selection/wrapper_methods/__init__.py +11 -0
- classifier_toolkit/feature_selection/wrapper_methods/rfe.py +345 -0
- classifier_toolkit/feature_selection/wrapper_methods/sequential_selection.py +566 -0
- classifier_toolkit/model_fitting/__init__.py +0 -0
- classifier_toolkit/model_fitting/model_search.py +3 -0
- classifier_toolkit-0.1.0.dist-info/METADATA +99 -0
- classifier_toolkit-0.1.0.dist-info/RECORD +26 -0
- classifier_toolkit-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1248 @@
|
|
|
1
|
+
import colorsys
|
|
2
|
+
import math
|
|
3
|
+
from typing import List, Literal, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import plotly.express as px
|
|
9
|
+
import plotly.graph_objects as go
|
|
10
|
+
import statsmodels.api as sm
|
|
11
|
+
from pandas.core.indexes.base import Index as PandasIndex
|
|
12
|
+
from plotly.subplots import make_subplots
|
|
13
|
+
from sklearn.linear_model import LinearRegression
|
|
14
|
+
from sklearn.preprocessing import MinMaxScaler
|
|
15
|
+
from statsmodels.stats.outliers_influence import variance_inflation_factor
|
|
16
|
+
from tqdm import tqdm
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Visualizations:
|
|
20
|
+
PHI = (1 + 5**0.5) / 2
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
dataframe: pd.DataFrame,
|
|
25
|
+
target_column: str,
|
|
26
|
+
numerical_columns: List[str],
|
|
27
|
+
categorical_columns: List[str],
|
|
28
|
+
) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Initialize the Visualizations class with data and configuration.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
dataframe : pd.DataFrame
|
|
35
|
+
The data to be analyzed.
|
|
36
|
+
target_column : str
|
|
37
|
+
The target column for analysis.
|
|
38
|
+
numerical_columns : List[str]
|
|
39
|
+
List of numerical columns.
|
|
40
|
+
categorical_columns : List[str]
|
|
41
|
+
List of categorical columns.
|
|
42
|
+
"""
|
|
43
|
+
self.data = dataframe
|
|
44
|
+
self.target_column = target_column
|
|
45
|
+
self.numerical_columns = numerical_columns
|
|
46
|
+
self.categorical_columns = categorical_columns
|
|
47
|
+
|
|
48
|
+
def plot_target_dist_by_category(self, column: str) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Plot the distribution of a categorical column with respect to the target column.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
column : str
|
|
55
|
+
Name of the column to plot.
|
|
56
|
+
"""
|
|
57
|
+
aggregated = self.data[column].value_counts().reset_index()
|
|
58
|
+
aggregated.columns = PandasIndex([column, "total_count"])
|
|
59
|
+
|
|
60
|
+
positive_cases = (
|
|
61
|
+
self.data[self.data[self.target_column] == 1][column]
|
|
62
|
+
.value_counts()
|
|
63
|
+
.reset_index()
|
|
64
|
+
)
|
|
65
|
+
positive_cases.columns = PandasIndex([column, "positive_count"])
|
|
66
|
+
|
|
67
|
+
merged_data = pd.merge(aggregated, positive_cases, on=column, how="left")
|
|
68
|
+
merged_data["positive_count"] = merged_data["positive_count"].fillna(0)
|
|
69
|
+
merged_data["positive_info"] = (
|
|
70
|
+
merged_data["positive_count"].astype(int).astype(str)
|
|
71
|
+
+ f" {self.target_column.capitalize()} Cases"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
figure = px.pie(
|
|
75
|
+
merged_data,
|
|
76
|
+
values="total_count",
|
|
77
|
+
names=column,
|
|
78
|
+
hole=0.5,
|
|
79
|
+
title=f"Distribution of {column} with {self.target_column.capitalize()} Events",
|
|
80
|
+
hover_data=["positive_info"],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
figure.show()
|
|
84
|
+
|
|
85
|
+
def plot_target_balance(self) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Plot the distribution of the target column.
|
|
88
|
+
"""
|
|
89
|
+
value_counts = self.data[self.target_column].value_counts().reset_index()
|
|
90
|
+
value_counts.columns = PandasIndex(["Class", "Count"])
|
|
91
|
+
|
|
92
|
+
total = value_counts["Count"].sum()
|
|
93
|
+
value_counts["Percentage"] = value_counts["Count"] / total * 100
|
|
94
|
+
|
|
95
|
+
fig = px.pie(
|
|
96
|
+
value_counts,
|
|
97
|
+
values="Count",
|
|
98
|
+
names="Class",
|
|
99
|
+
title=f"Distribution of {self.target_column}",
|
|
100
|
+
labels={"Class": self.target_column, "Count": "Number of Instances"},
|
|
101
|
+
hover_data=["Percentage"],
|
|
102
|
+
hole=0,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
fig.update_traces(
|
|
106
|
+
textposition="inside",
|
|
107
|
+
textinfo="percent+label",
|
|
108
|
+
insidetextorientation="radial",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
fig.update_layout(
|
|
112
|
+
height=500,
|
|
113
|
+
legend_title=self.target_column,
|
|
114
|
+
legend={
|
|
115
|
+
"orientation": "h",
|
|
116
|
+
"yanchor": "bottom",
|
|
117
|
+
"y": 1.02,
|
|
118
|
+
"xanchor": "right",
|
|
119
|
+
"x": 1,
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
fig.show()
|
|
123
|
+
|
|
124
|
+
def plot_target_evolution(self, time_column: str, amount_column: str) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Plot the evolution of the target column over time.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
time_column : str
|
|
131
|
+
Name of the time column.
|
|
132
|
+
amount_column : str
|
|
133
|
+
Name of the amount column.
|
|
134
|
+
"""
|
|
135
|
+
# Ensure the time_column exists in the DataFrame
|
|
136
|
+
if time_column not in self.data.columns:
|
|
137
|
+
raise ValueError(f"Column '{time_column}' does not exist in the DataFrame.")
|
|
138
|
+
|
|
139
|
+
# Try to convert the time_column to datetime using the correct format
|
|
140
|
+
try:
|
|
141
|
+
self.data[time_column] = pd.to_datetime(
|
|
142
|
+
self.data[time_column], format="%Y-%m-%d %H:%M:%S.%f%z"
|
|
143
|
+
)
|
|
144
|
+
except ValueError:
|
|
145
|
+
# If it fails, coerce errors to NaT and drop rows with NaT
|
|
146
|
+
self.data[time_column] = pd.to_datetime(
|
|
147
|
+
self.data[time_column], errors="coerce"
|
|
148
|
+
)
|
|
149
|
+
self.data.dropna(subset=[time_column], inplace=True)
|
|
150
|
+
|
|
151
|
+
# Ensure the target column is boolean
|
|
152
|
+
self.data[self.target_column] = self.data[self.target_column].astype(bool)
|
|
153
|
+
|
|
154
|
+
# Group by time and calculate the fraud rate and observation count
|
|
155
|
+
freq = "MS"
|
|
156
|
+
data = self.data.copy()
|
|
157
|
+
|
|
158
|
+
# 1. Population Risk Rate
|
|
159
|
+
risk_rate_df = data.groupby(
|
|
160
|
+
[pd.Grouper(key=time_column, freq=freq), self.target_column]
|
|
161
|
+
).size()
|
|
162
|
+
assert isinstance(risk_rate_df, pd.Series)
|
|
163
|
+
risk_rate = risk_rate_df.reset_index(name="count")
|
|
164
|
+
risk_rate["rate"] = risk_rate.groupby(time_column)["count"].transform(
|
|
165
|
+
lambda x: x / x.sum()
|
|
166
|
+
)
|
|
167
|
+
risk_rate_fraud = risk_rate[risk_rate[self.target_column] == True]
|
|
168
|
+
|
|
169
|
+
# 2. Population's Observation Number
|
|
170
|
+
obs_count = (
|
|
171
|
+
data.groupby([pd.Grouper(key=time_column, freq=freq)])[self.target_column]
|
|
172
|
+
.count()
|
|
173
|
+
.reset_index(name="count")
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# 3. Population Risk Rate in EUR
|
|
177
|
+
fraud_amount = data.groupby(
|
|
178
|
+
[self.target_column, pd.Grouper(key=time_column, freq=freq)]
|
|
179
|
+
)[amount_column].sum()
|
|
180
|
+
fraud_amount = (
|
|
181
|
+
fraud_amount / fraud_amount.groupby(level=1).sum()
|
|
182
|
+
).reset_index()
|
|
183
|
+
fraud_amount_fraud = fraud_amount[fraud_amount[self.target_column] == True]
|
|
184
|
+
|
|
185
|
+
# 4. Population's Observation in EUR
|
|
186
|
+
amount_sum = (
|
|
187
|
+
data.groupby([pd.Grouper(key=time_column, freq=freq)])[amount_column]
|
|
188
|
+
.sum()
|
|
189
|
+
.reset_index()
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Create subplots
|
|
193
|
+
fig = make_subplots(
|
|
194
|
+
rows=2,
|
|
195
|
+
cols=2,
|
|
196
|
+
subplot_titles=(
|
|
197
|
+
"Population Risk Rate",
|
|
198
|
+
"Population's Observation Number",
|
|
199
|
+
"Population Risk Rate (in EUR)",
|
|
200
|
+
"Population's Observation (in EUR)",
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Plot risk rate
|
|
205
|
+
fig.add_trace(
|
|
206
|
+
go.Scatter(
|
|
207
|
+
x=risk_rate_fraud[time_column],
|
|
208
|
+
y=risk_rate_fraud["rate"],
|
|
209
|
+
mode="lines",
|
|
210
|
+
name="Population Fraud Rate",
|
|
211
|
+
),
|
|
212
|
+
row=1,
|
|
213
|
+
col=1,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Plot observation count
|
|
217
|
+
fig.add_trace(
|
|
218
|
+
go.Bar(
|
|
219
|
+
x=obs_count[time_column],
|
|
220
|
+
y=obs_count["count"],
|
|
221
|
+
name="Population Observation Count",
|
|
222
|
+
),
|
|
223
|
+
row=1,
|
|
224
|
+
col=2,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Plot risk rate in EUR
|
|
228
|
+
fig.add_trace(
|
|
229
|
+
go.Scatter(
|
|
230
|
+
x=fraud_amount_fraud[time_column],
|
|
231
|
+
y=fraud_amount_fraud[amount_column],
|
|
232
|
+
mode="lines",
|
|
233
|
+
name="Population Fraud Rate (in EUR)",
|
|
234
|
+
),
|
|
235
|
+
row=2,
|
|
236
|
+
col=1,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Plot observation amount in EUR
|
|
240
|
+
fig.add_trace(
|
|
241
|
+
go.Bar(
|
|
242
|
+
x=amount_sum[time_column],
|
|
243
|
+
y=amount_sum[amount_column],
|
|
244
|
+
name="Population Observation (in EUR)",
|
|
245
|
+
),
|
|
246
|
+
row=2,
|
|
247
|
+
col=2,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Update layout
|
|
251
|
+
fig.update_layout(
|
|
252
|
+
title_text="Target Evolution Over Time", height=800, showlegend=True
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
fig.update_xaxes(title_text="Time", row=1, col=1)
|
|
256
|
+
fig.update_yaxes(title_text="Risk Rate", row=1, col=1)
|
|
257
|
+
fig.update_xaxes(title_text="Time", row=1, col=2)
|
|
258
|
+
fig.update_yaxes(title_text="Nb obs", row=1, col=2)
|
|
259
|
+
fig.update_xaxes(title_text="Time", row=2, col=1)
|
|
260
|
+
fig.update_yaxes(title_text="Risk Rate (in EUR)", row=2, col=1)
|
|
261
|
+
fig.update_xaxes(title_text="Time", row=2, col=2)
|
|
262
|
+
fig.update_yaxes(title_text="Sum of EUR", row=2, col=2)
|
|
263
|
+
|
|
264
|
+
fig.show()
|
|
265
|
+
|
|
266
|
+
def _check_cardinality(self, threshold: int = 10) -> Tuple[List[str], List[str]]:
|
|
267
|
+
"""
|
|
268
|
+
Check the cardinality of categorical columns.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
threshold : int, optional
|
|
273
|
+
Threshold to determine high and low cardinality, by default 10.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
Tuple[List[str], List[str]]
|
|
278
|
+
Lists of high and low cardinality columns.
|
|
279
|
+
"""
|
|
280
|
+
high_car = [
|
|
281
|
+
cat
|
|
282
|
+
for cat in self.categorical_columns
|
|
283
|
+
if len(self.data[cat].value_counts()) >= threshold
|
|
284
|
+
]
|
|
285
|
+
low_car = [
|
|
286
|
+
cat
|
|
287
|
+
for cat in self.categorical_columns
|
|
288
|
+
if len(self.data[cat].value_counts()) < threshold
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
return high_car, low_car
|
|
292
|
+
|
|
293
|
+
def visualize_high_cardinality(
|
|
294
|
+
self,
|
|
295
|
+
excluded_vars: List[str],
|
|
296
|
+
top_n: int = 10,
|
|
297
|
+
id_column: Optional[str] = None,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Visualize high cardinality features.
|
|
301
|
+
|
|
302
|
+
Parameters
|
|
303
|
+
----------
|
|
304
|
+
excluded_vars : List[str]
|
|
305
|
+
List of columns to exclude.
|
|
306
|
+
top_n : int, optional
|
|
307
|
+
Number of top categories to display, by default 10.
|
|
308
|
+
id_column : Optional[str], optional
|
|
309
|
+
Name of the id column, by default None.
|
|
310
|
+
"""
|
|
311
|
+
high_car, _ = self._check_cardinality()
|
|
312
|
+
|
|
313
|
+
if id_column:
|
|
314
|
+
excluded_vars.append(id_column)
|
|
315
|
+
|
|
316
|
+
# Exclude specified variables from the high cardinality list
|
|
317
|
+
high_car = [cat for cat in high_car if cat not in excluded_vars]
|
|
318
|
+
|
|
319
|
+
if not high_car:
|
|
320
|
+
print("No high cardinality features found.")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
fig = go.Figure()
|
|
324
|
+
# Create traces for each high cardinality column but initially hide them
|
|
325
|
+
for _i, cat in enumerate(high_car):
|
|
326
|
+
value_counts = self.data[cat].value_counts()
|
|
327
|
+
|
|
328
|
+
# Keep top N categories and group others
|
|
329
|
+
top_categories = value_counts.nlargest(top_n - 1).index
|
|
330
|
+
draw = self.data[cat].apply(
|
|
331
|
+
lambda x: x if x in top_categories else "Other" # noqa: B023
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
draw = draw.value_counts().sort_values(ascending=True)
|
|
335
|
+
|
|
336
|
+
fig.add_trace(
|
|
337
|
+
go.Bar(
|
|
338
|
+
y=draw.index,
|
|
339
|
+
x=draw.values,
|
|
340
|
+
orientation="h",
|
|
341
|
+
name=cat,
|
|
342
|
+
text=draw.values,
|
|
343
|
+
textposition="auto",
|
|
344
|
+
marker={"color": draw.values, "colorscale": "Plasma"},
|
|
345
|
+
visible=False, # Initially hide all traces
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Add dropdown menu to toggle between traces
|
|
350
|
+
buttons = []
|
|
351
|
+
for i, cat in enumerate(high_car):
|
|
352
|
+
buttons.append(
|
|
353
|
+
{
|
|
354
|
+
"method": "update",
|
|
355
|
+
"label": cat,
|
|
356
|
+
"args": [
|
|
357
|
+
{"visible": [j == i for j in range(len(high_car))]},
|
|
358
|
+
{
|
|
359
|
+
"title": f"Distribution of {cat}",
|
|
360
|
+
"xaxis": {"title": "Count"},
|
|
361
|
+
"yaxis": {"title": "Category"},
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
fig.update_layout(
|
|
368
|
+
updatemenus=[
|
|
369
|
+
{
|
|
370
|
+
"active": 0,
|
|
371
|
+
"buttons": buttons,
|
|
372
|
+
"direction": "down",
|
|
373
|
+
"showactive": True,
|
|
374
|
+
}
|
|
375
|
+
]
|
|
376
|
+
)
|
|
377
|
+
# if I assert, this create a bug in the example for no reason
|
|
378
|
+
if len(fig.data) > 0: # type: ignore
|
|
379
|
+
fig.data[0].visible = True # type: ignore
|
|
380
|
+
fig.update_layout(title=f"Distribution of {high_car[0]}", showlegend=False)
|
|
381
|
+
|
|
382
|
+
fig.show()
|
|
383
|
+
|
|
384
|
+
def visualize_low_cardinality(self) -> None:
|
|
385
|
+
"""
|
|
386
|
+
Visualize low cardinality features.
|
|
387
|
+
"""
|
|
388
|
+
_, low_car = self._check_cardinality()
|
|
389
|
+
|
|
390
|
+
if not low_car:
|
|
391
|
+
print("No low cardinality features found.")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
# Calculate number of rows and columns
|
|
395
|
+
n_cols = 2
|
|
396
|
+
n_rows = math.ceil(len(low_car) / n_cols)
|
|
397
|
+
|
|
398
|
+
# Create subplots
|
|
399
|
+
fig = make_subplots(
|
|
400
|
+
rows=n_rows,
|
|
401
|
+
cols=n_cols,
|
|
402
|
+
subplot_titles=low_car,
|
|
403
|
+
vertical_spacing=0.1,
|
|
404
|
+
horizontal_spacing=0.05,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Plot each low cardinality feature
|
|
408
|
+
for idx, cat in enumerate(low_car):
|
|
409
|
+
row = idx // n_cols + 1
|
|
410
|
+
col = idx % n_cols + 1
|
|
411
|
+
|
|
412
|
+
value_counts = self.data[cat].value_counts().sort_values(ascending=True)
|
|
413
|
+
|
|
414
|
+
fig.add_trace(
|
|
415
|
+
go.Bar(
|
|
416
|
+
x=value_counts.index,
|
|
417
|
+
y=value_counts.values,
|
|
418
|
+
name=cat,
|
|
419
|
+
text=value_counts.values,
|
|
420
|
+
textposition="auto",
|
|
421
|
+
),
|
|
422
|
+
row=row,
|
|
423
|
+
col=col,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
fig.update_xaxes(title_text=cat, row=row, col=col)
|
|
427
|
+
fig.update_yaxes(title_text="Count", row=row, col=col)
|
|
428
|
+
|
|
429
|
+
# Update layout
|
|
430
|
+
fig.update_layout(
|
|
431
|
+
title_text="Distribution of Low Cardinality Categorical Features",
|
|
432
|
+
showlegend=False,
|
|
433
|
+
height=300 * n_rows,
|
|
434
|
+
width=1000,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
fig.show()
|
|
438
|
+
|
|
439
|
+
def calculate_vif(self, threshold: int = 5, show_plot: bool = True) -> pd.DataFrame:
|
|
440
|
+
"""
|
|
441
|
+
Calculate the Variance Inflation Factor (VIF) for numerical features.
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
threshold : int, optional
|
|
446
|
+
Threshold value to determine multicollinearity, by default 5.
|
|
447
|
+
show_plot : bool, optional
|
|
448
|
+
If True, the plot will be displayed, by default True.
|
|
449
|
+
|
|
450
|
+
Returns
|
|
451
|
+
-------
|
|
452
|
+
pd.DataFrame
|
|
453
|
+
DataFrame containing VIF values for features.
|
|
454
|
+
"""
|
|
455
|
+
# Select numerical data from the DataFrame, excluding the target column
|
|
456
|
+
numerical_columns = [
|
|
457
|
+
col for col in self.numerical_columns if col != self.target_column
|
|
458
|
+
]
|
|
459
|
+
numerical_data = self.data[numerical_columns].copy()
|
|
460
|
+
|
|
461
|
+
# Convert boolean columns to integers
|
|
462
|
+
for column in numerical_data.columns:
|
|
463
|
+
if numerical_data[column].dtype == "bool":
|
|
464
|
+
numerical_data[column] = numerical_data[column].astype(int)
|
|
465
|
+
|
|
466
|
+
# Check for missing values
|
|
467
|
+
missing_values = numerical_data.isnull().sum()
|
|
468
|
+
if missing_values.sum() > 0:
|
|
469
|
+
print("\nWARNING: Missing values detected in the following columns:")
|
|
470
|
+
print(missing_values[missing_values > 0])
|
|
471
|
+
print("\nImputing missing values with mean for VIF calculation.")
|
|
472
|
+
print(
|
|
473
|
+
"It is strongly recommended to properly handle missing values in your dataset."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Impute missing values with mean
|
|
477
|
+
numerical_data = numerical_data.fillna(numerical_data.mean())
|
|
478
|
+
|
|
479
|
+
# Add a constant to the model (intercept)
|
|
480
|
+
numerical_data = sm.add_constant(numerical_data)
|
|
481
|
+
assert isinstance(numerical_data, pd.DataFrame)
|
|
482
|
+
|
|
483
|
+
# Calculate VIF for each feature
|
|
484
|
+
vif_data = pd.DataFrame()
|
|
485
|
+
vif_data["Feature"] = numerical_data.columns
|
|
486
|
+
vif_data["VIF"] = [
|
|
487
|
+
variance_inflation_factor(numerical_data.values, i)
|
|
488
|
+
for i in range(numerical_data.shape[1])
|
|
489
|
+
]
|
|
490
|
+
|
|
491
|
+
# Handle infinite VIF values
|
|
492
|
+
vif_data["VIF"] = np.where(
|
|
493
|
+
np.isinf(vif_data["VIF"]), np.finfo(np.float64).max, vif_data["VIF"]
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Drop the constant column from the dataframe
|
|
497
|
+
vif_data = vif_data[vif_data["Feature"] != "const"]
|
|
498
|
+
|
|
499
|
+
# Sort VIF values in descending order
|
|
500
|
+
vif_data = vif_data.sort_values("VIF", ascending=False).reset_index(drop=True)
|
|
501
|
+
|
|
502
|
+
# Assign colors based on the threshold
|
|
503
|
+
vif_data["Color"] = [
|
|
504
|
+
"red" if vif > threshold else "green" for vif in vif_data["VIF"]
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
# Create the bar plot
|
|
508
|
+
fig = go.Figure()
|
|
509
|
+
fig.add_trace(
|
|
510
|
+
go.Bar(
|
|
511
|
+
x=vif_data["Feature"],
|
|
512
|
+
y=vif_data["VIF"],
|
|
513
|
+
marker_color=vif_data["Color"],
|
|
514
|
+
text=vif_data["VIF"].round(2),
|
|
515
|
+
textposition="outside",
|
|
516
|
+
)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# Update layout
|
|
520
|
+
fig.update_layout(
|
|
521
|
+
title="Variance Inflation Factor (VIF) for Features",
|
|
522
|
+
xaxis_title="Features",
|
|
523
|
+
yaxis_title="VIF",
|
|
524
|
+
yaxis={
|
|
525
|
+
"range": [0, max(vif_data["VIF"]) * 1.1]
|
|
526
|
+
}, # Set y-axis range with some padding
|
|
527
|
+
showlegend=False,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Add a horizontal line at the threshold value
|
|
531
|
+
fig.add_shape(
|
|
532
|
+
type="line",
|
|
533
|
+
x0=-0.5,
|
|
534
|
+
y0=threshold,
|
|
535
|
+
x1=len(vif_data["Feature"]) - 0.5,
|
|
536
|
+
y1=threshold,
|
|
537
|
+
line={"color": "black", "width": 2, "dash": "dash"},
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
if show_plot:
|
|
541
|
+
fig.show()
|
|
542
|
+
|
|
543
|
+
return vif_data
|
|
544
|
+
|
|
545
|
+
def _bin_and_target_freq(
|
|
546
|
+
self, variable_name: str, n_bins: int = 10, scale: bool = False
|
|
547
|
+
) -> pd.DataFrame:
|
|
548
|
+
"""
|
|
549
|
+
Bin a numerical variable and calculate target frequency.
|
|
550
|
+
|
|
551
|
+
Parameters
|
|
552
|
+
----------
|
|
553
|
+
variable_name : str
|
|
554
|
+
Name of the numerical variable.
|
|
555
|
+
n_bins : int, optional
|
|
556
|
+
Number of bins to use, by default 10.
|
|
557
|
+
scale : bool, optional
|
|
558
|
+
Whether to scale the variable before binning, by default False.
|
|
559
|
+
|
|
560
|
+
Returns
|
|
561
|
+
-------
|
|
562
|
+
pd.DataFrame
|
|
563
|
+
DataFrame with binned data and target frequency.
|
|
564
|
+
"""
|
|
565
|
+
data = self.data[[variable_name, self.target_column]].copy()
|
|
566
|
+
|
|
567
|
+
# Ensure target column is numeric (0 or 1)
|
|
568
|
+
data[self.target_column] = data[self.target_column].astype(int)
|
|
569
|
+
|
|
570
|
+
# Drop missing values and inform user
|
|
571
|
+
original_count = len(data)
|
|
572
|
+
data.dropna(subset=[variable_name], inplace=True)
|
|
573
|
+
dropped_count = original_count - len(data)
|
|
574
|
+
if dropped_count > 0:
|
|
575
|
+
print(
|
|
576
|
+
f"Dropped {dropped_count} rows with missing values for {variable_name}"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
if scale:
|
|
580
|
+
# Scale the variable individually
|
|
581
|
+
min_val = data[variable_name].min()
|
|
582
|
+
max_val = data[variable_name].max()
|
|
583
|
+
if min_val != max_val:
|
|
584
|
+
data[variable_name] = (data[variable_name] - min_val) / (
|
|
585
|
+
max_val - min_val
|
|
586
|
+
)
|
|
587
|
+
else:
|
|
588
|
+
print(
|
|
589
|
+
f"Warning: {variable_name} has constant value. Scaling not applied."
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
# Use 'drop' option to handle duplicate bin edges
|
|
593
|
+
try:
|
|
594
|
+
data["bin"] = pd.qcut(
|
|
595
|
+
data[variable_name], q=n_bins, labels=False, duplicates="drop"
|
|
596
|
+
)
|
|
597
|
+
except ValueError as e:
|
|
598
|
+
print(f"Warning: {e}")
|
|
599
|
+
print(
|
|
600
|
+
f"Attempting to create {n_bins} bins for {variable_name}, but some may be merged due to duplicate values."
|
|
601
|
+
)
|
|
602
|
+
# If qcut fails, fall back to cut with equal-width bins
|
|
603
|
+
data["bin"] = pd.cut(data[variable_name], bins=n_bins, labels=False)
|
|
604
|
+
|
|
605
|
+
grouped = data.groupby("bin").agg(
|
|
606
|
+
{variable_name: "mean", self.target_column: ["count", "sum"]}
|
|
607
|
+
)
|
|
608
|
+
grouped.columns = PandasIndex([variable_name, "total_count", "target_sum"])
|
|
609
|
+
grouped["target_freq"] = grouped["target_sum"] / grouped["total_count"]
|
|
610
|
+
return grouped.reset_index()
|
|
611
|
+
|
|
612
|
+
def plot_hexagon_target_vs_numerical(
|
|
613
|
+
self, var1: str, var2: str, n_bins: int = 20, scale: bool = False
|
|
614
|
+
) -> None:
|
|
615
|
+
"""
|
|
616
|
+
Create a hexbin plot comparing two numerical variables and their relationship to the target variable.
|
|
617
|
+
|
|
618
|
+
Parameters
|
|
619
|
+
----------
|
|
620
|
+
var1 : str
|
|
621
|
+
Name of the first numerical variable.
|
|
622
|
+
var2 : str
|
|
623
|
+
Name of the second numerical variable.
|
|
624
|
+
n_bins : int, optional
|
|
625
|
+
Number of bins to use, by default 20.
|
|
626
|
+
scale : bool, optional
|
|
627
|
+
Whether to scale variables before binning, by default False.
|
|
628
|
+
"""
|
|
629
|
+
data = self.data[[var1, var2, self.target_column]].copy()
|
|
630
|
+
|
|
631
|
+
# Drop rows with missing values
|
|
632
|
+
data.dropna(subset=[var1, var2], inplace=True)
|
|
633
|
+
|
|
634
|
+
# Ensure target column is numeric (0 or 1)
|
|
635
|
+
data[self.target_column] = data[self.target_column].astype(int)
|
|
636
|
+
|
|
637
|
+
if scale:
|
|
638
|
+
scaler = MinMaxScaler()
|
|
639
|
+
data[[var1, var2]] = scaler.fit_transform(data[[var1, var2]])
|
|
640
|
+
|
|
641
|
+
# Create the hexbin plot
|
|
642
|
+
plt.figure(figsize=(10, 8))
|
|
643
|
+
hb = plt.hexbin(
|
|
644
|
+
data[var1],
|
|
645
|
+
data[var2],
|
|
646
|
+
C=data[self.target_column],
|
|
647
|
+
gridsize=n_bins,
|
|
648
|
+
cmap="YlOrRd",
|
|
649
|
+
reduce_C_function=np.mean,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
plt.colorbar(hb, label="Target Frequency")
|
|
653
|
+
plt.xlabel(var1)
|
|
654
|
+
plt.ylabel(var2)
|
|
655
|
+
plt.title(f"Hexbin Plot of {var1} vs {var2} (Target Frequency)")
|
|
656
|
+
|
|
657
|
+
# Add a text annotation for the correlation
|
|
658
|
+
correlation = data[[var1, var2]].corr().iloc[0, 1]
|
|
659
|
+
plt.text(
|
|
660
|
+
0.05,
|
|
661
|
+
0.95,
|
|
662
|
+
f"Correlation: {correlation:.2f}",
|
|
663
|
+
transform=plt.gca().transAxes,
|
|
664
|
+
fontsize=10,
|
|
665
|
+
verticalalignment="top",
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
plt.tight_layout()
|
|
669
|
+
plt.show()
|
|
670
|
+
|
|
671
|
+
def plot_categorical_value_counts(
|
|
672
|
+
self, plot_type: Literal["bar", "pie"] = "bar"
|
|
673
|
+
) -> None:
|
|
674
|
+
"""
|
|
675
|
+
Generate value counts for categorical features and visualize them with a dropdown menu to select columns.
|
|
676
|
+
Includes a red line representing target percentages for bar plots.
|
|
677
|
+
|
|
678
|
+
Parameters
|
|
679
|
+
----------
|
|
680
|
+
plot_type : {'bar', 'pie'}, optional
|
|
681
|
+
Type of plot to display, by default 'bar'.
|
|
682
|
+
"""
|
|
683
|
+
fig = go.Figure()
|
|
684
|
+
|
|
685
|
+
for col in self.categorical_columns:
|
|
686
|
+
counts = self.data[col].value_counts().reset_index()
|
|
687
|
+
counts.columns = [col, "Frequency"]
|
|
688
|
+
total = counts["Frequency"].sum()
|
|
689
|
+
counts["Percentage"] = counts["Frequency"] / total * 100
|
|
690
|
+
|
|
691
|
+
# Calculate target percentage for each category
|
|
692
|
+
target_percentages = self.data.groupby(col, observed=False)[
|
|
693
|
+
self.target_column
|
|
694
|
+
].mean()
|
|
695
|
+
counts["TargetPercentage"] = (
|
|
696
|
+
counts[col].map(target_percentages).astype(float)
|
|
697
|
+
)
|
|
698
|
+
counts["TargetPercentage_"] = counts["TargetPercentage"] / 100
|
|
699
|
+
|
|
700
|
+
if counts.empty:
|
|
701
|
+
print(f"No data to plot for column: {col}")
|
|
702
|
+
continue
|
|
703
|
+
|
|
704
|
+
if plot_type == "bar":
|
|
705
|
+
# Add bar trace
|
|
706
|
+
fig.add_trace(
|
|
707
|
+
go.Bar(
|
|
708
|
+
x=counts[col],
|
|
709
|
+
y=counts["Frequency"],
|
|
710
|
+
name=col,
|
|
711
|
+
text=counts["Percentage"].apply(lambda x: f"{x:.1f}%"),
|
|
712
|
+
textposition="inside",
|
|
713
|
+
hovertemplate=(
|
|
714
|
+
f"<b>{col}</b>: %{{x}}<br>"
|
|
715
|
+
"Frequency: %{y}<br>"
|
|
716
|
+
"Percentage: %{customdata[0]:.2f}%<br>"
|
|
717
|
+
f"{self.target_column} %: %{{customdata[1]:.2%}}"
|
|
718
|
+
),
|
|
719
|
+
customdata=counts[["Percentage", "TargetPercentage_"]],
|
|
720
|
+
visible=False,
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# Add red line trace for target percentages
|
|
725
|
+
fig.add_trace(
|
|
726
|
+
go.Scatter(
|
|
727
|
+
x=counts[col],
|
|
728
|
+
y=counts["TargetPercentage"],
|
|
729
|
+
mode="lines",
|
|
730
|
+
name=f"{self.target_column} %",
|
|
731
|
+
line={"color": "red", "width": 2},
|
|
732
|
+
yaxis="y2",
|
|
733
|
+
hovertemplate=f"{self.target_column} %: %{{customdata:.2%}}<extra></extra>",
|
|
734
|
+
customdata=counts["TargetPercentage_"],
|
|
735
|
+
visible=False,
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
elif plot_type == "pie":
|
|
740
|
+
fig.add_trace(
|
|
741
|
+
go.Pie(
|
|
742
|
+
labels=counts[col],
|
|
743
|
+
values=counts["Frequency"],
|
|
744
|
+
name=col,
|
|
745
|
+
text=counts["Percentage"].apply(lambda x: f"{x:.1f}%"),
|
|
746
|
+
hovertemplate=(
|
|
747
|
+
f"<b>{col}</b>: %{{label}}<br>"
|
|
748
|
+
"Frequency: %{value}<br>"
|
|
749
|
+
"Percentage: %{customdata[0]:.2f}%<br>"
|
|
750
|
+
f"{self.target_column} %: %{{customdata[1]:.2%}}"
|
|
751
|
+
),
|
|
752
|
+
customdata=counts[["Percentage", "TargetPercentage_"]],
|
|
753
|
+
visible=False,
|
|
754
|
+
)
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# Create dropdown menu
|
|
758
|
+
buttons = []
|
|
759
|
+
for i, col in enumerate(self.categorical_columns):
|
|
760
|
+
visible = [False] * len(fig.data) # type: ignore
|
|
761
|
+
if plot_type == "bar":
|
|
762
|
+
visible[i * 2] = True # Bar trace
|
|
763
|
+
visible[i * 2 + 1] = True # Line trace
|
|
764
|
+
else:
|
|
765
|
+
visible[i] = True
|
|
766
|
+
buttons.append(
|
|
767
|
+
{
|
|
768
|
+
"method": "update",
|
|
769
|
+
"label": col,
|
|
770
|
+
"args": [{"visible": visible}, {"title": f"Distribution of {col}"}],
|
|
771
|
+
}
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# Update layout
|
|
775
|
+
fig.update_layout(
|
|
776
|
+
updatemenus=[
|
|
777
|
+
{
|
|
778
|
+
"active": 0,
|
|
779
|
+
"buttons": buttons,
|
|
780
|
+
"direction": "down",
|
|
781
|
+
"showactive": True,
|
|
782
|
+
"x": 1.0,
|
|
783
|
+
"xanchor": "left",
|
|
784
|
+
"y": 1.15,
|
|
785
|
+
"yanchor": "top",
|
|
786
|
+
}
|
|
787
|
+
],
|
|
788
|
+
title=f"Distribution of {self.categorical_columns[0]}",
|
|
789
|
+
xaxis_title=self.categorical_columns[0] if plot_type == "bar" else None,
|
|
790
|
+
yaxis_title="Frequency" if plot_type == "bar" else None,
|
|
791
|
+
showlegend=True,
|
|
792
|
+
legend={
|
|
793
|
+
"orientation": "h",
|
|
794
|
+
"yanchor": "bottom",
|
|
795
|
+
"y": 1.02,
|
|
796
|
+
"xanchor": "right",
|
|
797
|
+
"x": 1,
|
|
798
|
+
},
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
if plot_type == "bar":
|
|
802
|
+
max_target_percentage = max(
|
|
803
|
+
self.data.groupby(col, observed=False)[self.target_column].mean().max()
|
|
804
|
+
for col in self.categorical_columns
|
|
805
|
+
)
|
|
806
|
+
fig.update_layout(
|
|
807
|
+
yaxis={"title": "Frequency", "side": "left"},
|
|
808
|
+
yaxis2={
|
|
809
|
+
"title": "",
|
|
810
|
+
"overlaying": "y",
|
|
811
|
+
"side": "right",
|
|
812
|
+
"range": [0, max_target_percentage],
|
|
813
|
+
"tickformat": ".0%",
|
|
814
|
+
"showticklabels": False, # Hide tick labels
|
|
815
|
+
"showgrid": False, # Hide grid lines
|
|
816
|
+
"zeroline": False, # Hide zero line
|
|
817
|
+
"showline": False, # Hide axis line
|
|
818
|
+
},
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
# Make the first plot visible
|
|
822
|
+
if len(fig.data) > 0: # type: ignore
|
|
823
|
+
fig.data[0].visible = True # type: ignore
|
|
824
|
+
if plot_type == "bar" and len(fig.data) > 1: # type: ignore
|
|
825
|
+
fig.data[1].visible = True # type: ignore
|
|
826
|
+
|
|
827
|
+
fig.show()
|
|
828
|
+
|
|
829
|
+
def _compute_categories_per_ring(self, n_categories: int) -> List[int]:
|
|
830
|
+
"""
|
|
831
|
+
Determine the number of categories for each ring in the custom plot.
|
|
832
|
+
|
|
833
|
+
Parameters
|
|
834
|
+
----------
|
|
835
|
+
n_categories : int
|
|
836
|
+
Total number of categories.
|
|
837
|
+
|
|
838
|
+
Returns
|
|
839
|
+
-------
|
|
840
|
+
List[int]
|
|
841
|
+
Number of categories for each ring.
|
|
842
|
+
"""
|
|
843
|
+
power = 5
|
|
844
|
+
rings: List[int] = []
|
|
845
|
+
remaining = n_categories
|
|
846
|
+
while remaining > 0:
|
|
847
|
+
ring_size = math.ceil(Visualizations.PHI ** (power + 1)) - math.ceil(
|
|
848
|
+
Visualizations.PHI**power
|
|
849
|
+
)
|
|
850
|
+
if remaining <= ring_size:
|
|
851
|
+
# If this is the last ring or second to last ring
|
|
852
|
+
if len(rings) >= 1:
|
|
853
|
+
# Distribute remaining categories: 2/3 to second last, 1/3 to last
|
|
854
|
+
second_last = math.ceil(remaining * 2 / 3)
|
|
855
|
+
last = remaining - second_last
|
|
856
|
+
rings[-1] += second_last
|
|
857
|
+
if last > 0:
|
|
858
|
+
rings.append(last)
|
|
859
|
+
else:
|
|
860
|
+
rings.append(remaining)
|
|
861
|
+
break
|
|
862
|
+
rings.append(ring_size)
|
|
863
|
+
remaining -= ring_size
|
|
864
|
+
power += 1
|
|
865
|
+
return rings
|
|
866
|
+
|
|
867
|
+
def plot_stacked_donut(self, column: str) -> None:
|
|
868
|
+
"""
|
|
869
|
+
Plot the distribution of a categorical column with respect to the target column using a custom plot.
|
|
870
|
+
|
|
871
|
+
Parameters
|
|
872
|
+
----------
|
|
873
|
+
column : str
|
|
874
|
+
Name of the column to plot.
|
|
875
|
+
"""
|
|
876
|
+
# Aggregate and prepare data
|
|
877
|
+
aggregated = self.data[column].value_counts().reset_index()
|
|
878
|
+
aggregated.columns = pd.Index([column, "total_count"])
|
|
879
|
+
total_obs = aggregated["total_count"].sum()
|
|
880
|
+
aggregated["percentage"] = aggregated["total_count"] / total_obs * 100
|
|
881
|
+
positive_cases = (
|
|
882
|
+
self.data[self.data[self.target_column] == 1][column]
|
|
883
|
+
.value_counts()
|
|
884
|
+
.reset_index()
|
|
885
|
+
)
|
|
886
|
+
positive_cases.columns = PandasIndex([column, "positive_count"])
|
|
887
|
+
merged_data = pd.merge(aggregated, positive_cases, on=column, how="left")
|
|
888
|
+
merged_data["positive_count"] = merged_data["positive_count"].fillna(0)
|
|
889
|
+
merged_data["positive_rate"] = (
|
|
890
|
+
merged_data["positive_count"] / merged_data["total_count"]
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
# Determine ring distribution
|
|
894
|
+
n_categories = len(merged_data)
|
|
895
|
+
ring_sizes = self._compute_categories_per_ring(n_categories)
|
|
896
|
+
|
|
897
|
+
# Assign categories to rings
|
|
898
|
+
merged_data["ring"] = 0
|
|
899
|
+
start_idx = 0
|
|
900
|
+
for ring, size in enumerate(ring_sizes):
|
|
901
|
+
end_idx = start_idx + size
|
|
902
|
+
merged_data.loc[start_idx : end_idx - 1, "ring"] = ring
|
|
903
|
+
start_idx = end_idx
|
|
904
|
+
|
|
905
|
+
# Adjust base hole size and growth rate
|
|
906
|
+
base_hole_size = 0.3
|
|
907
|
+
hole_growth_rate = 0.1
|
|
908
|
+
|
|
909
|
+
# Create traces for each ring
|
|
910
|
+
traces = []
|
|
911
|
+
for ring in range(len(ring_sizes)):
|
|
912
|
+
ring_data = merged_data[merged_data["ring"] == ring]
|
|
913
|
+
|
|
914
|
+
if ring_data.empty:
|
|
915
|
+
continue
|
|
916
|
+
|
|
917
|
+
# Generate distinct colors for this ring
|
|
918
|
+
n_colors = len(ring_data)
|
|
919
|
+
hue_start = ring * 0.25 # Shift hue for each ring
|
|
920
|
+
colors = [
|
|
921
|
+
f"rgb({int(r*255)},{int(g*255)},{int(b*255)})"
|
|
922
|
+
for r, g, b in [
|
|
923
|
+
colorsys.hsv_to_rgb((hue_start + j / n_colors) % 1, 0.7, 0.9)
|
|
924
|
+
for j in range(n_colors)
|
|
925
|
+
]
|
|
926
|
+
]
|
|
927
|
+
|
|
928
|
+
# Adjust hole size based on ring (ensure it doesn't exceed 0.9)
|
|
929
|
+
hole_size = min(0.9, base_hole_size + ring * hole_growth_rate)
|
|
930
|
+
|
|
931
|
+
# Prepare hover text
|
|
932
|
+
hover_text = [
|
|
933
|
+
f"<b>{label}</b><br>"
|
|
934
|
+
f"Count: {count}<br>"
|
|
935
|
+
f"Percentage: {percentage:.2f}%<br>"
|
|
936
|
+
f"Positive Rate: {positive_rate:.2%}"
|
|
937
|
+
for label, count, percentage, positive_rate in zip(
|
|
938
|
+
ring_data[column],
|
|
939
|
+
ring_data["total_count"],
|
|
940
|
+
ring_data["percentage"],
|
|
941
|
+
ring_data["positive_rate"],
|
|
942
|
+
)
|
|
943
|
+
]
|
|
944
|
+
|
|
945
|
+
traces.append(
|
|
946
|
+
go.Pie(
|
|
947
|
+
values=ring_data["total_count"],
|
|
948
|
+
labels=ring_data[column],
|
|
949
|
+
name=f"Ring {ring+1}",
|
|
950
|
+
hole=hole_size,
|
|
951
|
+
domain={"x": [0, 1], "y": [0, 1]},
|
|
952
|
+
hoverinfo="text",
|
|
953
|
+
hovertext=hover_text,
|
|
954
|
+
marker={
|
|
955
|
+
"colors": colors,
|
|
956
|
+
"line": {"color": "white", "width": 2}, # Add white borders
|
|
957
|
+
},
|
|
958
|
+
textposition="none",
|
|
959
|
+
showlegend=True,
|
|
960
|
+
)
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
# Create the figure
|
|
964
|
+
fig = go.Figure(data=traces)
|
|
965
|
+
|
|
966
|
+
# Update layout
|
|
967
|
+
layout_annotations = []
|
|
968
|
+
if len(ring_sizes) > 1:
|
|
969
|
+
layout_annotations = [
|
|
970
|
+
{
|
|
971
|
+
"text": "Most<br>Frequent",
|
|
972
|
+
"x": 0.5,
|
|
973
|
+
"y": 0.5,
|
|
974
|
+
"font_size": 10,
|
|
975
|
+
"showarrow": False,
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
"text": "Least<br>Frequent",
|
|
979
|
+
"x": 0.5,
|
|
980
|
+
"y": 0.95,
|
|
981
|
+
"font_size": 10,
|
|
982
|
+
"showarrow": False,
|
|
983
|
+
},
|
|
984
|
+
]
|
|
985
|
+
|
|
986
|
+
fig.update_layout(
|
|
987
|
+
title=f"Distribution of {column} with {self.target_column.capitalize()} Events",
|
|
988
|
+
annotations=layout_annotations,
|
|
989
|
+
legend={
|
|
990
|
+
"orientation": "v",
|
|
991
|
+
"yanchor": "top",
|
|
992
|
+
"y": 1,
|
|
993
|
+
"xanchor": "left",
|
|
994
|
+
"x": 1.05,
|
|
995
|
+
},
|
|
996
|
+
legend_title_text="Categories",
|
|
997
|
+
margin={"r": 150}, # Increase right margin to accommodate legend
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
fig.show()
|
|
1001
|
+
|
|
1002
|
+
def plot_numerical_target_frequency_line(
|
|
1003
|
+
self, x_log_scale: bool = False, y_log_scale: bool = False, scale: bool = False
|
|
1004
|
+
) -> None:
|
|
1005
|
+
"""
|
|
1006
|
+
Plot the target frequency for all numerical columns in a single plot with different colored lines.
|
|
1007
|
+
All lines are initially invisible and can be toggled on/off interactively.
|
|
1008
|
+
|
|
1009
|
+
Parameters
|
|
1010
|
+
----------
|
|
1011
|
+
x_log_scale : bool, optional
|
|
1012
|
+
If True, the x-axis will be plotted in log scale, by default False.
|
|
1013
|
+
y_log_scale : bool, optional
|
|
1014
|
+
If True, the y-axis will be plotted in log scale, by default False.
|
|
1015
|
+
scale : bool, optional
|
|
1016
|
+
If True, scale each column's values from 0-1 before plotting, by default False.
|
|
1017
|
+
"""
|
|
1018
|
+
fig = go.Figure()
|
|
1019
|
+
|
|
1020
|
+
for col in self.numerical_columns:
|
|
1021
|
+
grouped = self._bin_and_target_freq(col, scale=scale)
|
|
1022
|
+
fig.add_trace(
|
|
1023
|
+
go.Scatter(
|
|
1024
|
+
x=grouped[col],
|
|
1025
|
+
y=grouped["target_freq"],
|
|
1026
|
+
mode="lines",
|
|
1027
|
+
name=col,
|
|
1028
|
+
visible="legendonly", # This makes the line invisible by default
|
|
1029
|
+
)
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
fig.update_layout(
|
|
1033
|
+
title="Target Frequency for Numerical Columns",
|
|
1034
|
+
xaxis_title="Value" if not scale else "Scaled Value (0-1)",
|
|
1035
|
+
yaxis_title="Target Frequency",
|
|
1036
|
+
xaxis_type="log" if x_log_scale else "linear",
|
|
1037
|
+
yaxis_type="log" if y_log_scale else "linear",
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
fig.show()
|
|
1041
|
+
|
|
1042
|
+
def calc_cat_target_freq(self) -> pd.DataFrame:
|
|
1043
|
+
"""
|
|
1044
|
+
Calculate the target frequency for each category of all categorical columns.
|
|
1045
|
+
|
|
1046
|
+
Returns
|
|
1047
|
+
-------
|
|
1048
|
+
pd.DataFrame
|
|
1049
|
+
DataFrame with a multi-index structure for easy viewing and analysis.
|
|
1050
|
+
"""
|
|
1051
|
+
data_list = []
|
|
1052
|
+
|
|
1053
|
+
for col in self.categorical_columns:
|
|
1054
|
+
grouped = (
|
|
1055
|
+
self.data.groupby(col, observed=False)
|
|
1056
|
+
.agg({self.target_column: ["mean", "count"], col: "count"})
|
|
1057
|
+
.reset_index()
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
grouped.columns = PandasIndex(
|
|
1061
|
+
[
|
|
1062
|
+
"Category",
|
|
1063
|
+
"Target_Frequency",
|
|
1064
|
+
"Total_Count",
|
|
1065
|
+
"Category_Count",
|
|
1066
|
+
]
|
|
1067
|
+
)
|
|
1068
|
+
grouped["Column"] = col
|
|
1069
|
+
grouped["Category_Percentage"] = (
|
|
1070
|
+
grouped["Category_Count"] / grouped["Total_Count"].sum() * 100
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
data_list.append(grouped)
|
|
1074
|
+
|
|
1075
|
+
# Combine all dataframes
|
|
1076
|
+
result_df = pd.concat(data_list, ignore_index=True)
|
|
1077
|
+
# Create multi-index DataFrame
|
|
1078
|
+
result_df = result_df.set_index(["Column", "Category"])
|
|
1079
|
+
# Reorder columns
|
|
1080
|
+
result_df = result_df[
|
|
1081
|
+
["Category_Count", "Category_Percentage", "Target_Frequency"]
|
|
1082
|
+
]
|
|
1083
|
+
# Rename columns for clarity
|
|
1084
|
+
result_df.columns = PandasIndex(["Count", "Percentage", "Target_Frequency"])
|
|
1085
|
+
|
|
1086
|
+
# Sort by Column and then by Count (descending)
|
|
1087
|
+
result_df = result_df.sort_values(["Column", "Count"], ascending=[True, False])
|
|
1088
|
+
|
|
1089
|
+
return result_df
|
|
1090
|
+
|
|
1091
|
+
def _calculate_vif_manual(self, X, feature_idx):
|
|
1092
|
+
"""
|
|
1093
|
+
Manually calculate VIF for a single feature.
|
|
1094
|
+
"""
|
|
1095
|
+
other_features = [i for i in range(X.shape[1]) if i != feature_idx]
|
|
1096
|
+
X_other = X[:, other_features]
|
|
1097
|
+
y = X[:, feature_idx]
|
|
1098
|
+
|
|
1099
|
+
# Check if the feature has any variation
|
|
1100
|
+
if np.all(y == y[0]):
|
|
1101
|
+
return np.inf # or any large number to indicate perfect multicollinearity
|
|
1102
|
+
|
|
1103
|
+
# Fit linear regression
|
|
1104
|
+
model = LinearRegression()
|
|
1105
|
+
model.fit(X_other, y)
|
|
1106
|
+
|
|
1107
|
+
# Calculate R-squared
|
|
1108
|
+
y_pred = model.predict(X_other)
|
|
1109
|
+
ss_total = np.sum((y - np.mean(y)) ** 2)
|
|
1110
|
+
ss_residual = np.sum((y - y_pred) ** 2)
|
|
1111
|
+
|
|
1112
|
+
# Handle the case where ss_total is very close to zero
|
|
1113
|
+
if np.isclose(ss_total, 0):
|
|
1114
|
+
return np.inf # or any large number to indicate perfect multicollinearity
|
|
1115
|
+
|
|
1116
|
+
r_squared = 1 - (ss_residual / ss_total)
|
|
1117
|
+
|
|
1118
|
+
# Calculate VIF
|
|
1119
|
+
vif = 1 / (1 - r_squared)
|
|
1120
|
+
|
|
1121
|
+
return vif
|
|
1122
|
+
|
|
1123
|
+
def plot_vif_failsafe(self, threshold: int = 5, show_plot: bool = True) -> None:
|
|
1124
|
+
"""
|
|
1125
|
+
Calculate the Variance Inflation Factor (VIF) for numerical features.
|
|
1126
|
+
"""
|
|
1127
|
+
# Select numerical data from the DataFrame, excluding the target column
|
|
1128
|
+
numerical_columns = [
|
|
1129
|
+
col for col in self.numerical_columns if col != self.target_column
|
|
1130
|
+
]
|
|
1131
|
+
numerical_data = self.data[numerical_columns].copy()
|
|
1132
|
+
|
|
1133
|
+
# Convert boolean columns to integers
|
|
1134
|
+
for column in numerical_data.columns:
|
|
1135
|
+
if numerical_data[column].dtype == "bool":
|
|
1136
|
+
numerical_data[column] = numerical_data[column].astype(int)
|
|
1137
|
+
|
|
1138
|
+
# Check for missing values
|
|
1139
|
+
missing_values = numerical_data.isnull().sum()
|
|
1140
|
+
if missing_values.sum() > 0:
|
|
1141
|
+
print("\nWARNING: Missing values detected in the following columns:")
|
|
1142
|
+
print(missing_values[missing_values > 0])
|
|
1143
|
+
print("\nImputing missing values with mean for VIF calculation.")
|
|
1144
|
+
print(
|
|
1145
|
+
"It is strongly recommended to properly handle missing values in your dataset."
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
# Impute missing values with mean
|
|
1149
|
+
numerical_data = numerical_data.fillna(numerical_data.mean())
|
|
1150
|
+
|
|
1151
|
+
# Calculate VIF for each feature
|
|
1152
|
+
X = numerical_data.values
|
|
1153
|
+
vif_data = pd.DataFrame()
|
|
1154
|
+
vif_data["Feature"] = numerical_data.columns
|
|
1155
|
+
|
|
1156
|
+
# Use tqdm to create a progress bar
|
|
1157
|
+
vif_values = []
|
|
1158
|
+
for i in tqdm(range(X.shape[1]), desc="Calculating VIF", unit="feature"):
|
|
1159
|
+
vif_values.append(self._calculate_vif_manual(X, i))
|
|
1160
|
+
vif_data["VIF"] = vif_values
|
|
1161
|
+
|
|
1162
|
+
# Sort VIF values in descending order
|
|
1163
|
+
vif_data = vif_data.sort_values("VIF", ascending=False).reset_index(drop=True)
|
|
1164
|
+
|
|
1165
|
+
# Assign colors based on the threshold
|
|
1166
|
+
vif_data["Color"] = [
|
|
1167
|
+
"red" if vif > threshold else "green" for vif in vif_data["VIF"]
|
|
1168
|
+
]
|
|
1169
|
+
|
|
1170
|
+
# Create the bar plot
|
|
1171
|
+
fig = go.Figure()
|
|
1172
|
+
fig.add_trace(
|
|
1173
|
+
go.Bar(
|
|
1174
|
+
x=vif_data["Feature"],
|
|
1175
|
+
y=vif_data["VIF"],
|
|
1176
|
+
marker_color=vif_data["Color"],
|
|
1177
|
+
text=vif_data["VIF"].round(2),
|
|
1178
|
+
textposition="outside",
|
|
1179
|
+
)
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1182
|
+
# Update layout
|
|
1183
|
+
fig.update_layout(
|
|
1184
|
+
title="Variance Inflation Factor (VIF) for Features",
|
|
1185
|
+
xaxis_title="Features",
|
|
1186
|
+
yaxis_title="VIF",
|
|
1187
|
+
yaxis={
|
|
1188
|
+
"range": [0, min(max(vif_data["VIF"]), 100) * 1.1]
|
|
1189
|
+
}, # Set y-axis range with some padding and upper limit
|
|
1190
|
+
showlegend=False,
|
|
1191
|
+
xaxis={"tickangle": 45},
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
# Add a horizontal line at the threshold value
|
|
1195
|
+
fig.add_shape(
|
|
1196
|
+
type="line",
|
|
1197
|
+
x0=-0.5,
|
|
1198
|
+
y0=threshold,
|
|
1199
|
+
x1=len(vif_data["Feature"]) - 0.5,
|
|
1200
|
+
y1=threshold,
|
|
1201
|
+
line={"color": "black", "width": 2, "dash": "dash"},
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
if show_plot:
|
|
1205
|
+
fig.show()
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
'''
|
|
1209
|
+
def plot_categorical_value_counts(
|
|
1210
|
+
self, plot_type: Literal["bar", "pie"] = "bar"
|
|
1211
|
+
) -> None:
|
|
1212
|
+
"""
|
|
1213
|
+
Generate value counts for categorical features and visualize them.
|
|
1214
|
+
|
|
1215
|
+
Parameters
|
|
1216
|
+
----------
|
|
1217
|
+
plot_type : {'bar', 'pie'}, optional
|
|
1218
|
+
Type of plot to display, by default 'bar'.
|
|
1219
|
+
"""
|
|
1220
|
+
value_counts = {
|
|
1221
|
+
col: self.data[col].value_counts() for col in self.categorical_columns
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
for col in self.categorical_columns:
|
|
1225
|
+
counts = value_counts[col].reset_index()
|
|
1226
|
+
counts.columns = PandasIndex([col, "Frequency"])
|
|
1227
|
+
|
|
1228
|
+
if counts.empty:
|
|
1229
|
+
print(f"No data to plot for column: {col}")
|
|
1230
|
+
continue
|
|
1231
|
+
|
|
1232
|
+
if plot_type == "bar":
|
|
1233
|
+
fig = px.bar(
|
|
1234
|
+
counts, x=col, y="Frequency", title=f"Distribution of {col}"
|
|
1235
|
+
)
|
|
1236
|
+
elif plot_type == "pie":
|
|
1237
|
+
fig = px.pie(
|
|
1238
|
+
counts,
|
|
1239
|
+
names=col,
|
|
1240
|
+
values="Frequency",
|
|
1241
|
+
title=f"Distribution of {col}",
|
|
1242
|
+
)
|
|
1243
|
+
else:
|
|
1244
|
+
raise ValueError("plot_type should be either 'bar' or 'pie'")
|
|
1245
|
+
|
|
1246
|
+
fig.update_layout(title={"x": 0.5})
|
|
1247
|
+
fig.show()
|
|
1248
|
+
'''
|