datapilot-polars 0.3.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.
- datapilot/__init__.py +119 -0
- datapilot/ai/__init__.py +3 -0
- datapilot/ai/base.py +57 -0
- datapilot/ai/factory.py +70 -0
- datapilot/ai/insights.py +46 -0
- datapilot/ai/providers/__init__.py +0 -0
- datapilot/ai/providers/claude_provider.py +45 -0
- datapilot/ai/providers/gemini_provider.py +46 -0
- datapilot/ai/providers/groq_provider.py +52 -0
- datapilot/ai/providers/ollama_provider.py +35 -0
- datapilot/ai/providers/openai_provider.py +43 -0
- datapilot/analysis/__init__.py +0 -0
- datapilot/analysis/auto_clean.py +131 -0
- datapilot/analysis/benchmark.py +137 -0
- datapilot/analysis/compare.py +129 -0
- datapilot/analysis/correlations.py +41 -0
- datapilot/analysis/duplicates.py +22 -0
- datapilot/analysis/eda.py +96 -0
- datapilot/analysis/missing.py +32 -0
- datapilot/analysis/outliers.py +102 -0
- datapilot/analysis/suggest.py +126 -0
- datapilot/analysis/summary.py +22 -0
- datapilot/dashboard/__init__.py +3 -0
- datapilot/dashboard/dashboard.py +326 -0
- datapilot/ml/__init__.py +6 -0
- datapilot/ml/classification.py +40 -0
- datapilot/ml/diagnostics.py +28 -0
- datapilot/ml/regression.py +67 -0
- datapilot/utils/__init__.py +0 -0
- datapilot/utils/validation.py +19 -0
- datapilot/visualization/__init__.py +5 -0
- datapilot/visualization/boxplot.py +23 -0
- datapilot/visualization/heatmap.py +26 -0
- datapilot/visualization/histogram.py +25 -0
- datapilot_polars-0.3.0.dist-info/METADATA +618 -0
- datapilot_polars-0.3.0.dist-info/RECORD +38 -0
- datapilot_polars-0.3.0.dist-info/WHEEL +5 -0
- datapilot_polars-0.3.0.dist-info/top_level.txt +1 -0
datapilot/__init__.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
2
|
+
|
|
3
|
+
# ── Analysis ──────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
def summary(df):
|
|
6
|
+
"""Calculates lightning fast structural overview metrics of the dataset."""
|
|
7
|
+
from .analysis.summary import summary as _summary
|
|
8
|
+
return _summary(df)
|
|
9
|
+
|
|
10
|
+
def missing(df):
|
|
11
|
+
"""Generates a report detailing missing count and percentage per column."""
|
|
12
|
+
from .analysis.missing import missing as _missing
|
|
13
|
+
return _missing(df)
|
|
14
|
+
|
|
15
|
+
def duplicates(df):
|
|
16
|
+
"""Calculates duplicate row counts and percentages using multi-threaded hashing."""
|
|
17
|
+
from .analysis.duplicates import duplicates as _duplicates
|
|
18
|
+
return _duplicates(df)
|
|
19
|
+
|
|
20
|
+
def correlation(df, threshold=0.6):
|
|
21
|
+
"""Calculates a high-speed Pearson correlation matrix and flags strong pairs."""
|
|
22
|
+
from .analysis.correlations import correlation as _correlation
|
|
23
|
+
return _correlation(df, threshold)
|
|
24
|
+
|
|
25
|
+
def analyze(df, use_ai=False, ai_provider="ollama", ai_model=None, api_key=None):
|
|
26
|
+
"""Runs automated EDA with optional AI insights from local or cloud providers.
|
|
27
|
+
|
|
28
|
+
ai_provider options: 'ollama' (local/default), 'openai', 'gemini', 'claude', 'groq'
|
|
29
|
+
"""
|
|
30
|
+
from .analysis.eda import analyze as _analyze
|
|
31
|
+
return _analyze(df, use_ai=use_ai, ai_provider=ai_provider,
|
|
32
|
+
ai_model=ai_model, api_key=api_key)
|
|
33
|
+
|
|
34
|
+
def suggest(df):
|
|
35
|
+
"""Analyses dataset structure and returns actionable preprocessing suggestions."""
|
|
36
|
+
from .analysis.suggest import suggest as _suggest
|
|
37
|
+
return _suggest(df)
|
|
38
|
+
|
|
39
|
+
def compare(df_train, df_test, threshold=0.1):
|
|
40
|
+
"""Detects distribution drift between a training and test/production DataFrame."""
|
|
41
|
+
from .analysis.compare import compare as _compare
|
|
42
|
+
return _compare(df_train, df_test, threshold)
|
|
43
|
+
|
|
44
|
+
def outliers(df, method="both", z_threshold=3.0, iqr_multiplier=1.5):
|
|
45
|
+
"""Detects outliers in all numeric columns using IQR and/or Z-score methods."""
|
|
46
|
+
from .analysis.outliers import outliers as _outliers
|
|
47
|
+
return _outliers(df, method=method, z_threshold=z_threshold, iqr_multiplier=iqr_multiplier)
|
|
48
|
+
|
|
49
|
+
def auto_clean(df, drop_null_threshold=0.6, impute_strategy="auto",
|
|
50
|
+
drop_id_columns=True, drop_constant_columns=True):
|
|
51
|
+
"""Automatically cleans a dataset — drops bad columns, imputes nulls."""
|
|
52
|
+
from .analysis.auto_clean import auto_clean as _auto_clean
|
|
53
|
+
return _auto_clean(
|
|
54
|
+
df,
|
|
55
|
+
drop_null_threshold=drop_null_threshold,
|
|
56
|
+
impute_strategy=impute_strategy,
|
|
57
|
+
drop_id_columns=drop_id_columns,
|
|
58
|
+
drop_constant_columns=drop_constant_columns,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def benchmark(df):
|
|
62
|
+
"""Benchmarks DataPilot (Polars) operations vs equivalent Pandas operations."""
|
|
63
|
+
from .analysis.benchmark import benchmark as _benchmark
|
|
64
|
+
return _benchmark(df)
|
|
65
|
+
|
|
66
|
+
# ── Visualization ─────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
def hist(df, column, bins=10):
|
|
69
|
+
"""Generates a clean distribution histogram for a selected numerical feature."""
|
|
70
|
+
from .visualization.histogram import hist as _hist
|
|
71
|
+
return _hist(df, column, bins)
|
|
72
|
+
|
|
73
|
+
def box(df, column):
|
|
74
|
+
"""Generates a clean box plot to showcase distribution quartiles and outliers."""
|
|
75
|
+
from .visualization.boxplot import box as _box
|
|
76
|
+
return _box(df, column)
|
|
77
|
+
|
|
78
|
+
def heatmap(df):
|
|
79
|
+
"""Generates a visual correlation matrix heatmap for all numerical features."""
|
|
80
|
+
from .visualization.heatmap import heatmap as _heatmap
|
|
81
|
+
return _heatmap(df)
|
|
82
|
+
|
|
83
|
+
# ── Machine Learning ──────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def classification_report(y_true, y_pred, average="auto"):
|
|
86
|
+
"""Calculates key classification performance metrics (binary or multi-class)."""
|
|
87
|
+
from .ml.classification import classification_report as _cr
|
|
88
|
+
return _cr(y_true, y_pred, average=average)
|
|
89
|
+
|
|
90
|
+
def regression_report(y_true, y_pred):
|
|
91
|
+
"""Calculates comprehensive regression evaluation metrics (MAE, RMSE, R², MAPE)."""
|
|
92
|
+
from .ml.regression import regression_report as _rr
|
|
93
|
+
return _rr(y_true, y_pred)
|
|
94
|
+
|
|
95
|
+
def diagnose(train_score, test_score, metric_name="Accuracy"):
|
|
96
|
+
"""Diagnoses model health, highlighting overfitting, underfitting, or optimal states."""
|
|
97
|
+
from .ml.diagnostics import diagnose as _diagnose
|
|
98
|
+
return _diagnose(train_score, test_score, metric_name)
|
|
99
|
+
|
|
100
|
+
# ── Dashboard ─────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def dashboard(df, output_path="datapilot_report.html"):
|
|
103
|
+
"""Generates a standalone, beautifully styled HTML dashboard report from the dataset."""
|
|
104
|
+
from .dashboard.dashboard import dashboard as _dashboard
|
|
105
|
+
return _dashboard(df, output_path)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
__all__ = [
|
|
109
|
+
"__version__",
|
|
110
|
+
# Analysis
|
|
111
|
+
"summary", "missing", "duplicates", "correlation", "analyze",
|
|
112
|
+
"suggest", "compare", "outliers", "auto_clean", "benchmark",
|
|
113
|
+
# Visualization
|
|
114
|
+
"hist", "box", "heatmap",
|
|
115
|
+
# ML
|
|
116
|
+
"classification_report", "regression_report", "diagnose",
|
|
117
|
+
# Dashboard
|
|
118
|
+
"dashboard",
|
|
119
|
+
]
|
datapilot/ai/__init__.py
ADDED
datapilot/ai/base.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseAIProvider(ABC):
|
|
6
|
+
"""Abstract base class for all DataPilot AI provider backends.
|
|
7
|
+
|
|
8
|
+
Every provider must implement a single `generate` method that accepts
|
|
9
|
+
the dataset metadata and returns a plain-text insight string.
|
|
10
|
+
Raw data rows are NEVER passed to any provider — only lightweight
|
|
11
|
+
statistical summaries are transmitted (metadata-only pattern).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, model: str, api_key: str = None, **kwargs):
|
|
15
|
+
self.model = model
|
|
16
|
+
self.api_key = api_key
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def generate(
|
|
20
|
+
self,
|
|
21
|
+
meta: Dict[str, Any],
|
|
22
|
+
missing_list: List[Dict[str, Any]],
|
|
23
|
+
strong_relations: List[str],
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Generate AI insights from dataset metadata.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
meta: Structural stats dict (rows, columns, dtypes, null counts).
|
|
29
|
+
missing_list: List of dicts with column null stats.
|
|
30
|
+
strong_relations: List of strong correlation pair strings.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A plain-text string with actionable recommendations.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def _build_system_prompt(self) -> str:
|
|
37
|
+
return (
|
|
38
|
+
"You are DataPilot, an elite data science assistant. "
|
|
39
|
+
"Analyse the structural statistics provided and write exactly 3 brief, "
|
|
40
|
+
"impactful bullet points giving expert data preprocessing, cleaning, or "
|
|
41
|
+
"modelling advice. Be direct and conversational. "
|
|
42
|
+
"Do not include introductory text or markdown headers."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _build_user_prompt(
|
|
46
|
+
self,
|
|
47
|
+
meta: Dict[str, Any],
|
|
48
|
+
missing_list: List[Dict[str, Any]],
|
|
49
|
+
strong_relations: List[str],
|
|
50
|
+
) -> str:
|
|
51
|
+
return (
|
|
52
|
+
f"Dataset Profile:\n"
|
|
53
|
+
f"- Dimensions: {meta['rows']} rows × {meta['columns']} columns\n"
|
|
54
|
+
f"- Duplicate Rows: {meta.get('duplicates_count', 'N/A')}\n"
|
|
55
|
+
f"- Missing Data: {missing_list}\n"
|
|
56
|
+
f"- Strong Correlations: {strong_relations}\n"
|
|
57
|
+
)
|
datapilot/ai/factory.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from .base import BaseAIProvider
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Registry mapping provider name → class (lazy import avoids hard dependencies)
|
|
6
|
+
_PROVIDER_REGISTRY = {
|
|
7
|
+
"ollama": "datapilot.ai.providers.ollama_provider.OllamaProvider",
|
|
8
|
+
"openai": "datapilot.ai.providers.openai_provider.OpenAIProvider",
|
|
9
|
+
"gemini": "datapilot.ai.providers.gemini_provider.GeminiProvider",
|
|
10
|
+
"claude": "datapilot.ai.providers.claude_provider.ClaudeProvider",
|
|
11
|
+
"groq": "datapilot.ai.providers.groq_provider.GroqProvider",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Human-readable default models per provider
|
|
15
|
+
DEFAULT_MODELS = {
|
|
16
|
+
"ollama": "llama3",
|
|
17
|
+
"openai": "gpt-4o-mini",
|
|
18
|
+
"gemini": "gemini-1.5-flash",
|
|
19
|
+
"claude": "claude-3-haiku-20240307",
|
|
20
|
+
"groq": "llama3-70b-8192",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_provider(
|
|
25
|
+
ai_provider: str,
|
|
26
|
+
ai_model: Optional[str] = None,
|
|
27
|
+
api_key: Optional[str] = None,
|
|
28
|
+
) -> BaseAIProvider:
|
|
29
|
+
"""Resolves and instantiates the correct AI provider from a string name.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
ai_provider: Name of the AI backend. One of:
|
|
33
|
+
'ollama' (local, default), 'openai', 'gemini', 'claude', 'groq'.
|
|
34
|
+
ai_model: Model name within the provider. If None, uses the provider's
|
|
35
|
+
sensible default (e.g. 'gpt-4o-mini' for OpenAI).
|
|
36
|
+
api_key: API key required for cloud providers. Not needed for Ollama.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
An instantiated BaseAIProvider subclass ready to call .generate().
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If the provider name is not recognised.
|
|
43
|
+
"""
|
|
44
|
+
provider_name = ai_provider.lower().strip()
|
|
45
|
+
|
|
46
|
+
if provider_name not in _PROVIDER_REGISTRY:
|
|
47
|
+
supported = ", ".join(f"'{k}'" for k in _PROVIDER_REGISTRY)
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Unknown AI provider: '{ai_provider}'. "
|
|
50
|
+
f"Supported providers: {supported}."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Validate API key requirement for cloud providers
|
|
54
|
+
if provider_name != "ollama" and not api_key:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"'{ai_provider}' is a cloud provider and requires an API key. "
|
|
57
|
+
f"Pass it via: dp.analyze(df, use_ai=True, ai_provider='{ai_provider}', "
|
|
58
|
+
f"api_key='your-key-here')"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Resolve default model
|
|
62
|
+
model = ai_model or DEFAULT_MODELS[provider_name]
|
|
63
|
+
|
|
64
|
+
# Lazy import of the provider class
|
|
65
|
+
module_path, class_name = _PROVIDER_REGISTRY[provider_name].rsplit(".", 1)
|
|
66
|
+
import importlib
|
|
67
|
+
module = importlib.import_module(module_path)
|
|
68
|
+
provider_class = getattr(module, class_name)
|
|
69
|
+
|
|
70
|
+
return provider_class(model=model, api_key=api_key)
|
datapilot/ai/insights.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from .factory import get_provider
|
|
2
|
+
from typing import Dict, Any, List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def generate_insights(
|
|
6
|
+
meta: Dict[str, Any],
|
|
7
|
+
missing_list: List[Dict[str, Any]],
|
|
8
|
+
strong_relations: List[str],
|
|
9
|
+
model_name: str = "llama3",
|
|
10
|
+
ai_provider: str = "ollama",
|
|
11
|
+
api_key: Optional[str] = None,
|
|
12
|
+
) -> str:
|
|
13
|
+
"""Sends lightweight dataset metadata to the configured AI provider and
|
|
14
|
+
returns conversational, context-aware engineering recommendations.
|
|
15
|
+
|
|
16
|
+
DataPilot uses a Metadata-Only AI Pattern — raw data rows are NEVER
|
|
17
|
+
transmitted to any provider. Only concise statistical summaries are sent.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
meta: Structural stats dict from dp.summary().
|
|
21
|
+
missing_list: List of column null-stat dicts from dp.missing().
|
|
22
|
+
strong_relations: List of correlation pair strings from dp.correlation().
|
|
23
|
+
model_name: Model to use within the chosen provider.
|
|
24
|
+
Defaults vary per provider if not specified.
|
|
25
|
+
ai_provider: AI backend to use. Options:
|
|
26
|
+
'ollama' — local, free, private (default)
|
|
27
|
+
'openai' — GPT-4o, GPT-4, GPT-3.5
|
|
28
|
+
'gemini' — Google Gemini 1.5 Pro/Flash
|
|
29
|
+
'claude' — Anthropic Claude 3.5 Sonnet/Haiku
|
|
30
|
+
'groq' — Ultra-fast free-tier (Llama3, Mixtral)
|
|
31
|
+
api_key: API key for cloud providers. Not needed for Ollama.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Plain-text string with 3 actionable data science recommendations.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
provider = get_provider(
|
|
38
|
+
ai_provider=ai_provider,
|
|
39
|
+
ai_model=model_name,
|
|
40
|
+
api_key=api_key,
|
|
41
|
+
)
|
|
42
|
+
return provider.generate(meta, missing_list, strong_relations)
|
|
43
|
+
except ValueError as e:
|
|
44
|
+
return f"⚠️ Configuration error: {e}"
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return f"⚠️ Could not generate AI Insights. Error: {e}"
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from ..base import BaseAIProvider
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ClaudeProvider(BaseAIProvider):
|
|
6
|
+
"""Anthropic Claude cloud AI provider (claude-3-5-sonnet, claude-3-haiku, etc.)
|
|
7
|
+
|
|
8
|
+
Requires the `anthropic` package: `pip install anthropic`
|
|
9
|
+
Only dataset metadata (stats, null counts) is sent — never raw data rows.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
dp.analyze(df, use_ai=True, ai_provider="claude",
|
|
13
|
+
ai_model="claude-3-5-sonnet-20241022", api_key="sk-ant-...")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def generate(
|
|
17
|
+
self,
|
|
18
|
+
meta: Dict[str, Any],
|
|
19
|
+
missing_list: List[Dict[str, Any]],
|
|
20
|
+
strong_relations: List[str],
|
|
21
|
+
) -> str:
|
|
22
|
+
try:
|
|
23
|
+
import anthropic
|
|
24
|
+
except ImportError:
|
|
25
|
+
return (
|
|
26
|
+
"⚠️ Anthropic package not installed. "
|
|
27
|
+
"Run: pip install anthropic"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
client = anthropic.Anthropic(api_key=self.api_key)
|
|
32
|
+
response = client.messages.create(
|
|
33
|
+
model=self.model,
|
|
34
|
+
max_tokens=300,
|
|
35
|
+
system=self._build_system_prompt(),
|
|
36
|
+
messages=[
|
|
37
|
+
{
|
|
38
|
+
"role": "user",
|
|
39
|
+
"content": self._build_user_prompt(meta, missing_list, strong_relations),
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
return response.content[0].text.strip()
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return f"⚠️ Anthropic Claude API error: {e}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from ..base import BaseAIProvider
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GeminiProvider(BaseAIProvider):
|
|
6
|
+
"""Google Gemini cloud AI provider (gemini-1.5-pro, gemini-1.5-flash, etc.)
|
|
7
|
+
|
|
8
|
+
Requires the `google-generativeai` package: `pip install google-generativeai`
|
|
9
|
+
Only dataset metadata (stats, null counts) is sent — never raw data rows.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
dp.analyze(df, use_ai=True, ai_provider="gemini",
|
|
13
|
+
ai_model="gemini-1.5-flash", api_key="AIza...")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def generate(
|
|
17
|
+
self,
|
|
18
|
+
meta: Dict[str, Any],
|
|
19
|
+
missing_list: List[Dict[str, Any]],
|
|
20
|
+
strong_relations: List[str],
|
|
21
|
+
) -> str:
|
|
22
|
+
try:
|
|
23
|
+
import google.generativeai as genai
|
|
24
|
+
except ImportError:
|
|
25
|
+
return (
|
|
26
|
+
"⚠️ Google Generative AI package not installed. "
|
|
27
|
+
"Run: pip install google-generativeai"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
genai.configure(api_key=self.api_key)
|
|
32
|
+
model = genai.GenerativeModel(
|
|
33
|
+
model_name=self.model,
|
|
34
|
+
system_instruction=self._build_system_prompt(),
|
|
35
|
+
)
|
|
36
|
+
user_prompt = self._build_user_prompt(meta, missing_list, strong_relations)
|
|
37
|
+
response = model.generate_content(
|
|
38
|
+
user_prompt,
|
|
39
|
+
generation_config=genai.GenerationConfig(
|
|
40
|
+
max_output_tokens=300,
|
|
41
|
+
temperature=0.4,
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
return response.text.strip()
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return f"⚠️ Google Gemini API error: {e}"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from ..base import BaseAIProvider
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GroqProvider(BaseAIProvider):
|
|
6
|
+
"""Groq cloud AI provider — ultra-fast inference (llama3-70b, mixtral, gemma2, etc.)
|
|
7
|
+
|
|
8
|
+
Groq offers a generous free tier and is significantly faster than OpenAI
|
|
9
|
+
for real-time use cases. Uses an OpenAI-compatible API.
|
|
10
|
+
|
|
11
|
+
Requires the `groq` package: `pip install groq`
|
|
12
|
+
Only dataset metadata (stats, null counts) is sent — never raw data rows.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
dp.analyze(df, use_ai=True, ai_provider="groq",
|
|
16
|
+
ai_model="llama3-70b-8192", api_key="gsk_...")
|
|
17
|
+
|
|
18
|
+
Popular free models:
|
|
19
|
+
- llama3-70b-8192 (recommended — best quality)
|
|
20
|
+
- llama3-8b-8192 (fastest)
|
|
21
|
+
- mixtral-8x7b-32768 (long context)
|
|
22
|
+
- gemma2-9b-it (Google's Gemma via Groq)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def generate(
|
|
26
|
+
self,
|
|
27
|
+
meta: Dict[str, Any],
|
|
28
|
+
missing_list: List[Dict[str, Any]],
|
|
29
|
+
strong_relations: List[str],
|
|
30
|
+
) -> str:
|
|
31
|
+
try:
|
|
32
|
+
from groq import Groq
|
|
33
|
+
except ImportError:
|
|
34
|
+
return (
|
|
35
|
+
"⚠️ Groq package not installed. "
|
|
36
|
+
"Run: pip install groq"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
client = Groq(api_key=self.api_key)
|
|
41
|
+
response = client.chat.completions.create(
|
|
42
|
+
model=self.model,
|
|
43
|
+
messages=[
|
|
44
|
+
{"role": "system", "content": self._build_system_prompt()},
|
|
45
|
+
{"role": "user", "content": self._build_user_prompt(meta, missing_list, strong_relations)},
|
|
46
|
+
],
|
|
47
|
+
max_tokens=300,
|
|
48
|
+
temperature=0.4,
|
|
49
|
+
)
|
|
50
|
+
return response.choices[0].message.content.strip()
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return f"⚠️ Groq API error: {e}"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from ..base import BaseAIProvider
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class OllamaProvider(BaseAIProvider):
|
|
6
|
+
"""Local Ollama AI provider — fully private, no internet required.
|
|
7
|
+
|
|
8
|
+
Communicates with a locally running Ollama daemon. Raw data rows
|
|
9
|
+
are never transmitted; only metadata summaries are sent.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
dp.analyze(df, use_ai=True, ai_provider="ollama", ai_model="llama3")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def generate(
|
|
16
|
+
self,
|
|
17
|
+
meta: Dict[str, Any],
|
|
18
|
+
missing_list: List[Dict[str, Any]],
|
|
19
|
+
strong_relations: List[str],
|
|
20
|
+
) -> str:
|
|
21
|
+
try:
|
|
22
|
+
import ollama
|
|
23
|
+
response = ollama.chat(
|
|
24
|
+
model=self.model,
|
|
25
|
+
messages=[
|
|
26
|
+
{"role": "system", "content": self._build_system_prompt()},
|
|
27
|
+
{"role": "user", "content": self._build_user_prompt(meta, missing_list, strong_relations)},
|
|
28
|
+
],
|
|
29
|
+
)
|
|
30
|
+
return response["message"]["content"].strip()
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return (
|
|
33
|
+
f"⚠️ Could not reach Ollama. Ensure it is running (`ollama serve`). "
|
|
34
|
+
f"Error: {e}"
|
|
35
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from ..base import BaseAIProvider
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class OpenAIProvider(BaseAIProvider):
|
|
6
|
+
"""OpenAI cloud AI provider (GPT-4o, GPT-4, GPT-3.5-turbo, etc.)
|
|
7
|
+
|
|
8
|
+
Requires the `openai` package: `pip install openai`
|
|
9
|
+
Only dataset metadata (stats, null counts) is sent — never raw data rows.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
dp.analyze(df, use_ai=True, ai_provider="openai",
|
|
13
|
+
ai_model="gpt-4o", api_key="sk-...")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def generate(
|
|
17
|
+
self,
|
|
18
|
+
meta: Dict[str, Any],
|
|
19
|
+
missing_list: List[Dict[str, Any]],
|
|
20
|
+
strong_relations: List[str],
|
|
21
|
+
) -> str:
|
|
22
|
+
try:
|
|
23
|
+
from openai import OpenAI
|
|
24
|
+
except ImportError:
|
|
25
|
+
return (
|
|
26
|
+
"⚠️ OpenAI package not installed. "
|
|
27
|
+
"Run: pip install openai"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
client = OpenAI(api_key=self.api_key)
|
|
32
|
+
response = client.chat.completions.create(
|
|
33
|
+
model=self.model,
|
|
34
|
+
messages=[
|
|
35
|
+
{"role": "system", "content": self._build_system_prompt()},
|
|
36
|
+
{"role": "user", "content": self._build_user_prompt(meta, missing_list, strong_relations)},
|
|
37
|
+
],
|
|
38
|
+
max_tokens=300,
|
|
39
|
+
temperature=0.4,
|
|
40
|
+
)
|
|
41
|
+
return response.choices[0].message.content.strip()
|
|
42
|
+
except Exception as e:
|
|
43
|
+
return f"⚠️ OpenAI API error: {e}"
|
|
File without changes
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from ..utils.validation import ensure_polars
|
|
2
|
+
import polars as pl
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from typing import Union, Tuple, Dict, Any, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def auto_clean(
|
|
8
|
+
df: Union[pd.DataFrame, pl.DataFrame],
|
|
9
|
+
drop_null_threshold: float = 0.6,
|
|
10
|
+
impute_strategy: str = "auto",
|
|
11
|
+
drop_id_columns: bool = True,
|
|
12
|
+
drop_constant_columns: bool = True,
|
|
13
|
+
) -> Tuple[Union[pd.DataFrame, pl.DataFrame], List[Dict[str, Any]]]:
|
|
14
|
+
"""Automatically cleans a dataset by applying common preprocessing steps.
|
|
15
|
+
|
|
16
|
+
Performs the following operations (in order):
|
|
17
|
+
1. Drops constant columns (zero variance)
|
|
18
|
+
2. Drops ID-like columns (100% unique values)
|
|
19
|
+
3. Drops columns exceeding the null threshold
|
|
20
|
+
4. Imputes remaining numeric nulls with median
|
|
21
|
+
5. Imputes remaining categorical/string nulls with mode
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
df: Input DataFrame (Pandas or Polars).
|
|
25
|
+
drop_null_threshold: Columns with null % above this are dropped (default: 0.60 = 60%).
|
|
26
|
+
impute_strategy: 'auto' (median for numeric, mode for categorical), 'median', or 'mode'.
|
|
27
|
+
drop_id_columns: Whether to drop columns where all values are unique (default: True).
|
|
28
|
+
drop_constant_columns: Whether to drop columns with a single unique value (default: True).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (cleaned_df, change_log) where change_log is a list of dicts
|
|
32
|
+
describing every action taken. The returned DataFrame matches the input type.
|
|
33
|
+
"""
|
|
34
|
+
local_df, original_engine = ensure_polars(df)
|
|
35
|
+
total_rows = local_df.height
|
|
36
|
+
change_log: List[Dict[str, Any]] = []
|
|
37
|
+
cols_to_drop: List[str] = []
|
|
38
|
+
|
|
39
|
+
print("=" * 58)
|
|
40
|
+
print(" DATAPILOT AUTO-CLEAN ENGINE ")
|
|
41
|
+
print("=" * 58)
|
|
42
|
+
print(f" Input: {total_rows:,} rows × {local_df.width} columns")
|
|
43
|
+
|
|
44
|
+
null_counts = local_df.null_count()
|
|
45
|
+
|
|
46
|
+
for col in local_df.columns:
|
|
47
|
+
series = local_df[col]
|
|
48
|
+
n_unique = series.n_unique()
|
|
49
|
+
null_ct = null_counts[col][0]
|
|
50
|
+
null_pct = null_ct / total_rows if total_rows > 0 else 0
|
|
51
|
+
|
|
52
|
+
# ── 1. Constant columns ──────────────────────────────────────────────
|
|
53
|
+
if drop_constant_columns and n_unique == 1:
|
|
54
|
+
cols_to_drop.append(col)
|
|
55
|
+
change_log.append({
|
|
56
|
+
"action": "DROPPED",
|
|
57
|
+
"column": col,
|
|
58
|
+
"reason": "Constant column — all values are identical (zero variance)",
|
|
59
|
+
})
|
|
60
|
+
print(f"\n 🗑️ DROP '{col}' → constant column (1 unique value)")
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# ── 2. ID-like columns ───────────────────────────────────────────────
|
|
64
|
+
if drop_id_columns and n_unique == total_rows and total_rows > 10:
|
|
65
|
+
cols_to_drop.append(col)
|
|
66
|
+
change_log.append({
|
|
67
|
+
"action": "DROPPED",
|
|
68
|
+
"column": col,
|
|
69
|
+
"reason": "ID/key column — 100% unique values carry no predictive signal",
|
|
70
|
+
})
|
|
71
|
+
print(f"\n 🗑️ DROP '{col}' → ID-like column (all {n_unique} values unique)")
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# ── 3. High-null columns ──────────────────────────────────────────────
|
|
75
|
+
if null_pct >= drop_null_threshold:
|
|
76
|
+
cols_to_drop.append(col)
|
|
77
|
+
change_log.append({
|
|
78
|
+
"action": "DROPPED",
|
|
79
|
+
"column": col,
|
|
80
|
+
"reason": f"High null rate ({null_pct*100:.1f}%) exceeds threshold ({drop_null_threshold*100:.0f}%)",
|
|
81
|
+
})
|
|
82
|
+
print(f"\n 🗑️ DROP '{col}' → {null_pct*100:.1f}% nulls (above {drop_null_threshold*100:.0f}% threshold)")
|
|
83
|
+
|
|
84
|
+
# Apply drops
|
|
85
|
+
clean_df = local_df.drop(cols_to_drop) if cols_to_drop else local_df.clone()
|
|
86
|
+
|
|
87
|
+
# ── 4 & 5. Impute remaining nulls ─────────────────────────────────────────
|
|
88
|
+
impute_exprs = []
|
|
89
|
+
for col in clean_df.columns:
|
|
90
|
+
dtype = clean_df[col].dtype
|
|
91
|
+
null_ct = clean_df[col].null_count()
|
|
92
|
+
if null_ct == 0:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if dtype.is_numeric():
|
|
96
|
+
if impute_strategy in ("auto", "median"):
|
|
97
|
+
median_val = clean_df[col].median()
|
|
98
|
+
if median_val is not None:
|
|
99
|
+
impute_exprs.append(pl.col(col).fill_null(median_val))
|
|
100
|
+
change_log.append({
|
|
101
|
+
"action": "IMPUTED",
|
|
102
|
+
"column": col,
|
|
103
|
+
"reason": f"Filled {null_ct} null(s) with median ({round(median_val, 4)})",
|
|
104
|
+
})
|
|
105
|
+
print(f"\n 🔧 IMPUTE '{col}' → filled {null_ct} null(s) with median={round(median_val, 4)}")
|
|
106
|
+
else:
|
|
107
|
+
if impute_strategy in ("auto", "mode"):
|
|
108
|
+
mode_series = clean_df[col].drop_nulls().value_counts().sort("count", descending=True)
|
|
109
|
+
if mode_series.height > 0:
|
|
110
|
+
mode_val = str(mode_series["value"][0])
|
|
111
|
+
impute_exprs.append(pl.col(col).fill_null(mode_val))
|
|
112
|
+
change_log.append({
|
|
113
|
+
"action": "IMPUTED",
|
|
114
|
+
"column": col,
|
|
115
|
+
"reason": f"Filled {null_ct} null(s) with mode ('{mode_val}')",
|
|
116
|
+
})
|
|
117
|
+
print(f"\n 🔧 IMPUTE '{col}' → filled {null_ct} null(s) with mode='{mode_val}'")
|
|
118
|
+
|
|
119
|
+
if impute_exprs:
|
|
120
|
+
clean_df = clean_df.with_columns(impute_exprs)
|
|
121
|
+
|
|
122
|
+
print("\n" + "-" * 58)
|
|
123
|
+
print(f" Output: {clean_df.height:,} rows × {clean_df.width} columns")
|
|
124
|
+
dropped = local_df.width - clean_df.width
|
|
125
|
+
imputed = sum(1 for c in change_log if c["action"] == "IMPUTED")
|
|
126
|
+
print(f" ✅ {dropped} column(s) dropped | {imputed} column(s) imputed")
|
|
127
|
+
print("=" * 58)
|
|
128
|
+
|
|
129
|
+
# Return in original format
|
|
130
|
+
result_df = clean_df.to_pandas() if original_engine == "pandas" else clean_df
|
|
131
|
+
return result_df, change_log
|