datadoc-cli 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.
@@ -0,0 +1,70 @@
1
+ from abc import ABC, abstractmethod
2
+ import polars as pl
3
+
4
+ class BasePlugin(ABC):
5
+ @property
6
+ @abstractmethod
7
+ def name(self) -> str:
8
+ pass
9
+
10
+ @property
11
+ def version(self) -> str:
12
+ return "0.1.0"
13
+
14
+ @property
15
+ def description(self) -> str:
16
+ return "No description provided."
17
+
18
+ @property
19
+ def priority(self) -> int:
20
+ """Lower number = runs first. Default is 50."""
21
+ return 50
22
+
23
+ @property
24
+ def supported_datatypes(self) -> list:
25
+ return ["numeric", "categorical"]
26
+
27
+ @property
28
+ def dependencies(self) -> list:
29
+ """List of plugin names that must run before this one."""
30
+ return []
31
+
32
+ @abstractmethod
33
+ def analyze(self, df: pl.DataFrame) -> dict:
34
+ """Analyze the dataframe and return metadata/flags if the plugin should run."""
35
+ pass
36
+
37
+ @abstractmethod
38
+ def recommend(self, analysis_result: dict) -> list[str]:
39
+ """Return a list of recommendations based on the analysis."""
40
+ pass
41
+
42
+ @abstractmethod
43
+ def generate_code(self, analysis_result: dict) -> str:
44
+ """Return the Python code string to replicate this plugin's transformation."""
45
+ pass
46
+
47
+ @abstractmethod
48
+ def apply(self, df: pl.DataFrame) -> pl.DataFrame:
49
+ """Apply the engineering transformation and return the new dataframe."""
50
+ pass
51
+
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
+ def explain(self) -> str:
63
+ """Return a human-readable explanation of what this plugin does."""
64
+ return f"{self.name} (v{self.version}): {self.description}"
65
+
66
+ def estimate_runtime(self, df: pl.DataFrame) -> float:
67
+ """Estimate runtime in seconds based on dataset size."""
68
+ rows = df.height
69
+ # Simple linear estimate: ~1ms per 1000 rows
70
+ return round(rows / 1_000_000, 4)
@@ -0,0 +1,102 @@
1
+ import polars as pl
2
+ from datadoc.plugins.base import BasePlugin
3
+
4
+ class DatetimePlugin(BasePlugin):
5
+ @property
6
+ def name(self) -> str:
7
+ return "DatetimePlugin"
8
+
9
+ @property
10
+ def version(self) -> str:
11
+ return "0.1.0"
12
+
13
+ @property
14
+ def description(self) -> str:
15
+ return "Detects datetime-like columns and extracts year, month, day, dayofweek features."
16
+
17
+ @property
18
+ def priority(self) -> int:
19
+ return 30 # After missing values and outliers, before encoding
20
+
21
+ @property
22
+ def supported_datatypes(self) -> list:
23
+ return ["datetime"]
24
+
25
+ @property
26
+ def dependencies(self) -> list:
27
+ return ["MissingValuePlugin"]
28
+
29
+ def analyze(self, df: pl.DataFrame) -> dict:
30
+ datetime_cols = []
31
+ for col in df.columns:
32
+ if df[col].dtype in [pl.Date, pl.Datetime]:
33
+ datetime_cols.append(col)
34
+ elif df[col].dtype == pl.String:
35
+ try:
36
+ parsed = df.select(pl.col(col).str.to_datetime(strict=False))
37
+ # if more than 50% are successfully parsed
38
+ if parsed.height > 0:
39
+ not_null_ratio = 1 - (parsed[col].null_count() / parsed.height)
40
+ if not_null_ratio > 0.5:
41
+ datetime_cols.append(col)
42
+ except Exception:
43
+ pass
44
+
45
+ return {
46
+ "has_datetime": len(datetime_cols) > 0,
47
+ "datetime_columns": datetime_cols,
48
+ }
49
+
50
+ def recommend(self, analysis_result: dict) -> list[str]:
51
+ recs = []
52
+ if analysis_result.get("has_datetime"):
53
+ cols = analysis_result["datetime_columns"]
54
+ recs.append(
55
+ f"Datetime columns detected: {', '.join(cols)}. "
56
+ f"Recommendation: Extract year, month, day, day_of_week features and drop original."
57
+ )
58
+ return recs
59
+
60
+ def generate_code(self, analysis_result: dict) -> str:
61
+ if not analysis_result.get("has_datetime"):
62
+ return ""
63
+ cols_str = str(analysis_result.get("datetime_columns", []))
64
+ return f"""# Datetime Feature Extraction
65
+ datetime_cols = {cols_str}
66
+ for col in datetime_cols:
67
+ if df[col].dtype == pl.String:
68
+ df = df.with_columns(pl.col(col).str.to_datetime(strict=False).alias(col))
69
+
70
+ df = df.with_columns([
71
+ pl.col(col).dt.year().alias(col + '_year'),
72
+ pl.col(col).dt.month().alias(col + '_month'),
73
+ pl.col(col).dt.day().alias(col + '_day'),
74
+ pl.col(col).dt.weekday().alias(col + '_dayofweek')
75
+ ]).drop(col)"""
76
+
77
+ def apply(self, df: pl.DataFrame) -> pl.DataFrame:
78
+ df_clean = df.clone()
79
+ dt_cols = self.analyze(df_clean).get("datetime_columns", [])
80
+
81
+ for col in dt_cols:
82
+ if df_clean[col].dtype == pl.String:
83
+ df_clean = df_clean.with_columns(pl.col(col).str.to_datetime(strict=False).alias(col))
84
+
85
+ df_clean = df_clean.with_columns([
86
+ pl.col(col).dt.year().alias(col + '_year'),
87
+ pl.col(col).dt.month().alias(col + '_month'),
88
+ pl.col(col).dt.day().alias(col + '_day'),
89
+ pl.col(col).dt.weekday().alias(col + '_dayofweek')
90
+ ]).drop(col)
91
+
92
+ return df_clean
93
+
94
+ def validate(self, df: pl.DataFrame) -> bool:
95
+ return True
96
+
97
+ def explain(self) -> str:
98
+ return (
99
+ "DatetimePlugin detects columns containing dates/times and extracts "
100
+ "year, month, day, and day_of_week as new numeric features. "
101
+ "The original datetime column is dropped."
102
+ )
@@ -0,0 +1,73 @@
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 "Detects categorical columns and applies One-Hot Encoding (drop_first=True)."
16
+
17
+ @property
18
+ def priority(self) -> int:
19
+ return 40 # After missing values and outliers
20
+
21
+ @property
22
+ def supported_datatypes(self) -> list:
23
+ return ["categorical"]
24
+
25
+ @property
26
+ def dependencies(self) -> list:
27
+ return ["MissingValuePlugin"]
28
+
29
+ def analyze(self, df: pl.DataFrame) -> dict:
30
+ cat_cols = [col for col in df.columns if df[col].dtype == pl.String]
31
+ valid_cats = [col for col in cat_cols if df[col].n_unique() < 10 and df[col].n_unique() > 1]
32
+ cardinality = {col: df[col].n_unique() for col in valid_cats}
33
+
34
+ return {
35
+ "has_categorical": len(valid_cats) > 0,
36
+ "categorical_columns": valid_cats,
37
+ "cardinality": cardinality,
38
+ }
39
+
40
+ def recommend(self, analysis_result: dict) -> list[str]:
41
+ recs = []
42
+ if analysis_result.get("has_categorical"):
43
+ info = analysis_result.get("cardinality", {})
44
+ detail = ", ".join([f"{c} ({v} unique)" for c, v in info.items()])
45
+ recs.append(
46
+ f"Categorical columns found: {detail}. "
47
+ f"Recommendation: Apply One-Hot Encoding."
48
+ )
49
+ return recs
50
+
51
+ def generate_code(self, analysis_result: dict) -> str:
52
+ if not analysis_result.get("has_categorical"):
53
+ return ""
54
+ cols_str = str(analysis_result.get("categorical_columns", []))
55
+ return f"""# Categorical Encoding (One-Hot)
56
+ cat_cols = {cols_str}
57
+ df = df.to_dummies(columns=cat_cols, drop_first=True)"""
58
+
59
+ def apply(self, df: pl.DataFrame) -> pl.DataFrame:
60
+ df_clean = df.clone()
61
+ cat_cols = self.analyze(df_clean).get("categorical_columns", [])
62
+ if cat_cols:
63
+ df_clean = df_clean.to_dummies(columns=cat_cols, drop_first=True)
64
+ return df_clean
65
+
66
+ def validate(self, df: pl.DataFrame) -> bool:
67
+ return True
68
+
69
+ def explain(self) -> str:
70
+ return (
71
+ "CategoricalEncoderPlugin detects text/string columns with fewer than 10 unique values "
72
+ "and applies One-Hot Encoding with drop_first=True to avoid multicollinearity."
73
+ )
@@ -0,0 +1,74 @@
1
+ import polars as pl
2
+ from datadoc.plugins.base import BasePlugin
3
+
4
+ class MissingValuePlugin(BasePlugin):
5
+ @property
6
+ def name(self) -> str:
7
+ return "MissingValuePlugin"
8
+
9
+ @property
10
+ def version(self) -> str:
11
+ return "0.1.0"
12
+
13
+ @property
14
+ def description(self) -> str:
15
+ return "Detects and imputes missing values using Median (numeric) and Mode (categorical)."
16
+
17
+ @property
18
+ def priority(self) -> int:
19
+ return 10 # Should run first
20
+
21
+ @property
22
+ def supported_datatypes(self) -> list:
23
+ return ["numeric", "categorical"]
24
+
25
+ def analyze(self, df: pl.DataFrame) -> dict:
26
+ cols_with_missing = {c: df[c].null_count() for c in df.columns if df[c].null_count() > 0}
27
+ total_missing = sum(cols_with_missing.values())
28
+ return {
29
+ "has_missing_values": bool(total_missing > 0),
30
+ "total_missing": int(total_missing),
31
+ "columns_affected": cols_with_missing,
32
+ }
33
+
34
+ def recommend(self, analysis_result: dict) -> list[str]:
35
+ recs = []
36
+ if analysis_result.get("has_missing_values"):
37
+ total = analysis_result["total_missing"]
38
+ cols = analysis_result.get("columns_affected", {})
39
+ col_detail = ", ".join([f"{c} ({v})" for c, v in cols.items()])
40
+ recs.append(
41
+ f"Found {total} missing values in: {col_detail}. "
42
+ f"Recommendation: Impute numeric with Median and categorical with Mode."
43
+ )
44
+ return recs
45
+
46
+ def generate_code(self, analysis_result: dict) -> str:
47
+ if not analysis_result.get("has_missing_values"):
48
+ return ""
49
+ return """# Missing Value Imputation
50
+ for col in df.columns:
51
+ if df[col].null_count() > 0:
52
+ if df[col].dtype.is_numeric():
53
+ df = df.with_columns(pl.col(col).fill_null(pl.col(col).median()))
54
+ else:
55
+ df = df.with_columns(pl.col(col).fill_null(pl.col(col).drop_nulls().mode().first()))"""
56
+
57
+ def apply(self, df: pl.DataFrame) -> pl.DataFrame:
58
+ df_clean = df.clone()
59
+ for col in df_clean.columns:
60
+ if df_clean[col].null_count() > 0:
61
+ if df_clean[col].dtype.is_numeric():
62
+ df_clean = df_clean.with_columns(pl.col(col).fill_null(pl.col(col).median()))
63
+ else:
64
+ df_clean = df_clean.with_columns(pl.col(col).fill_null(pl.col(col).drop_nulls().mode().first()))
65
+ return df_clean
66
+
67
+ def validate(self, df: pl.DataFrame) -> bool:
68
+ return bool(sum(df[c].null_count() for c in df.columns) == 0)
69
+
70
+ def explain(self) -> str:
71
+ return (
72
+ "MissingValuePlugin fills missing numeric values with the column median "
73
+ "and missing categorical values with the column mode (most frequent value)."
74
+ )
@@ -0,0 +1,93 @@
1
+ import polars as pl
2
+ from datadoc.plugins.base import BasePlugin
3
+
4
+ class OutlierPlugin(BasePlugin):
5
+ @property
6
+ def name(self) -> str:
7
+ return "OutlierPlugin"
8
+
9
+ @property
10
+ def version(self) -> str:
11
+ return "0.1.0"
12
+
13
+ @property
14
+ def description(self) -> str:
15
+ return "Detects outliers using IQR and clips extreme values at 5th/95th percentiles."
16
+
17
+ @property
18
+ def priority(self) -> int:
19
+ return 20 # After missing values
20
+
21
+ @property
22
+ def supported_datatypes(self) -> list:
23
+ return ["numeric"]
24
+
25
+ @property
26
+ def dependencies(self) -> list:
27
+ return ["MissingValuePlugin"]
28
+
29
+ def analyze(self, df: pl.DataFrame) -> dict:
30
+ outlier_info = {}
31
+ num_cols = [c for c in df.columns if df[c].dtype.is_numeric()]
32
+
33
+ for col in num_cols:
34
+ Q1 = df[col].quantile(0.25)
35
+ Q3 = df[col].quantile(0.75)
36
+ if Q1 is None or Q3 is None:
37
+ continue
38
+ IQR = Q3 - Q1
39
+ lower = Q1 - 1.5 * IQR
40
+ upper = Q3 + 1.5 * IQR
41
+
42
+ # Count outliers
43
+ count = df.select(((pl.col(col) < lower) | (pl.col(col) > upper)).sum())[col][0]
44
+ if count and count > 0:
45
+ outlier_info[col] = int(count)
46
+
47
+ return {
48
+ "has_outliers": len(outlier_info) > 0,
49
+ "outlier_columns": list(outlier_info.keys()),
50
+ "outlier_counts": outlier_info,
51
+ }
52
+
53
+ def recommend(self, analysis_result: dict) -> list[str]:
54
+ recs = []
55
+ if analysis_result.get("has_outliers"):
56
+ info = analysis_result.get("outlier_counts", {})
57
+ detail = ", ".join([f"{c} ({v} outliers)" for c, v in info.items()])
58
+ recs.append(
59
+ f"Outliers detected in: {detail}. "
60
+ f"Recommendation: Clip values at 5th and 95th percentiles."
61
+ )
62
+ return recs
63
+
64
+ def generate_code(self, analysis_result: dict) -> str:
65
+ if not analysis_result.get("has_outliers"):
66
+ return ""
67
+ cols_str = str(analysis_result.get("outlier_columns", []))
68
+ return f"""# Outlier Clipping
69
+ outlier_cols = {cols_str}
70
+ for col in outlier_cols:
71
+ lower = df[col].quantile(0.05)
72
+ upper = df[col].quantile(0.95)
73
+ if lower is not None and upper is not None:
74
+ df = df.with_columns(pl.col(col).clip(lower, upper))"""
75
+
76
+ def apply(self, df: pl.DataFrame) -> pl.DataFrame:
77
+ df_clean = df.clone()
78
+ outlier_cols = self.analyze(df_clean).get("outlier_columns", [])
79
+ for col in outlier_cols:
80
+ lower = df_clean[col].quantile(0.05)
81
+ upper = df_clean[col].quantile(0.95)
82
+ if lower is not None and upper is not None:
83
+ df_clean = df_clean.with_columns(pl.col(col).clip(lower, upper))
84
+ return df_clean
85
+
86
+ def validate(self, df: pl.DataFrame) -> bool:
87
+ return True
88
+
89
+ def explain(self) -> str:
90
+ return (
91
+ "OutlierPlugin detects statistical outliers using the IQR (Interquartile Range) "
92
+ "method and clips extreme values to the 5th and 95th percentile boundaries."
93
+ )
@@ -0,0 +1,103 @@
1
+ import polars as pl
2
+ from datadoc.plugins.base import BasePlugin
3
+
4
+ class ScalingPlugin(BasePlugin):
5
+ @property
6
+ def name(self) -> str:
7
+ return "ScalingPlugin"
8
+
9
+ @property
10
+ def version(self) -> str:
11
+ return "0.1.0"
12
+
13
+ @property
14
+ def description(self) -> str:
15
+ return "Detects numeric columns with vastly different scales and applies StandardScaler normalization."
16
+
17
+ @property
18
+ def priority(self) -> int:
19
+ return 45 # Near the end, after encoding
20
+
21
+ @property
22
+ def supported_datatypes(self) -> list:
23
+ return ["numeric"]
24
+
25
+ @property
26
+ def dependencies(self) -> list:
27
+ return ["MissingValuePlugin", "OutlierPlugin"]
28
+
29
+ def analyze(self, df: pl.DataFrame) -> dict:
30
+ num_cols = [c for c in df.columns if df[c].dtype.is_numeric()]
31
+ if len(num_cols) < 2:
32
+ return {"has_scale_issues": False, "columns_to_scale": []}
33
+
34
+ stds = {}
35
+ for col in num_cols:
36
+ std = df[col].std()
37
+ if std is not None:
38
+ stds[col] = std
39
+
40
+ if not stds:
41
+ return {"has_scale_issues": False, "columns_to_scale": []}
42
+
43
+ min_std = min(stds.values())
44
+ max_std = max(stds.values())
45
+
46
+ if min_std == 0:
47
+ cols_to_scale = [c for c, s in stds.items() if s > 0]
48
+ ratio = float('inf')
49
+ else:
50
+ ratio = max_std / min_std
51
+ cols_to_scale = num_cols if ratio > 10 else []
52
+
53
+ return {
54
+ "has_scale_issues": len(cols_to_scale) > 0,
55
+ "columns_to_scale": cols_to_scale,
56
+ "scale_ratio": round(ratio, 2) if ratio != float('inf') else ratio,
57
+ }
58
+
59
+ def recommend(self, analysis_result: dict) -> list[str]:
60
+ recs = []
61
+ if analysis_result.get("has_scale_issues"):
62
+ ratio = analysis_result.get("scale_ratio", 0)
63
+ cols = analysis_result["columns_to_scale"]
64
+ recs.append(
65
+ f"Numeric columns have a scale ratio of {ratio}x across {len(cols)} columns. "
66
+ f"Recommendation: Apply Standard Scaling (zero mean, unit variance)."
67
+ )
68
+ return recs
69
+
70
+ def generate_code(self, analysis_result: dict) -> str:
71
+ if not analysis_result.get("has_scale_issues"):
72
+ return ""
73
+ cols_str = str(analysis_result.get("columns_to_scale", []))
74
+ return f"""# Standard Scaling
75
+ scale_cols = {cols_str}
76
+ exprs = [((pl.col(c) - pl.col(c).mean()) / pl.col(c).std()).alias(c) for c in scale_cols]
77
+ if exprs:
78
+ df = df.with_columns(exprs)"""
79
+
80
+ def apply(self, df: pl.DataFrame) -> pl.DataFrame:
81
+ df_clean = df.clone()
82
+ cols_to_scale = self.analyze(df_clean).get("columns_to_scale", [])
83
+
84
+ exprs = []
85
+ for col in cols_to_scale:
86
+ exprs.append(
87
+ ((pl.col(col) - pl.col(col).mean()) / pl.col(col).std()).alias(col)
88
+ )
89
+
90
+ if exprs:
91
+ df_clean = df_clean.with_columns(exprs)
92
+
93
+ return df_clean
94
+
95
+ def validate(self, df: pl.DataFrame) -> bool:
96
+ return True
97
+
98
+ def explain(self) -> str:
99
+ return (
100
+ "ScalingPlugin detects when numeric columns have vastly different scales "
101
+ "(std ratio > 10x) and applies Standard Scaling: (value - mean) / std, "
102
+ "resulting in zero-mean, unit-variance features."
103
+ )