datadoc-cli 0.2.0__tar.gz → 0.3.0__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.
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/LICENSE +1 -1
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/PKG-INFO +12 -8
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/README.md +11 -7
- datadoc_cli-0.3.0/comprehensive_test.csv +11 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/cli/app.py +19 -33
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/core/engine.py +84 -2
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/plugins/base.py +15 -21
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/plugins/datetime_feat.py +48 -22
- datadoc_cli-0.3.0/datadoc/plugins/encoders.py +95 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/plugins/missing_values.py +2 -5
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/plugins/outliers.py +28 -26
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/plugins/scaling.py +38 -27
- datadoc_cli-0.3.0/diagnose.py +97 -0
- datadoc_cli-0.3.0/diagnose_output.txt +130 -0
- datadoc_cli-0.3.0/diagnose_output2.txt +76 -0
- datadoc_cli-0.3.0/docs/api-docs.html +140 -0
- datadoc_cli-0.3.0/docs/contribute.html +128 -0
- datadoc_cli-0.3.0/docs/css/style.css +522 -0
- datadoc_cli-0.3.0/docs/how-it-works.html +127 -0
- datadoc_cli-0.3.0/docs/index.html +259 -0
- datadoc_cli-0.3.0/docs/license.html +106 -0
- datadoc_cli-0.3.0/docs/sdk.html +137 -0
- datadoc_cli-0.3.0/docs/setup.html +160 -0
- datadoc_cli-0.3.0/generate_heavy_test.py +113 -0
- datadoc_cli-0.3.0/heavy_test.csv +10001 -0
- datadoc_cli-0.3.0/heavy_test_output.txt +52 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/pyproject.toml +1 -1
- datadoc_cli-0.3.0/test_output.txt +7 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/tests/test_core.py +41 -6
- datadoc_cli-0.3.0/uv.lock +3940 -0
- datadoc_cli-0.2.0/datadoc/plugins/encoders.py +0 -73
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/.github/workflows/ci.yml +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/.gitignore +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/CONTRIBUTING.md +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/FULL_DOCUMENTATION.md +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/__init__.py +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/cli/__init__.py +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/core/__init__.py +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/datadoc/plugins/__init__.py +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/test.csv +0 -0
- {datadoc_cli-0.2.0 → datadoc_cli-0.3.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datadoc-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The Open Source Operating System for Dataset Engineering.
|
|
5
5
|
Project-URL: Homepage, https://github.com/narain-karti/DATADOC
|
|
6
6
|
Project-URL: Repository, https://github.com/narain-karti/DATADOC
|
|
@@ -33,7 +33,7 @@ Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
36
|
-
<pre>
|
|
36
|
+
<pre align="center">
|
|
37
37
|
____ _ _____ _ ____ ___ ____
|
|
38
38
|
| _ \ / \|_ _|/ \ | _ \ / _ \ / ___|
|
|
39
39
|
| | | |/ _ \ | | / _ \ | | | | | | | |
|
|
@@ -44,6 +44,10 @@ Description-Content-Type: text/markdown
|
|
|
44
44
|
|
|
45
45
|
<h3 align="center">The Open Source Operating System for Dataset Engineering.</h3>
|
|
46
46
|
|
|
47
|
+
<p align="center">
|
|
48
|
+
<a href="https://narain-karti.github.io/DATADOC/"><b>📖 View Official Documentation Website</b></a>
|
|
49
|
+
</p>
|
|
50
|
+
|
|
47
51
|
<p align="center">
|
|
48
52
|
<a href="https://pypi.org/project/datadoc-cli/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/datadoc-cli.svg"></a>
|
|
49
53
|
<a href="https://pypi.org/project/datadoc-cli/"><img alt="Python Versions" src="https://img.shields.io/pypi/pyversions/datadoc-cli.svg"></a>
|
|
@@ -51,14 +55,14 @@ Description-Content-Type: text/markdown
|
|
|
51
55
|
</p>
|
|
52
56
|
|
|
53
57
|
<p align="center">
|
|
54
|
-
<a href="#installation">Install</a>
|
|
55
|
-
<a href="#why-datadoc">Why DATADOC?</a>
|
|
56
|
-
<a href="#quick-start">Quick Start</a>
|
|
57
|
-
<a href="#cli-commands">CLI Commands</a>
|
|
58
|
-
<a href="#architecture--plugins">Architecture</a>
|
|
58
|
+
<a href="#installation"><b>Install</b></a> •
|
|
59
|
+
<a href="#why-datadoc"><b>Why DATADOC?</b></a> •
|
|
60
|
+
<a href="#quick-start"><b>Quick Start</b></a> •
|
|
61
|
+
<a href="#cli-commands"><b>CLI Commands</b></a> •
|
|
62
|
+
<a href="#architecture--plugins"><b>Architecture</b></a>
|
|
59
63
|
</p>
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
<hr>
|
|
62
66
|
|
|
63
67
|
## 🚀 What is DATADOC?
|
|
64
68
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<pre>
|
|
2
|
+
<pre align="center">
|
|
3
3
|
____ _ _____ _ ____ ___ ____
|
|
4
4
|
| _ \ / \|_ _|/ \ | _ \ / _ \ / ___|
|
|
5
5
|
| | | |/ _ \ | | / _ \ | | | | | | | |
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
<h3 align="center">The Open Source Operating System for Dataset Engineering.</h3>
|
|
12
12
|
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="https://narain-karti.github.io/DATADOC/"><b>📖 View Official Documentation Website</b></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
13
17
|
<p align="center">
|
|
14
18
|
<a href="https://pypi.org/project/datadoc-cli/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/datadoc-cli.svg"></a>
|
|
15
19
|
<a href="https://pypi.org/project/datadoc-cli/"><img alt="Python Versions" src="https://img.shields.io/pypi/pyversions/datadoc-cli.svg"></a>
|
|
@@ -17,14 +21,14 @@
|
|
|
17
21
|
</p>
|
|
18
22
|
|
|
19
23
|
<p align="center">
|
|
20
|
-
<a href="#installation">Install</a>
|
|
21
|
-
<a href="#why-datadoc">Why DATADOC?</a>
|
|
22
|
-
<a href="#quick-start">Quick Start</a>
|
|
23
|
-
<a href="#cli-commands">CLI Commands</a>
|
|
24
|
-
<a href="#architecture--plugins">Architecture</a>
|
|
24
|
+
<a href="#installation"><b>Install</b></a> •
|
|
25
|
+
<a href="#why-datadoc"><b>Why DATADOC?</b></a> •
|
|
26
|
+
<a href="#quick-start"><b>Quick Start</b></a> •
|
|
27
|
+
<a href="#cli-commands"><b>CLI Commands</b></a> •
|
|
28
|
+
<a href="#architecture--plugins"><b>Architecture</b></a>
|
|
25
29
|
</p>
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
<hr>
|
|
28
32
|
|
|
29
33
|
## 🚀 What is DATADOC?
|
|
30
34
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ID,Name,Age,Income,Score,City,Membership_Level,Signup_Date
|
|
2
|
+
1,Alice,25,50000,0.85,New York,Silver,2023-01-15 10:00:00
|
|
3
|
+
2,Bob,30,60000,0.90,London,Gold,2023-02-20 14:30:00
|
|
4
|
+
3,Charlie,,55000,0.75,,Bronze,2023-03-05 09:15:00
|
|
5
|
+
4,David,45,10000000,0.92,Paris,Gold,2023-04-10 16:45:00
|
|
6
|
+
5,Eve,28,45000,0.88,New York,Silver,2023-05-12 11:20:00
|
|
7
|
+
6,Frank,35,70000,,London,Silver,2023-06-18 08:00:00
|
|
8
|
+
7,Grace,40,-500000,0.95,New York,Bronze,2023-07-22 13:10:00
|
|
9
|
+
8,Heidi,32,65000,0.82,Paris,Gold,2023-08-30 17:55:00
|
|
10
|
+
9,Ivan,29,52000,0.78,London,,2023-09-14 10:40:00
|
|
11
|
+
10,Judy,38,75000,0.89,New York,Bronze,2023-10-01 15:25:00
|
|
@@ -11,10 +11,10 @@ from dotenv import load_dotenv
|
|
|
11
11
|
# Load environment variables (including .env) at startup
|
|
12
12
|
load_dotenv()
|
|
13
13
|
|
|
14
|
-
# Determine the default model from the environment, fallback to
|
|
15
|
-
DEFAULT_MODEL = os.getenv("DATADOC_MODEL", "
|
|
14
|
+
# Determine the default model from the environment, fallback to Groq
|
|
15
|
+
DEFAULT_MODEL = os.getenv("DATADOC_MODEL", "groq/llama-3.3-70b-versatile")
|
|
16
16
|
|
|
17
|
-
from datadoc.core.engine import DATADOC
|
|
17
|
+
from datadoc.core.engine import DATADOC # noqa: E402
|
|
18
18
|
|
|
19
19
|
app = typer.Typer(
|
|
20
20
|
help="DATADOC: The Open Source Operating System for Dataset Engineering.",
|
|
@@ -55,7 +55,7 @@ def load_dataset(file_path: str) -> DATADOC:
|
|
|
55
55
|
print_step("[>>]", f"Loading [cyan]{file_path}[/cyan]...")
|
|
56
56
|
try:
|
|
57
57
|
doc = DATADOC(file_path)
|
|
58
|
-
except
|
|
58
|
+
except (FileNotFoundError, ValueError, RuntimeError) as e:
|
|
59
59
|
console.print(f"\n [bold red][X] Failed to read dataset:[/] {e}")
|
|
60
60
|
raise typer.Exit(code=1)
|
|
61
61
|
|
|
@@ -65,23 +65,15 @@ def load_dataset(file_path: str) -> DATADOC:
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
def _get_api_key(model: str) -> str:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
key_name = "ANTHROPIC_API_KEY"
|
|
78
|
-
elif model.startswith("groq"):
|
|
79
|
-
api_key = os.getenv("GROQ_API_KEY")
|
|
80
|
-
key_name = "GROQ_API_KEY"
|
|
81
|
-
else:
|
|
82
|
-
api_key = os.getenv("OPENAI_API_KEY") # Default fallback
|
|
83
|
-
key_name = "API_KEY"
|
|
84
|
-
|
|
68
|
+
key_map = {
|
|
69
|
+
"gemini": "GEMINI_API_KEY",
|
|
70
|
+
"gpt": "OPENAI_API_KEY", "openai": "OPENAI_API_KEY",
|
|
71
|
+
"claude": "ANTHROPIC_API_KEY", "anthropic": "ANTHROPIC_API_KEY",
|
|
72
|
+
"groq": "GROQ_API_KEY"
|
|
73
|
+
}
|
|
74
|
+
key_name = next((v for k, v in key_map.items() if model.startswith(k)), "API_KEY")
|
|
75
|
+
api_key = os.getenv("OPENAI_API_KEY") if key_name == "API_KEY" else os.getenv(key_name)
|
|
76
|
+
|
|
85
77
|
if not api_key:
|
|
86
78
|
console.print(f"\n [bold yellow][!][/bold yellow] {key_name} not found in environment or .env file.")
|
|
87
79
|
api_key = typer.prompt(f"Please enter your {key_name}", hide_input=True)
|
|
@@ -149,7 +141,7 @@ def analyze(
|
|
|
149
141
|
has_issue = p_stats.get("has_scale_issues", False)
|
|
150
142
|
ratio = p_stats.get("scale_ratio", 0)
|
|
151
143
|
if has_issue:
|
|
152
|
-
table.add_row("Scale Mismatch", f"{ratio}x",
|
|
144
|
+
table.add_row("Scale Mismatch", f"{ratio}x", "[yellow][!!] Needs scaling[/yellow]")
|
|
153
145
|
else:
|
|
154
146
|
table.add_row("Scale Mismatch", "--", "[green][OK] Balanced[/green]")
|
|
155
147
|
|
|
@@ -389,7 +381,6 @@ def visualize(file_path: str):
|
|
|
389
381
|
Generates a massive, interactive terminal dashboard comparing the before and after states.
|
|
390
382
|
"""
|
|
391
383
|
import plotext as plt
|
|
392
|
-
import numpy as np
|
|
393
384
|
import sys
|
|
394
385
|
import polars as pl
|
|
395
386
|
|
|
@@ -520,8 +511,8 @@ def report(file_path: str):
|
|
|
520
511
|
"",
|
|
521
512
|
"## Dataset Overview",
|
|
522
513
|
"",
|
|
523
|
-
|
|
524
|
-
|
|
514
|
+
"| Metric | Value |",
|
|
515
|
+
"|--------|-------|",
|
|
525
516
|
f"| File | `{file_path}` |",
|
|
526
517
|
f"| Rows | {report_data['rows']:,} |",
|
|
527
518
|
f"| Columns | {report_data['cols']} |",
|
|
@@ -585,12 +576,7 @@ def plugin_list():
|
|
|
585
576
|
"""
|
|
586
577
|
print_banner()
|
|
587
578
|
|
|
588
|
-
|
|
589
|
-
# We don't need a real file for this
|
|
590
|
-
import io
|
|
591
|
-
dummy_csv = b"a,b\n1,2\n"
|
|
592
|
-
import polars as pl
|
|
593
|
-
dummy_df = pl.read_csv(dummy_csv)
|
|
579
|
+
|
|
594
580
|
|
|
595
581
|
from datadoc.plugins.missing_values import MissingValuePlugin
|
|
596
582
|
from datadoc.plugins.outliers import OutlierPlugin
|
|
@@ -784,7 +770,7 @@ Be helpful, analytical, and concise. Format your responses with markdown."""
|
|
|
784
770
|
tool_result = f"Dataset successfully saved to {args['filename']}."
|
|
785
771
|
else:
|
|
786
772
|
tool_result = f"Error: Unknown function {func_name}"
|
|
787
|
-
except
|
|
773
|
+
except (ValueError, KeyError, RuntimeError) as e:
|
|
788
774
|
tool_result = f"Error executing {func_name}: {e}"
|
|
789
775
|
|
|
790
776
|
messages.append({
|
|
@@ -812,7 +798,7 @@ Be helpful, analytical, and concise. Format your responses with markdown."""
|
|
|
812
798
|
except KeyboardInterrupt:
|
|
813
799
|
console.print("\n[bold green]Ending chat session. Goodbye![/bold green]\n")
|
|
814
800
|
break
|
|
815
|
-
except
|
|
801
|
+
except (KeyboardInterrupt, RuntimeError) as e:
|
|
816
802
|
console.print(f"\n[bold red]Error:[/] {e}")
|
|
817
803
|
|
|
818
804
|
if __name__ == "__main__":
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import polars as pl
|
|
2
|
-
from typing import Dict, Any, List
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
3
|
import json
|
|
4
4
|
from collections import Counter
|
|
5
5
|
import litellm
|
|
@@ -26,6 +26,50 @@ class DATADOC:
|
|
|
26
26
|
ScalingPlugin(),
|
|
27
27
|
], key=lambda p: p.priority)
|
|
28
28
|
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _detect_column_roles(df: pl.DataFrame) -> Dict[str, str]:
|
|
31
|
+
"""Classify each column as 'id', 'name', 'constant', or 'feature'."""
|
|
32
|
+
roles = {}
|
|
33
|
+
for col in df.columns:
|
|
34
|
+
series = df[col]
|
|
35
|
+
# Constant columns (single unique value)
|
|
36
|
+
if series.drop_nulls().n_unique() <= 1:
|
|
37
|
+
roles[col] = "constant"
|
|
38
|
+
# Likely ID: integer, all unique, monotonically increasing
|
|
39
|
+
elif series.dtype.is_integer() and series.drop_nulls().n_unique() == series.drop_nulls().len() and series.drop_nulls().len() > 0:
|
|
40
|
+
sorted_vals = series.drop_nulls().sort()
|
|
41
|
+
diffs = sorted_vals.diff().drop_nulls()
|
|
42
|
+
if diffs.len() > 0 and (diffs == diffs[0]).all():
|
|
43
|
+
roles[col] = "id"
|
|
44
|
+
else:
|
|
45
|
+
roles[col] = "feature"
|
|
46
|
+
# String columns need extra checks
|
|
47
|
+
elif series.dtype == pl.String and series.drop_nulls().n_unique() == series.drop_nulls().len() and series.drop_nulls().len() > 0:
|
|
48
|
+
# Check if it's a datetime string before labelling as name
|
|
49
|
+
try:
|
|
50
|
+
parsed = df.select(pl.col(col).str.to_datetime(strict=False))
|
|
51
|
+
not_null_ratio = 1 - (parsed[col].null_count() / max(parsed.height, 1))
|
|
52
|
+
if not_null_ratio > 0.5:
|
|
53
|
+
roles[col] = "feature" # Datetime string — let DatetimePlugin handle it
|
|
54
|
+
else:
|
|
55
|
+
roles[col] = "name"
|
|
56
|
+
except (ValueError, TypeError, pl.exceptions.ComputeError, pl.exceptions.InvalidOperationError):
|
|
57
|
+
roles[col] = "name"
|
|
58
|
+
else:
|
|
59
|
+
roles[col] = "feature"
|
|
60
|
+
return roles
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _drop_constant_columns(df: pl.DataFrame) -> tuple[pl.DataFrame, list[str]]:
|
|
64
|
+
"""Drop columns with zero variance (single unique non-null value)."""
|
|
65
|
+
to_drop = []
|
|
66
|
+
for col in df.columns:
|
|
67
|
+
if df[col].drop_nulls().n_unique() <= 1:
|
|
68
|
+
to_drop.append(col)
|
|
69
|
+
if to_drop:
|
|
70
|
+
df = df.drop(to_drop)
|
|
71
|
+
return df, to_drop
|
|
72
|
+
|
|
29
73
|
def analyze(self) -> Dict[str, Any]:
|
|
30
74
|
"""Runs the analyze step across all plugins."""
|
|
31
75
|
dtype_counts = Counter([str(dt) for dt in self.df.dtypes])
|
|
@@ -89,6 +133,17 @@ class DATADOC:
|
|
|
89
133
|
|
|
90
134
|
self._applied_plugins = []
|
|
91
135
|
self._skipped_plugins = []
|
|
136
|
+
self._dropped_columns = []
|
|
137
|
+
|
|
138
|
+
# Phase 0: Drop ID and name columns before any plugin runs
|
|
139
|
+
roles = self._detect_column_roles(df_transformed)
|
|
140
|
+
cols_to_drop = [c for c, r in roles.items() if r in ("id", "name")]
|
|
141
|
+
if cols_to_drop:
|
|
142
|
+
df_transformed = df_transformed.drop(cols_to_drop)
|
|
143
|
+
self._dropped_columns.extend(cols_to_drop)
|
|
144
|
+
if progress_callback:
|
|
145
|
+
progress_callback("ColumnFilter", "applied",
|
|
146
|
+
[f"Dropped non-feature columns: {', '.join(cols_to_drop)}"])
|
|
92
147
|
|
|
93
148
|
for plugin in self.plugins:
|
|
94
149
|
if progress_callback:
|
|
@@ -107,6 +162,14 @@ class DATADOC:
|
|
|
107
162
|
if progress_callback:
|
|
108
163
|
progress_callback(plugin.name, "skipped", [])
|
|
109
164
|
|
|
165
|
+
# Phase final: Drop any constant columns created during pipeline
|
|
166
|
+
df_transformed, dropped_constants = self._drop_constant_columns(df_transformed)
|
|
167
|
+
if dropped_constants:
|
|
168
|
+
self._dropped_columns.extend(dropped_constants)
|
|
169
|
+
if progress_callback:
|
|
170
|
+
progress_callback("ColumnFilter", "applied",
|
|
171
|
+
[f"Dropped constant columns: {', '.join(dropped_constants)}"])
|
|
172
|
+
|
|
110
173
|
return df_transformed
|
|
111
174
|
|
|
112
175
|
def compare(self, clean_df: pl.DataFrame) -> Dict[str, Any]:
|
|
@@ -230,7 +293,7 @@ Respond in plain text formatted nicely with bullet points and paragraphs where a
|
|
|
230
293
|
|
|
231
294
|
try:
|
|
232
295
|
planner_response = self._generate_ai_plan(model, goal, api_key)
|
|
233
|
-
except
|
|
296
|
+
except (RuntimeError, ValueError, TypeError) as e:
|
|
234
297
|
if progress_callback:
|
|
235
298
|
progress_callback("AI Planner", "error", [f"LLM API Error: {e}"])
|
|
236
299
|
raise RuntimeError(f"AI Planner failed: {e}")
|
|
@@ -238,6 +301,17 @@ Respond in plain text formatted nicely with bullet points and paragraphs where a
|
|
|
238
301
|
df_transformed = self.df.clone()
|
|
239
302
|
self._applied_plugins = []
|
|
240
303
|
self._skipped_plugins = []
|
|
304
|
+
self._dropped_columns = []
|
|
305
|
+
|
|
306
|
+
# Phase 0: Drop ID and name columns before any plugin runs
|
|
307
|
+
roles = self._detect_column_roles(df_transformed)
|
|
308
|
+
cols_to_drop = [c for c, r in roles.items() if r in ("id", "name")]
|
|
309
|
+
if cols_to_drop:
|
|
310
|
+
df_transformed = df_transformed.drop(cols_to_drop)
|
|
311
|
+
self._dropped_columns.extend(cols_to_drop)
|
|
312
|
+
if progress_callback:
|
|
313
|
+
progress_callback("ColumnFilter", "applied",
|
|
314
|
+
[f"Dropped non-feature columns: {', '.join(cols_to_drop)}"])
|
|
241
315
|
|
|
242
316
|
if progress_callback:
|
|
243
317
|
progress_callback("AI Planner", "applied", [f"Generated plan with {len(planner_response.plan)} steps."])
|
|
@@ -266,6 +340,14 @@ Respond in plain text formatted nicely with bullet points and paragraphs where a
|
|
|
266
340
|
self._skipped_plugins.append(p.name)
|
|
267
341
|
if progress_callback:
|
|
268
342
|
progress_callback(p.name, "skipped", ["Skipped by AI Planner"])
|
|
343
|
+
|
|
344
|
+
# Phase final: Drop any constant columns created during pipeline
|
|
345
|
+
df_transformed, dropped_constants = self._drop_constant_columns(df_transformed)
|
|
346
|
+
if dropped_constants:
|
|
347
|
+
self._dropped_columns.extend(dropped_constants)
|
|
348
|
+
if progress_callback:
|
|
349
|
+
progress_callback("ColumnFilter", "applied",
|
|
350
|
+
[f"Dropped constant columns: {', '.join(dropped_constants)}"])
|
|
269
351
|
|
|
270
352
|
return df_transformed
|
|
271
353
|
|
|
@@ -21,12 +21,13 @@ class BasePlugin(ABC):
|
|
|
21
21
|
return 50
|
|
22
22
|
|
|
23
23
|
@property
|
|
24
|
-
def
|
|
25
|
-
|
|
24
|
+
def dependencies(self) -> list[str]:
|
|
25
|
+
"""List of plugin names this plugin depends on."""
|
|
26
|
+
return []
|
|
26
27
|
|
|
27
28
|
@property
|
|
28
|
-
def
|
|
29
|
-
"""List of
|
|
29
|
+
def supported_datatypes(self) -> list[str]:
|
|
30
|
+
"""List of datatypes this plugin can handle."""
|
|
30
31
|
return []
|
|
31
32
|
|
|
32
33
|
@abstractmethod
|
|
@@ -49,26 +50,19 @@ class BasePlugin(ABC):
|
|
|
49
50
|
"""Apply the engineering transformation and return the new dataframe."""
|
|
50
51
|
pass
|
|
51
52
|
|
|
52
|
-
def validate(self, df: pl.DataFrame) -> bool:
|
|
53
|
-
"""Validate that the transformation produced a valid result."""
|
|
54
|
-
if df.is_empty():
|
|
55
|
-
return False
|
|
56
|
-
return True
|
|
57
|
-
|
|
58
|
-
def rollback(self, original_df: pl.DataFrame) -> pl.DataFrame:
|
|
59
|
-
"""Return the original dataframe, undoing any transformation."""
|
|
60
|
-
return original_df.clone()
|
|
61
|
-
|
|
62
53
|
def explain(self) -> str:
|
|
63
54
|
"""Return a human-readable explanation of what this plugin does."""
|
|
64
55
|
return f"{self.name} (v{self.version}): {self.description}"
|
|
65
56
|
|
|
66
|
-
def
|
|
67
|
-
"""
|
|
68
|
-
return
|
|
57
|
+
def validate(self, df: pl.DataFrame) -> bool:
|
|
58
|
+
"""Validate that the plugin's transformation was applied correctly."""
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def rollback(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
62
|
+
"""Rollback the transformation (default: return df unchanged)."""
|
|
63
|
+
return df
|
|
69
64
|
|
|
70
65
|
def estimate_runtime(self, df: pl.DataFrame) -> float:
|
|
71
|
-
"""Estimate runtime in seconds based on
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return round(rows / 1_000_000, 4)
|
|
66
|
+
"""Estimate runtime in seconds based on dataframe size."""
|
|
67
|
+
return df.height * df.width * 1e-6
|
|
68
|
+
|
|
@@ -12,19 +12,22 @@ class DatetimePlugin(BasePlugin):
|
|
|
12
12
|
|
|
13
13
|
@property
|
|
14
14
|
def description(self) -> str:
|
|
15
|
-
return "Detects datetime-like columns and extracts year, month, day, dayofweek features."
|
|
15
|
+
return "Detects datetime-like columns and extracts year, month, day, dayofweek, and hour features. Drops constant-value features."
|
|
16
16
|
|
|
17
17
|
@property
|
|
18
18
|
def priority(self) -> int:
|
|
19
19
|
return 30 # After missing values and outliers, before encoding
|
|
20
20
|
|
|
21
|
-
@
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _has_time_component(series: pl.Series) -> bool:
|
|
23
|
+
"""Check if a datetime series has meaningful time components (not all midnight)."""
|
|
24
|
+
times = series.drop_nulls()
|
|
25
|
+
if times.len() == 0:
|
|
26
|
+
return False
|
|
27
|
+
hours = times.dt.hour()
|
|
28
|
+
minutes = times.dt.minute()
|
|
29
|
+
# If all hours and minutes are 0, there's no meaningful time info
|
|
30
|
+
return not ((hours == 0).all() and (minutes == 0).all())
|
|
28
31
|
|
|
29
32
|
def analyze(self, df: pl.DataFrame) -> dict:
|
|
30
33
|
datetime_cols = []
|
|
@@ -39,7 +42,8 @@ class DatetimePlugin(BasePlugin):
|
|
|
39
42
|
not_null_ratio = 1 - (parsed[col].null_count() / parsed.height)
|
|
40
43
|
if not_null_ratio > 0.5:
|
|
41
44
|
datetime_cols.append(col)
|
|
42
|
-
except
|
|
45
|
+
except (ValueError, TypeError, pl.exceptions.ComputeError, pl.exceptions.InvalidOperationError):
|
|
46
|
+
# Column is not a valid datetime string; skipping
|
|
43
47
|
pass
|
|
44
48
|
|
|
45
49
|
return {
|
|
@@ -53,7 +57,8 @@ class DatetimePlugin(BasePlugin):
|
|
|
53
57
|
cols = analysis_result["datetime_columns"]
|
|
54
58
|
recs.append(
|
|
55
59
|
f"Datetime columns detected: {', '.join(cols)}. "
|
|
56
|
-
f"Recommendation: Extract year, month, day, day_of_week features
|
|
60
|
+
f"Recommendation: Extract year, month, day, day_of_week, hour features. "
|
|
61
|
+
f"Drop original column and any constant features."
|
|
57
62
|
)
|
|
58
63
|
return recs
|
|
59
64
|
|
|
@@ -67,12 +72,23 @@ for col in datetime_cols:
|
|
|
67
72
|
if df[col].dtype == pl.String:
|
|
68
73
|
df = df.with_columns(pl.col(col).str.to_datetime(strict=False).alias(col))
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
new_cols = [
|
|
71
76
|
pl.col(col).dt.year().alias(col + '_year'),
|
|
72
77
|
pl.col(col).dt.month().alias(col + '_month'),
|
|
73
78
|
pl.col(col).dt.day().alias(col + '_day'),
|
|
74
|
-
pl.col(col).dt.weekday().alias(col + '_dayofweek')
|
|
75
|
-
]
|
|
79
|
+
pl.col(col).dt.weekday().alias(col + '_dayofweek'),
|
|
80
|
+
]
|
|
81
|
+
# Add hour if time component exists
|
|
82
|
+
hours = df[col].dt.hour()
|
|
83
|
+
if not ((hours == 0).all()):
|
|
84
|
+
new_cols.append(pl.col(col).dt.hour().alias(col + '_hour'))
|
|
85
|
+
|
|
86
|
+
df = df.with_columns(new_cols).drop(col)
|
|
87
|
+
|
|
88
|
+
# Drop any constant features (e.g. year when all dates are same year)
|
|
89
|
+
for c in [c for c in df.columns if c.startswith(col + '_')]:
|
|
90
|
+
if df[c].drop_nulls().n_unique() <= 1:
|
|
91
|
+
df = df.drop(c)"""
|
|
76
92
|
|
|
77
93
|
def apply(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
78
94
|
df_clean = df.clone()
|
|
@@ -81,22 +97,32 @@ for col in datetime_cols:
|
|
|
81
97
|
for col in dt_cols:
|
|
82
98
|
if df_clean[col].dtype == pl.String:
|
|
83
99
|
df_clean = df_clean.with_columns(pl.col(col).str.to_datetime(strict=False).alias(col))
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
|
|
101
|
+
new_cols = [
|
|
86
102
|
pl.col(col).dt.year().alias(col + '_year'),
|
|
87
103
|
pl.col(col).dt.month().alias(col + '_month'),
|
|
88
104
|
pl.col(col).dt.day().alias(col + '_day'),
|
|
89
|
-
pl.col(col).dt.weekday().alias(col + '_dayofweek')
|
|
90
|
-
]
|
|
105
|
+
pl.col(col).dt.weekday().alias(col + '_dayofweek'),
|
|
106
|
+
]
|
|
91
107
|
|
|
92
|
-
|
|
108
|
+
# Add hour if time component exists
|
|
109
|
+
if self._has_time_component(df_clean[col]):
|
|
110
|
+
new_cols.append(pl.col(col).dt.hour().alias(col + '_hour'))
|
|
111
|
+
|
|
112
|
+
df_clean = df_clean.with_columns(new_cols).drop(col)
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
|
|
114
|
+
# Drop constant datetime features (e.g. year=2023 for all rows)
|
|
115
|
+
for c in [c for c in df_clean.columns if c.startswith(col + '_')]:
|
|
116
|
+
if df_clean[c].drop_nulls().n_unique() <= 1:
|
|
117
|
+
df_clean = df_clean.drop(c)
|
|
118
|
+
|
|
119
|
+
return df_clean
|
|
96
120
|
|
|
97
121
|
def explain(self) -> str:
|
|
98
122
|
return (
|
|
99
123
|
"DatetimePlugin detects columns containing dates/times and extracts "
|
|
100
|
-
"year, month, day, and
|
|
101
|
-
"The original datetime column is dropped
|
|
124
|
+
"year, month, day, day_of_week, and hour as new numeric features. "
|
|
125
|
+
"The original datetime column is dropped, along with any constant features "
|
|
126
|
+
"(e.g., year when all dates are in the same year)."
|
|
102
127
|
)
|
|
128
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from datadoc.plugins.base import BasePlugin
|
|
3
|
+
|
|
4
|
+
class CategoricalEncoderPlugin(BasePlugin):
|
|
5
|
+
@property
|
|
6
|
+
def name(self) -> str:
|
|
7
|
+
return "CategoricalEncoderPlugin"
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def version(self) -> str:
|
|
11
|
+
return "0.1.0"
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def description(self) -> str:
|
|
15
|
+
return "Drops high-cardinality identifier columns and applies One-Hot Encoding (drop_first=True) to low-cardinality categoricals."
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def priority(self) -> int:
|
|
19
|
+
return 40 # After missing values and outliers
|
|
20
|
+
|
|
21
|
+
def analyze(self, df: pl.DataFrame) -> dict:
|
|
22
|
+
str_cols = [col for col in df.columns if df[col].dtype == pl.String]
|
|
23
|
+
|
|
24
|
+
# Identifier columns: every value is unique (e.g. Name, Email)
|
|
25
|
+
id_cols = [col for col in str_cols if df[col].drop_nulls().n_unique() >= df.height]
|
|
26
|
+
|
|
27
|
+
# Encodable categorical: low cardinality (2-9 unique), not an identifier
|
|
28
|
+
valid_cats = [
|
|
29
|
+
col for col in str_cols
|
|
30
|
+
if col not in id_cols and 1 < df[col].n_unique() < 10
|
|
31
|
+
]
|
|
32
|
+
cardinality = {col: df[col].n_unique() for col in valid_cats}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"has_categorical": len(valid_cats) > 0 or len(id_cols) > 0,
|
|
36
|
+
"categorical_columns": valid_cats,
|
|
37
|
+
"identifier_columns": id_cols,
|
|
38
|
+
"cardinality": cardinality,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def recommend(self, analysis_result: dict) -> list[str]:
|
|
42
|
+
recs = []
|
|
43
|
+
id_cols = analysis_result.get("identifier_columns", [])
|
|
44
|
+
if id_cols:
|
|
45
|
+
recs.append(
|
|
46
|
+
f"Identifier columns detected: {', '.join(id_cols)}. "
|
|
47
|
+
f"Recommendation: Drop these (every value is unique, no ML signal)."
|
|
48
|
+
)
|
|
49
|
+
if analysis_result.get("categorical_columns"):
|
|
50
|
+
info = analysis_result.get("cardinality", {})
|
|
51
|
+
detail = ", ".join([f"{c} ({v} unique)" for c, v in info.items()])
|
|
52
|
+
recs.append(
|
|
53
|
+
f"Categorical columns found: {detail}. "
|
|
54
|
+
f"Recommendation: Apply One-Hot Encoding."
|
|
55
|
+
)
|
|
56
|
+
return recs
|
|
57
|
+
|
|
58
|
+
def generate_code(self, analysis_result: dict) -> str:
|
|
59
|
+
lines = []
|
|
60
|
+
id_cols = analysis_result.get("identifier_columns", [])
|
|
61
|
+
if id_cols:
|
|
62
|
+
lines.append(f"# Drop identifier columns")
|
|
63
|
+
lines.append(f"df = df.drop({id_cols})")
|
|
64
|
+
|
|
65
|
+
cat_cols = analysis_result.get("categorical_columns", [])
|
|
66
|
+
if cat_cols:
|
|
67
|
+
cols_str = str(cat_cols)
|
|
68
|
+
lines.append(f"# Categorical Encoding (One-Hot)")
|
|
69
|
+
lines.append(f"cat_cols = {cols_str}")
|
|
70
|
+
lines.append(f"df = df.to_dummies(columns=cat_cols, drop_first=True)")
|
|
71
|
+
|
|
72
|
+
return "\n".join(lines)
|
|
73
|
+
|
|
74
|
+
def apply(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
75
|
+
df_clean = df.clone()
|
|
76
|
+
analysis = self.analyze(df_clean)
|
|
77
|
+
|
|
78
|
+
# Drop identifier columns first
|
|
79
|
+
id_cols = analysis.get("identifier_columns", [])
|
|
80
|
+
if id_cols:
|
|
81
|
+
df_clean = df_clean.drop(id_cols)
|
|
82
|
+
|
|
83
|
+
# One-hot encode low-cardinality categoricals
|
|
84
|
+
cat_cols = analysis.get("categorical_columns", [])
|
|
85
|
+
if cat_cols:
|
|
86
|
+
df_clean = df_clean.to_dummies(columns=cat_cols, drop_first=True)
|
|
87
|
+
return df_clean
|
|
88
|
+
|
|
89
|
+
def explain(self) -> str:
|
|
90
|
+
return (
|
|
91
|
+
"CategoricalEncoderPlugin detects text/string columns. "
|
|
92
|
+
"High-cardinality identifier columns (every value unique) are dropped. "
|
|
93
|
+
"Low-cardinality columns (< 10 unique values) are One-Hot Encoded with drop_first=True."
|
|
94
|
+
)
|
|
95
|
+
|
|
@@ -18,9 +18,7 @@ class MissingValuePlugin(BasePlugin):
|
|
|
18
18
|
def priority(self) -> int:
|
|
19
19
|
return 10 # Should run first
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
def supported_datatypes(self) -> list:
|
|
23
|
-
return ["numeric", "categorical"]
|
|
21
|
+
|
|
24
22
|
|
|
25
23
|
def analyze(self, df: pl.DataFrame) -> dict:
|
|
26
24
|
cols_with_missing = {c: df[c].null_count() for c in df.columns if df[c].null_count() > 0}
|
|
@@ -64,8 +62,7 @@ for col in df.columns:
|
|
|
64
62
|
df_clean = df_clean.with_columns(pl.col(col).fill_null(pl.col(col).drop_nulls().mode().first()))
|
|
65
63
|
return df_clean
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
return bool(sum(df[c].null_count() for c in df.columns) == 0)
|
|
65
|
+
|
|
69
66
|
|
|
70
67
|
def explain(self) -> str:
|
|
71
68
|
return (
|