dera 1.0.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.
@@ -0,0 +1,878 @@
1
+ import polars as pl
2
+ import pandas as pd
3
+ import numpy as np
4
+ import math
5
+
6
+ def handle_drop_columns(lf, params):
7
+ cols = params.get('columns', [])
8
+ if not cols and params.get('column'):
9
+ cols = [params.get('column')]
10
+ cols = [c for c in cols if c in lf.columns]
11
+ if cols:
12
+ lf = lf.drop(cols)
13
+ return lf
14
+
15
+ def handle_fill_null(lf, params):
16
+ cols = params.get('columns', [])
17
+ if not cols and params.get('column'):
18
+ cols = [params.get('column')]
19
+ strategy = params.get('strategy', 'mean')
20
+ val = params.get('value')
21
+
22
+ for col in cols:
23
+ if col not in lf.columns:
24
+ continue
25
+ if strategy == 'mean':
26
+ lf = lf.with_columns(pl.col(col).fill_null(pl.col(col).mean()))
27
+ elif strategy == 'median':
28
+ lf = lf.with_columns(pl.col(col).fill_null(pl.col(col).median()))
29
+ elif strategy == 'mode':
30
+ lf = lf.with_columns(pl.col(col).fill_null(pl.col(col).mode().first()))
31
+ elif strategy == 'constant':
32
+ if val is not None:
33
+ # Get the schema type of the column to cast the constant value correctly
34
+ schema = lf.schema
35
+ col_type = schema.get(col)
36
+ if col_type in (pl.Int64, pl.Int32, pl.Int16, pl.Int8):
37
+ try:
38
+ c_val = int(float(val))
39
+ except ValueError:
40
+ c_val = val
41
+ elif col_type in (pl.Float64, pl.Float32):
42
+ try:
43
+ c_val = float(val)
44
+ except ValueError:
45
+ c_val = val
46
+ elif col_type == pl.Boolean:
47
+ c_val = str(val).lower() in ('true', '1', 'yes', 'y', 't')
48
+ else:
49
+ c_val = str(val)
50
+ lf = lf.with_columns(pl.col(col).fill_null(pl.lit(c_val)))
51
+ return lf
52
+
53
+ def handle_remove_duplicates(lf, params):
54
+ return lf.unique()
55
+
56
+ def handle_rename_column(lf, params):
57
+ mapping = params.get('columns')
58
+ if not mapping and params.get('oldName') and params.get('newName'):
59
+ mapping = {params.get('oldName'): params.get('newName')}
60
+ if mapping:
61
+ mapping = {k: v for k, v in mapping.items() if k in lf.columns}
62
+ if mapping:
63
+ lf = lf.rename(mapping)
64
+ return lf
65
+
66
+ def handle_min_max_scale(lf, params):
67
+ cols = params.get('columns', [])
68
+ if not cols and params.get('column'):
69
+ cols = [params.get('column')]
70
+ for col in cols:
71
+ if col in lf.columns:
72
+ col_min = pl.col(col).min()
73
+ col_max = pl.col(col).max()
74
+ lf = lf.with_columns(
75
+ pl.when(col_max == col_min)
76
+ .then(pl.lit(0.0))
77
+ .otherwise((pl.col(col) - col_min) / (col_max - col_min))
78
+ .alias(col)
79
+ )
80
+ return lf
81
+
82
+ def handle_standardize(lf, params):
83
+ cols = params.get('columns', [])
84
+ if not cols and params.get('column'):
85
+ cols = [params.get('column')]
86
+ for col in cols:
87
+ if col in lf.columns:
88
+ col_mean = pl.col(col).mean()
89
+ col_std = pl.col(col).std()
90
+ lf = lf.with_columns(
91
+ pl.when(col_std == 0)
92
+ .then(pl.lit(0.0))
93
+ .otherwise((pl.col(col) - col_mean) / col_std)
94
+ .alias(col)
95
+ )
96
+ return lf
97
+
98
+ def handle_lowercase(lf, params):
99
+ cols = params.get('columns', [])
100
+ if not cols and params.get('column'):
101
+ cols = [params.get('column')]
102
+ for col in cols:
103
+ if col in lf.columns:
104
+ lf = lf.with_columns(pl.col(col).cast(pl.String).str.to_lowercase().alias(col))
105
+ return lf
106
+
107
+ def handle_uppercase(lf, params):
108
+ cols = params.get('columns', [])
109
+ if not cols and params.get('column'):
110
+ cols = [params.get('column')]
111
+ for col in cols:
112
+ if col in lf.columns:
113
+ lf = lf.with_columns(pl.col(col).cast(pl.String).str.to_uppercase().alias(col))
114
+ return lf
115
+
116
+ def handle_trim_spaces(lf, params):
117
+ cols = params.get('columns', [])
118
+ if not cols and params.get('column'):
119
+ cols = [params.get('column')]
120
+ for col in cols:
121
+ if col in lf.columns:
122
+ lf = lf.with_columns(pl.col(col).cast(pl.String).str.strip_chars().alias(col))
123
+ return lf
124
+
125
+ def handle_toggle_bool(lf, params):
126
+ cols = params.get('columns', [])
127
+ if not cols and params.get('column'):
128
+ cols = [params.get('column')]
129
+ for col in cols:
130
+ if col in lf.columns:
131
+ lf = lf.with_columns(pl.col(col).not_().alias(col))
132
+ return lf
133
+
134
+ def handle_one_hot_encode(lf, params):
135
+ cols = params.get('columns', [])
136
+ if not cols and params.get('column'):
137
+ cols = [params.get('column')]
138
+ cols = [c for c in cols if c in lf.columns]
139
+ for col in cols:
140
+ unique_vals = lf.select(col).unique().collect().get_column(col).to_list()
141
+ if len(unique_vals) > 0:
142
+ unique_vals = [v for v in unique_vals if v is not None]
143
+ unique_vals.sort()
144
+ # drop_first=True
145
+ for val in unique_vals[1:]:
146
+ col_name = f"{col}_{val}"
147
+ lf = lf.with_columns((pl.col(col) == val).cast(pl.Int32).alias(col_name))
148
+ lf = lf.drop(col)
149
+ return lf
150
+
151
+ def handle_change_datatype(lf, params):
152
+ cols = params.get('columns', [])
153
+ if not cols and params.get('column'):
154
+ cols = [params.get('column')]
155
+ new_type = params.get('dtype')
156
+ if new_type:
157
+ type_map = {
158
+ 'int64': pl.Int64,
159
+ 'float64': pl.Float64,
160
+ 'object': pl.String,
161
+ 'bool': pl.Boolean,
162
+ 'str': pl.String,
163
+ 'string': pl.String,
164
+ 'datetime': pl.Datetime
165
+ }
166
+ pl_type = type_map.get(new_type)
167
+ if pl_type:
168
+ for col in cols:
169
+ if col in lf.columns:
170
+ if pl_type == pl.Datetime:
171
+ lf = lf.with_columns(pl.col(col).cast(pl.String).str.to_datetime(strict=False).alias(col))
172
+ else:
173
+ lf = lf.with_columns(pl.col(col).cast(pl_type).alias(col))
174
+ return lf
175
+
176
+ def handle_filter_rows(lf, params):
177
+ col = params.get('column')
178
+ op = params.get('operator')
179
+ val = params.get('value')
180
+ if col in lf.columns and op and val is not None:
181
+ if op == '==':
182
+ lf = lf.filter(pl.col(col) == val)
183
+ elif op == '!=':
184
+ lf = lf.filter(pl.col(col) != val)
185
+ elif op == '>':
186
+ try:
187
+ lf = lf.filter(pl.col(col) > float(val))
188
+ except:
189
+ lf = lf.filter(pl.col(col) > val)
190
+ elif op == '<':
191
+ try:
192
+ lf = lf.filter(pl.col(col) < float(val))
193
+ except:
194
+ lf = lf.filter(pl.col(col) < val)
195
+ elif op == 'contains':
196
+ lf = lf.filter(pl.col(col).cast(pl.String).str.contains(str(val)))
197
+ return lf
198
+
199
+ def handle_sort_column(lf, params):
200
+ col = params.get('column')
201
+ ascending = params.get('ascending', True)
202
+ if col in lf.columns:
203
+ lf = lf.sort(col, descending=not ascending)
204
+ return lf
205
+
206
+ def handle_reorder_column(lf, params):
207
+ col = params.get('column')
208
+ direction = params.get('direction')
209
+ if col not in lf.columns:
210
+ return lf
211
+ cols = list(lf.columns)
212
+ idx = cols.index(col)
213
+ if direction == 'start':
214
+ cols.remove(col)
215
+ cols.insert(0, col)
216
+ elif direction == 'end':
217
+ cols.remove(col)
218
+ cols.append(col)
219
+ elif direction == 'left' and idx > 0:
220
+ cols[idx], cols[idx-1] = cols[idx-1], cols[idx]
221
+ elif direction == 'right' and idx < len(cols) - 1:
222
+ cols[idx], cols[idx+1] = cols[idx+1], cols[idx]
223
+ return lf.select(cols)
224
+
225
+ def handle_duplicate_column(lf, params):
226
+ col = params.get('column')
227
+ new_name = params.get('new_name')
228
+ if col in lf.columns and new_name:
229
+ lf = lf.with_columns(pl.col(col).alias(new_name))
230
+ return lf
231
+
232
+ def handle_split_column(lf, params):
233
+ col = params.get('column')
234
+ delimiter = params.get('delimiter', ',')
235
+ if col not in lf.columns:
236
+ return lf
237
+ split_lf = lf.select(pl.col(col).cast(pl.String).str.split(delimiter).alias("_splits"))
238
+ max_splits = split_lf.select(pl.col("_splits").list.len().max()).collect().item()
239
+ if max_splits and max_splits > 0:
240
+ exprs = []
241
+ for i in range(max_splits):
242
+ exprs.append(pl.col(col).cast(pl.String).str.split(delimiter).list.get(i, null_on_oob=True).alias(f"{col}_split_{i+1}"))
243
+ lf = lf.with_columns(exprs)
244
+ return lf
245
+
246
+ def handle_merge_columns(lf, params):
247
+ col1 = params.get('column')
248
+ col2 = params.get('column2')
249
+ separator = params.get('separator', ' ')
250
+ new_name = params.get('new_name')
251
+ if col1 in lf.columns and col2 in lf.columns and new_name:
252
+ lf = lf.with_columns(
253
+ (pl.col(col1).cast(pl.String) + pl.lit(separator) + pl.col(col2).cast(pl.String)).alias(new_name)
254
+ )
255
+ return lf
256
+
257
+ def handle_ffill(lf, params):
258
+ col = params.get('column')
259
+ if col in lf.columns:
260
+ lf = lf.with_columns(pl.col(col).forward_fill().alias(col))
261
+ return lf
262
+
263
+ def handle_bfill(lf, params):
264
+ col = params.get('column')
265
+ if col in lf.columns:
266
+ lf = lf.with_columns(pl.col(col).backward_fill().alias(col))
267
+ return lf
268
+
269
+ def handle_interpolate(lf, params):
270
+ col = params.get('column')
271
+ if col in lf.columns:
272
+ lf = lf.with_columns(pl.col(col).interpolate().alias(col))
273
+ return lf
274
+
275
+ def handle_flag_null(lf, params):
276
+ col = params.get('column')
277
+ if col in lf.columns:
278
+ lf = lf.with_columns(pl.col(col).is_null().cast(pl.Int32).alias(f"{col}_isnull"))
279
+ return lf
280
+
281
+ def handle_drop_null_rows(lf, params):
282
+ scope = params.get('scope', 'column')
283
+ col = params.get('column')
284
+ if scope == 'column' and col in lf.columns:
285
+ lf = lf.filter(pl.col(col).is_not_null())
286
+ elif scope == 'any':
287
+ lf = lf.drop_nulls()
288
+ elif scope == 'all':
289
+ lf = lf.filter(~pl.all_horizontal(pl.all().is_null()))
290
+ return lf
291
+
292
+ def handle_drop_cols_null_threshold(lf, params):
293
+ threshold = float(params.get('threshold', 50)) / 100.0
294
+ null_counts = lf.null_count().collect()
295
+ total_rows = lf.select(pl.len()).collect().item()
296
+ if total_rows > 0:
297
+ cols_to_keep = []
298
+ for col in lf.columns:
299
+ null_pct = null_counts.get_column(col)[0] / total_rows
300
+ if null_pct <= threshold:
301
+ cols_to_keep.append(col)
302
+ lf = lf.select(cols_to_keep)
303
+ return lf
304
+
305
+ def handle_deduplicate_subset(lf, params):
306
+ cols = params.get('columns', [])
307
+ cols = [c for c in cols if c in lf.columns]
308
+ if cols:
309
+ lf = lf.unique(subset=cols)
310
+ return lf
311
+
312
+ def handle_sample_rows(lf, params):
313
+ method = params.get('method', 'count')
314
+ val = float(params.get('value', 100))
315
+ random_state = int(params.get('random_state', 42))
316
+
317
+ df = lf.collect()
318
+ if method == 'count':
319
+ n = min(int(val), len(df))
320
+ df = df.sample(n=n, seed=random_state)
321
+ else:
322
+ frac = min(val, 1.0)
323
+ df = df.sample(fraction=frac, seed=random_state)
324
+ return df.lazy()
325
+
326
+ def handle_drop_rows_index(lf, params):
327
+ start = params.get('start')
328
+ end = params.get('end')
329
+ if start is not None and end is not None:
330
+ lf = lf.with_row_index("_idx")
331
+ lf = lf.filter((pl.col("_idx") < int(start)) | (pl.col("_idx") > int(end)))
332
+ lf = lf.drop("_idx")
333
+ return lf
334
+
335
+ def handle_label_encode(lf, params):
336
+ col = params.get('column')
337
+ if col in lf.columns:
338
+ lf = lf.with_columns(pl.col(col).cast(pl.Categorical).to_physical().alias(col))
339
+ return lf
340
+
341
+ def handle_ordinal_encode(lf, params):
342
+ col = params.get('column')
343
+ order = params.get('order', '')
344
+ if col in lf.columns and order:
345
+ categories = [cat.strip() for cat in order.split(',') if cat.strip()]
346
+ if categories:
347
+ expr = pl.when(pl.col(col) == categories[0]).then(0)
348
+ for idx, cat in enumerate(categories[1:]):
349
+ expr = expr.when(pl.col(col) == cat).then(idx + 1)
350
+ expr = expr.otherwise(-1).alias(col)
351
+ lf = lf.with_columns(expr)
352
+ return lf
353
+
354
+ def handle_binary_encode(lf, params):
355
+ col = params.get('column')
356
+ if col in lf.columns:
357
+ phys_lf = lf.with_columns(pl.col(col).cast(pl.Categorical).to_physical().alias("_code"))
358
+ max_code = phys_lf.select(pl.col("_code").max()).collect().item()
359
+ if max_code is None or max_code <= 0:
360
+ lf = lf.with_columns(pl.lit(0).alias(f"{col}_bin_0"))
361
+ return lf
362
+ num_bits = int(math.ceil(math.log2(max_code + 1)))
363
+ for i in range(num_bits):
364
+ expr = ((pl.col("_code") / (2**i)).floor().cast(pl.Int64) % 2).alias(f"{col}_bin_{i}")
365
+ phys_lf = phys_lf.with_columns(expr)
366
+ lf = phys_lf.drop(["_code", col])
367
+ return lf
368
+
369
+ def handle_robust_scale(lf, params):
370
+ col = params.get('column')
371
+ if col in lf.columns:
372
+ stats = lf.select([
373
+ pl.col(col).quantile(0.25).alias("q25"),
374
+ pl.col(col).median().alias("med"),
375
+ pl.col(col).quantile(0.75).alias("q75")
376
+ ]).collect()
377
+ q25 = stats.get_column("q25")[0]
378
+ med = stats.get_column("med")[0]
379
+ q75 = stats.get_column("q75")[0]
380
+ iqr = q75 - q25
381
+ if iqr == 0:
382
+ lf = lf.with_columns(pl.lit(0.0).alias(col))
383
+ else:
384
+ lf = lf.with_columns(((pl.col(col) - med) / iqr).alias(col))
385
+ return lf
386
+
387
+ def handle_log_transform(lf, params):
388
+ col = params.get('column')
389
+ shift = float(params.get('shift', 1))
390
+ if col in lf.columns:
391
+ lf = lf.with_columns((pl.col(col) + shift).log().alias(col))
392
+ return lf
393
+
394
+ def handle_sqrt_transform(lf, params):
395
+ col = params.get('column')
396
+ if col in lf.columns:
397
+ lf = lf.with_columns(pl.col(col).sqrt().alias(col))
398
+ return lf
399
+
400
+ def handle_power_transform(lf, params):
401
+ col = params.get('column')
402
+ exponent = float(params.get('exponent', 2))
403
+ if col in lf.columns:
404
+ lf = lf.with_columns((pl.col(col) ** exponent).alias(col))
405
+ return lf
406
+
407
+ def handle_custom_formula(lf, params):
408
+ formula = params.get('formula')
409
+ new_name = params.get('new_name')
410
+ if formula and new_name:
411
+ df = lf.collect().to_pandas()
412
+ try:
413
+ df[new_name] = df.eval(formula)
414
+ return pl.from_pandas(df).lazy()
415
+ except:
416
+ pass
417
+ return lf
418
+
419
+ def handle_bin_bucket(lf, params):
420
+ col = params.get('column')
421
+ num_bins = int(params.get('bins', 5))
422
+ new_name = params.get('new_name') or f"{col}_binned"
423
+ if col in lf.columns:
424
+ stats = lf.select([pl.col(col).min().alias("min_v"), pl.col(col).max().alias("max_v")]).collect()
425
+ min_v = stats.get_column("min_v")[0]
426
+ max_v = stats.get_column("max_v")[0]
427
+ if min_v is not None and max_v is not None and max_v > min_v:
428
+ bin_width = (max_v - min_v) / num_bins
429
+ breaks = [min_v + i * bin_width for i in range(1, num_bins)]
430
+ lf = lf.with_columns(pl.col(col).cut(breaks).cast(pl.String).alias(new_name))
431
+ return lf
432
+
433
+ def handle_date_parts(lf, params):
434
+ col = params.get('column')
435
+ parts = params.get('parts', ['year', 'month', 'day'])
436
+ if not col or col not in lf.columns:
437
+ raise ValueError(f"Column '{col}' not found in dataset.")
438
+ exprs = []
439
+ for part in parts:
440
+ if part == 'year':
441
+ exprs.append(pl.col(col).dt.year().alias(f"{col}_year"))
442
+ elif part == 'month':
443
+ exprs.append(pl.col(col).dt.month().alias(f"{col}_month"))
444
+ elif part == 'day':
445
+ exprs.append(pl.col(col).dt.day().alias(f"{col}_day"))
446
+ elif part == 'dayofweek':
447
+ exprs.append((pl.col(col).dt.weekday() - 1).alias(f"{col}_dayofweek"))
448
+ elif part == 'hour':
449
+ exprs.append(pl.col(col).dt.hour().alias(f"{col}_hour"))
450
+ elif part == 'quarter':
451
+ exprs.append(pl.col(col).dt.quarter().alias(f"{col}_quarter"))
452
+ elif part == 'minute':
453
+ exprs.append(pl.col(col).dt.minute().alias(f"{col}_minute"))
454
+ elif part == 'second':
455
+ exprs.append(pl.col(col).dt.second().alias(f"{col}_second"))
456
+ if exprs:
457
+ lf = lf.with_columns(exprs)
458
+ return lf
459
+
460
+ def handle_regex_extraction(lf, params):
461
+ col = params.get('column')
462
+ pattern = params.get('pattern')
463
+ new_name = params.get('new_name')
464
+ if col in lf.columns and pattern and new_name:
465
+ lf = lf.with_columns(pl.col(col).cast(pl.String).str.extract(pattern).alias(new_name))
466
+ return lf
467
+
468
+ def handle_rolling_window(lf, params):
469
+ col = params.get('column')
470
+ window = int(params.get('window', 3))
471
+ op = params.get('operation', 'mean')
472
+ new_name = params.get('new_name') or f"{col}_rolling_{op}_{window}"
473
+ if col in lf.columns:
474
+ if op == 'mean':
475
+ lf = lf.with_columns(pl.col(col).rolling_mean(window_size=window, min_periods=1).alias(new_name))
476
+ elif op == 'std':
477
+ lf = lf.with_columns(pl.col(col).rolling_std(window_size=window, min_periods=1).alias(new_name))
478
+ elif op == 'sum':
479
+ lf = lf.with_columns(pl.col(col).rolling_sum(window_size=window, min_periods=1).alias(new_name))
480
+ return lf
481
+
482
+ def handle_interaction_terms(lf, params):
483
+ col1 = params.get('column')
484
+ col2 = params.get('column2')
485
+ new_name = params.get('new_name') or f"{col1}_x_{col2}"
486
+ if col1 in lf.columns and col2 in lf.columns:
487
+ lf = lf.with_columns((pl.col(col1) * pl.col(col2)).alias(new_name))
488
+ return lf
489
+
490
+ def handle_correlation_filter(lf, params):
491
+ df = lf.collect()
492
+ target = params.get('target')
493
+ threshold = float(params.get('threshold', 0.1))
494
+ if target in df.columns:
495
+ numeric_cols = [c for c in df.columns if df[c].dtype.is_numeric()]
496
+ if target in numeric_cols:
497
+ corrs = {col: np.corrcoef(df[col].to_numpy(), df[target].to_numpy())[0, 1] for col in numeric_cols if col != target}
498
+ cols_to_drop = [col for col, corr in corrs.items() if not np.isnan(corr) and abs(corr) < threshold]
499
+ if cols_to_drop:
500
+ df = df.drop(cols_to_drop)
501
+ return df.lazy()
502
+
503
+ def handle_variance_threshold(lf, params):
504
+ df = lf.collect()
505
+ threshold = float(params.get('threshold', 0.0))
506
+ numeric_cols = [c for c in df.columns if df[c].dtype.is_numeric()]
507
+ cols_to_drop = [col for col in numeric_cols if df[col].var() <= threshold]
508
+ if cols_to_drop:
509
+ df = df.drop(cols_to_drop)
510
+ return df.lazy()
511
+
512
+ def handle_select_k_best(lf, params):
513
+ df = lf.collect()
514
+ target = params.get('target')
515
+ k = int(params.get('k', 5))
516
+ if target in df.columns:
517
+ numeric_cols = [c for c in df.columns if df[c].dtype.is_numeric() and c != target]
518
+ corrs = {}
519
+ for col in numeric_cols:
520
+ c_val = np.corrcoef(df[col].to_numpy(), df[target].to_numpy())[0, 1]
521
+ corrs[col] = abs(c_val) if not np.isnan(c_val) else 0.0
522
+ top_k_cols = sorted(corrs, key=corrs.get, reverse=True)[:k]
523
+ non_numeric = [c for c in df.columns if not df[c].dtype.is_numeric()]
524
+ df = df.select(top_k_cols + [target] + [c for c in non_numeric if c != target])
525
+ return df.lazy()
526
+
527
+ def handle_remove_constant_cols(lf, params):
528
+ df = lf.collect()
529
+ cols_to_keep = [col for col in df.columns if df[col].n_unique() > 1]
530
+ df = df.select(cols_to_keep)
531
+ return df.lazy()
532
+
533
+ def handle_remove_highly_correlated(lf, params):
534
+ df = lf.collect()
535
+ threshold = float(params.get('threshold', 0.9))
536
+ numeric_cols = [c for c in df.columns if df[c].dtype.is_numeric()]
537
+ if len(numeric_cols) > 1:
538
+ corr_matrix = np.corrcoef([df[c].to_numpy() for c in numeric_cols])
539
+ to_drop = []
540
+ for i in range(len(numeric_cols)):
541
+ for j in range(i+1, len(numeric_cols)):
542
+ if abs(corr_matrix[i, j]) > threshold:
543
+ to_drop.append(numeric_cols[j])
544
+ if to_drop:
545
+ df = df.drop(list(set(to_drop)))
546
+ return df.lazy()
547
+
548
+ def handle_detect_iqr(lf, params):
549
+ col = params.get('column')
550
+ if col in lf.columns:
551
+ stats = lf.select([
552
+ pl.col(col).quantile(0.25).alias("q25"),
553
+ pl.col(col).quantile(0.75).alias("q75")
554
+ ]).collect()
555
+ q25 = stats.get_column("q25")[0]
556
+ q75 = stats.get_column("q75")[0]
557
+ iqr = q75 - q25
558
+ lower = q25 - 1.5 * iqr
559
+ upper = q75 + 1.5 * iqr
560
+ lf = lf.with_columns(
561
+ ((pl.col(col) < lower) | (pl.col(col) > upper)).cast(pl.Int32).alias(f"{col}_outlier_iqr")
562
+ )
563
+ return lf
564
+
565
+ def handle_detect_zscore(lf, params):
566
+ col = params.get('column')
567
+ threshold = float(params.get('threshold', 3.0))
568
+ if col in lf.columns:
569
+ stats = lf.select([
570
+ pl.col(col).mean().alias("mean"),
571
+ pl.col(col).std().alias("std")
572
+ ]).collect()
573
+ mean = stats.get_column("mean")[0]
574
+ std = stats.get_column("std")[0]
575
+ if std and std > 0:
576
+ lf = lf.with_columns(
577
+ (((pl.col(col) - mean) / std).abs() > threshold).cast(pl.Int32).alias(f"{col}_outlier_z")
578
+ )
579
+ return lf
580
+
581
+ def handle_cap_clip(lf, params):
582
+ col = params.get('column')
583
+ lower_q = float(params.get('lower_q', 0.01))
584
+ upper_q = float(params.get('upper_q', 0.99))
585
+ if col in lf.columns:
586
+ stats = lf.select([
587
+ pl.col(col).quantile(lower_q).alias("lower"),
588
+ pl.col(col).quantile(upper_q).alias("upper")
589
+ ]).collect()
590
+ lower = stats.get_column("lower")[0]
591
+ upper = stats.get_column("upper")[0]
592
+ lf = lf.with_columns(
593
+ pl.col(col).clip(lower_bound=lower, upper_bound=upper).alias(col)
594
+ )
595
+ return lf
596
+
597
+ def handle_remove_outliers(lf, params):
598
+ col = params.get('column')
599
+ method = params.get('method', 'iqr')
600
+ if col in lf.columns:
601
+ if method == 'iqr':
602
+ stats = lf.select([
603
+ pl.col(col).quantile(0.25).alias("q25"),
604
+ pl.col(col).quantile(0.75).alias("q75")
605
+ ]).collect()
606
+ q25 = stats.get_column("q25")[0]
607
+ q75 = stats.get_column("q75")[0]
608
+ iqr = q75 - q25
609
+ lower = q25 - 1.5 * iqr
610
+ upper = q75 + 1.5 * iqr
611
+ lf = lf.filter((pl.col(col) >= lower) & (pl.col(col) <= upper))
612
+ else:
613
+ threshold = float(params.get('threshold', 3.0))
614
+ stats = lf.select([
615
+ pl.col(col).mean().alias("mean"),
616
+ pl.col(col).std().alias("std")
617
+ ]).collect()
618
+ mean = stats.get_column("mean")[0]
619
+ std = stats.get_column("std")[0]
620
+ if std and std > 0:
621
+ lf = lf.filter((((pl.col(col) - mean) / std).abs() <= threshold))
622
+ return lf
623
+
624
+ def handle_replace_substring(lf, params):
625
+ col = params.get('column')
626
+ old_val = params.get('old_val', '')
627
+ new_val = params.get('new_val', '')
628
+ if col in lf.columns:
629
+ lf = lf.with_columns(pl.col(col).cast(pl.String).str.replace(old_val, new_val, literal=True).alias(col))
630
+ return lf
631
+
632
+ def handle_regex_replace(lf, params):
633
+ col = params.get('column')
634
+ pattern = params.get('pattern', '')
635
+ replacement = params.get('replacement', '')
636
+ if col in lf.columns:
637
+ lf = lf.with_columns(pl.col(col).cast(pl.String).str.replace(pattern, replacement, literal=False).alias(col))
638
+ return lf
639
+
640
+ def handle_remove_special_chars(lf, params):
641
+ col = params.get('column')
642
+ if col in lf.columns:
643
+ lf = lf.with_columns(pl.col(col).cast(pl.String).str.replace_all(r'[^a-zA-Z0-9\s]', '').alias(col))
644
+ return lf
645
+
646
+ def handle_extract_domain(lf, params):
647
+ col = params.get('column')
648
+ if col in lf.columns:
649
+ emails = pl.col(col).cast(pl.String).str.extract(r'@([^\s]+)')
650
+ urls = pl.col(col).cast(pl.String).str.extract(r'https?://(?:www\.)?([^/\s]+)')
651
+ lf = lf.with_columns(
652
+ pl.coalesce([emails, urls, pl.lit('')]).alias(f"{col}_domain")
653
+ )
654
+ return lf
655
+
656
+ def handle_groupby_aggregate(lf, params):
657
+ group_cols = params.get('group_cols', [])
658
+ agg_col = params.get('agg_col')
659
+ agg_type = params.get('agg_type', 'mean')
660
+ if group_cols and agg_col in lf.columns:
661
+ if agg_type == 'mean':
662
+ lf = lf.group_by(group_cols).agg(pl.col(agg_col).mean().alias(agg_col))
663
+ elif agg_type == 'sum':
664
+ lf = lf.group_by(group_cols).agg(pl.col(agg_col).sum().alias(agg_col))
665
+ elif agg_type == 'count':
666
+ lf = lf.group_by(group_cols).agg(pl.col(agg_col).count().alias(agg_col))
667
+ elif agg_type == 'min':
668
+ lf = lf.group_by(group_cols).agg(pl.col(agg_col).min().alias(agg_col))
669
+ elif agg_type == 'max':
670
+ lf = lf.group_by(group_cols).agg(pl.col(agg_col).max().alias(agg_col))
671
+ return lf
672
+
673
+ def handle_pivot_table(lf, params):
674
+ df = lf.collect().to_pandas()
675
+ index = params.get('index')
676
+ columns = params.get('columns_col')
677
+ values = params.get('values')
678
+ aggfunc = params.get('aggfunc', 'mean')
679
+ if index in df.columns and columns in df.columns and values in df.columns:
680
+ df = df.pivot_table(index=index, columns=columns, values=values, aggfunc=aggfunc).reset_index()
681
+ return pl.from_pandas(df).lazy()
682
+ return lf
683
+
684
+ def handle_melt(lf, params):
685
+ df = lf.collect().to_pandas()
686
+ id_vars = params.get('id_vars', [])
687
+ value_vars = params.get('value_vars', [])
688
+ id_vars = [c for c in id_vars if c in df.columns]
689
+ value_vars = [c for c in value_vars if c in df.columns]
690
+ if id_vars or value_vars:
691
+ df = pd.melt(df, id_vars=id_vars, value_vars=value_vars)
692
+ return pl.from_pandas(df).lazy()
693
+ return lf
694
+
695
+ def handle_transpose(lf, params):
696
+ df = lf.collect().transpose(include_header=True, header_name="index")
697
+ return df.lazy()
698
+
699
+ def _handle_custom_binning(lf, params):
700
+ col = params.get('column')
701
+ out_col = params.get('outputColumn')
702
+ bins = params.get('bins', [])
703
+ default_label = params.get('defaultLabel')
704
+
705
+ if not out_col:
706
+ out_col = f"{col}_Bin"
707
+
708
+ if col not in lf.columns:
709
+ raise ValueError(f"Column '{col}' not found in dataset.")
710
+
711
+ parsed_bins = []
712
+ for idx, b in enumerate(bins):
713
+ b_from_val = b.get('from')
714
+ b_to_val = b.get('to')
715
+ b_label = b.get('label')
716
+
717
+ if b_from_val is None or b_to_val is None:
718
+ raise ValueError(f"Bin {idx+1} is missing 'from' or 'to' values.")
719
+
720
+ if b_label is None or str(b_label).strip() == '':
721
+ b_label = f"{b_from_val}–{b_to_val}"
722
+ else:
723
+ b_label = str(b_label).strip()
724
+
725
+ try:
726
+ b_from = float(b_from_val)
727
+ b_to = float(b_to_val)
728
+ except ValueError:
729
+ raise ValueError(f"Bin {idx+1} has non-numeric range values: '{b_from_val}' to '{b_to_val}'.")
730
+
731
+ if b_from > b_to:
732
+ raise ValueError(f"Invalid range in Bin {idx+1} ({b_label}): 'from' ({b_from}) cannot be greater than 'to' ({b_to}).")
733
+
734
+ from_inc = b.get('from_inclusive', True)
735
+ to_inc = b.get('to_inclusive', True)
736
+
737
+ parsed_bins.append((b_from, b_to, b_label, from_inc, to_inc))
738
+
739
+ sorted_parsed = sorted(parsed_bins, key=lambda x: x[0])
740
+
741
+ for i in range(1, len(sorted_parsed)):
742
+ prev_to = sorted_parsed[i-1][1]
743
+ curr_from = sorted_parsed[i][0]
744
+ if curr_from <= prev_to:
745
+ raise ValueError(
746
+ f"Overlapping ranges detected: Bin '{sorted_parsed[i-1][2]}' ({sorted_parsed[i-1][0]} to {sorted_parsed[i-1][1]}) "
747
+ f"overlaps with Bin '{sorted_parsed[i][2]}' ({sorted_parsed[i][0]} to {sorted_parsed[i][1]})."
748
+ )
749
+
750
+ if not sorted_parsed:
751
+ if default_label is not None:
752
+ expr = pl.lit(default_label)
753
+ else:
754
+ expr = pl.lit(None).cast(pl.String)
755
+ return lf.with_columns(expr.alias(out_col))
756
+
757
+ main_expr = None
758
+ for b_from, b_to, b_label, from_inc, to_inc in sorted_parsed:
759
+ cond_from = pl.col(col) >= b_from if from_inc else pl.col(col) > b_from
760
+ cond_to = pl.col(col) <= b_to if to_inc else pl.col(col) < b_to
761
+ cond = cond_from & cond_to
762
+
763
+ if main_expr is None:
764
+ main_expr = pl.when(cond).then(pl.lit(b_label))
765
+ else:
766
+ main_expr = main_expr.when(cond).then(pl.lit(b_label))
767
+
768
+ if main_expr is not None:
769
+ if default_label is not None:
770
+ main_expr = main_expr.otherwise(pl.lit(default_label))
771
+ else:
772
+ main_expr = main_expr.otherwise(pl.lit(None).cast(pl.String))
773
+ lf = lf.with_columns(main_expr.alias(out_col))
774
+
775
+ return lf
776
+
777
+ def _handle_equal_width_binning(lf, params):
778
+ raise NotImplementedError("Equal Width Binning mode not yet implemented.")
779
+
780
+ def _handle_equal_frequency_binning(lf, params):
781
+ raise NotImplementedError("Equal Frequency Binning mode not yet implemented.")
782
+
783
+ def _handle_quantile_binning(lf, params):
784
+ raise NotImplementedError("Quantile Binning mode not yet implemented.")
785
+
786
+ def handle_binning(lf, params):
787
+ mode = params.get('mode', 'custom')
788
+ if mode == 'custom':
789
+ return _handle_custom_binning(lf, params)
790
+ elif mode == 'equal_width':
791
+ return _handle_equal_width_binning(lf, params)
792
+ elif mode == 'equal_frequency':
793
+ return _handle_equal_frequency_binning(lf, params)
794
+ elif mode == 'quantile':
795
+ return _handle_quantile_binning(lf, params)
796
+ else:
797
+ raise ValueError(f"Unsupported binning mode: '{mode}'")
798
+
799
+ TRANSFORMS = {
800
+ 'drop_columns': handle_drop_columns,
801
+ 'fill_null': handle_fill_null,
802
+ 'remove_duplicates': handle_remove_duplicates,
803
+ 'rename_column': handle_rename_column,
804
+ 'min_max_scale': handle_min_max_scale,
805
+ 'standardize': handle_standardize,
806
+ 'lowercase': handle_lowercase,
807
+ 'uppercase': handle_uppercase,
808
+ 'trim_spaces': handle_trim_spaces,
809
+ 'toggle_bool': handle_toggle_bool,
810
+ 'one_hot_encode': handle_one_hot_encode,
811
+ 'change_datatype': handle_change_datatype,
812
+ 'filter_rows': handle_filter_rows,
813
+ 'sort_column': handle_sort_column,
814
+ 'reorder_column': handle_reorder_column,
815
+ 'duplicate_column': handle_duplicate_column,
816
+ 'split_column': handle_split_column,
817
+ 'merge_columns': handle_merge_columns,
818
+ 'ffill': handle_ffill,
819
+ 'bfill': handle_bfill,
820
+ 'interpolate': handle_interpolate,
821
+ 'flag_null': handle_flag_null,
822
+ 'drop_null_rows': handle_drop_null_rows,
823
+ 'drop_cols_null_threshold': handle_drop_cols_null_threshold,
824
+ 'deduplicate_subset': handle_deduplicate_subset,
825
+ 'sample_rows': handle_sample_rows,
826
+ 'drop_rows_index': handle_drop_rows_index,
827
+ 'label_encode': handle_label_encode,
828
+ 'ordinal_encode': handle_ordinal_encode,
829
+ 'binary_encode': handle_binary_encode,
830
+ 'robust_scale': handle_robust_scale,
831
+ 'log_transform': handle_log_transform,
832
+ 'sqrt_transform': handle_sqrt_transform,
833
+ 'power_transform': handle_power_transform,
834
+ 'custom_formula': handle_custom_formula,
835
+ 'bin_bucket': handle_bin_bucket,
836
+ 'date_parts': handle_date_parts,
837
+ 'regex_extraction': handle_regex_extraction,
838
+ 'rolling_window': handle_rolling_window,
839
+ 'interaction_terms': handle_interaction_terms,
840
+ 'correlation_filter': handle_correlation_filter,
841
+ 'variance_threshold': handle_variance_threshold,
842
+ 'select_k_best': handle_select_k_best,
843
+ 'remove_constant_cols': handle_remove_constant_cols,
844
+ 'remove_highly_correlated': handle_remove_highly_correlated,
845
+ 'detect_iqr': handle_detect_iqr,
846
+ 'detect_zscore': handle_detect_zscore,
847
+ 'cap_clip': handle_cap_clip,
848
+ 'remove_outliers': handle_remove_outliers,
849
+ 'replace_substring': handle_replace_substring,
850
+ 'regex_replace': handle_regex_replace,
851
+ 'remove_special_chars': handle_remove_special_chars,
852
+ 'extract_domain': handle_extract_domain,
853
+ 'groupby_aggregate': handle_groupby_aggregate,
854
+ 'pivot_table': handle_pivot_table,
855
+ 'melt': handle_melt,
856
+ 'transpose': handle_transpose,
857
+ 'binning': handle_binning,
858
+ 'custom_binning': handle_binning
859
+ }
860
+
861
+ def load_dataset(path):
862
+ path_lower = path.lower()
863
+ if path_lower.endswith(('.xlsx', '.xls')):
864
+ return pl.read_excel(path).lazy()
865
+ elif path_lower.endswith('.parquet'):
866
+ return pl.scan_parquet(path)
867
+ else:
868
+ return pl.scan_csv(path)
869
+
870
+ def apply_pipeline(lf, steps):
871
+ for step in steps:
872
+ step_type = step.get('type')
873
+ step_params = step.get('params', {})
874
+ if step_type in TRANSFORMS:
875
+ lf = TRANSFORMS[step_type](lf, step_params)
876
+ else:
877
+ raise ValueError(f"Unsupported transformation action: '{step_type}'")
878
+ return lf