lazyanalyst 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.
- lazyanalyst/__init__.py +1 -0
- lazyanalyst/analyze.py +204 -0
- lazyanalyst/cleaner.py +89 -0
- lazyanalyst/dashboard.py +233 -0
- lazyanalyst/eda.py +72 -0
- lazyanalyst/features.py +67 -0
- lazyanalyst/insights.py +72 -0
- lazyanalyst/loader.py +49 -0
- lazyanalyst/quality.py +110 -0
- lazyanalyst/reporter.py +301 -0
- lazyanalyst/schema.py +76 -0
- lazyanalyst/stats.py +150 -0
- lazyanalyst/visualizer.py +175 -0
- lazyanalyst-1.0.0.dist-info/METADATA +224 -0
- lazyanalyst-1.0.0.dist-info/RECORD +18 -0
- lazyanalyst-1.0.0.dist-info/WHEEL +5 -0
- lazyanalyst-1.0.0.dist-info/licenses/LICENSE +21 -0
- lazyanalyst-1.0.0.dist-info/top_level.txt +1 -0
lazyanalyst/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .analyze import analyze, LazyAnalystResult
|
lazyanalyst/analyze.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from . import loader
|
|
3
|
+
from . import schema
|
|
4
|
+
from . import quality
|
|
5
|
+
from . import cleaner
|
|
6
|
+
from . import eda
|
|
7
|
+
from . import visualizer
|
|
8
|
+
from . import features
|
|
9
|
+
from . import stats
|
|
10
|
+
from . import insights
|
|
11
|
+
from . import dashboard
|
|
12
|
+
from . import reporter
|
|
13
|
+
|
|
14
|
+
class LazyAnalystResult:
|
|
15
|
+
"""result object returned by analyze"""
|
|
16
|
+
def __init__(self, cleaned_data_path, report_path, dashboard_path, cleaned_df):
|
|
17
|
+
self.cleaned_data_path = cleaned_data_path
|
|
18
|
+
self.report_path = report_path
|
|
19
|
+
self.dashboard_path = dashboard_path
|
|
20
|
+
self._cleaned_df = cleaned_df
|
|
21
|
+
|
|
22
|
+
def dashboard(self):
|
|
23
|
+
"""opens dashboard.html in browser"""
|
|
24
|
+
import webbrowser
|
|
25
|
+
webbrowser.open(self.dashboard_path)
|
|
26
|
+
|
|
27
|
+
def report(self):
|
|
28
|
+
"""opens report.html in browser"""
|
|
29
|
+
import webbrowser
|
|
30
|
+
webbrowser.open(self.report_path)
|
|
31
|
+
|
|
32
|
+
def cleaned_data(self):
|
|
33
|
+
"""returns cleaned pandas DataFrame"""
|
|
34
|
+
return self._cleaned_df
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def analyze(filepath, dashboard=True, report=True):
|
|
38
|
+
"""runs full analysis pipeline"""
|
|
39
|
+
|
|
40
|
+
# make sure outputs folder exists
|
|
41
|
+
if not os.path.exists("outputs"):
|
|
42
|
+
os.makedirs("outputs")
|
|
43
|
+
print("[LazyAnalyst] Created outputs/ folder")
|
|
44
|
+
|
|
45
|
+
# store all results in a dict
|
|
46
|
+
all_results = {
|
|
47
|
+
"filename": os.path.basename(filepath),
|
|
48
|
+
"cleaned_df": None,
|
|
49
|
+
"schema": None,
|
|
50
|
+
"quality_report": None,
|
|
51
|
+
"cleaning_actions": [],
|
|
52
|
+
"eda_results": {},
|
|
53
|
+
"features_created": [],
|
|
54
|
+
"stats_results": [],
|
|
55
|
+
"insights": []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Module 1 - Loader
|
|
59
|
+
print("\n[LazyAnalyst] Step 1/11 — Loading dataset...")
|
|
60
|
+
try:
|
|
61
|
+
df = loader.load(filepath)
|
|
62
|
+
if df.empty:
|
|
63
|
+
raise ValueError("Loaded dataframe is empty")
|
|
64
|
+
all_results["original_df"] = df
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"[LazyAnalyst] Warning: loader failed — {e}. Skipping this step.")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
# Module 2 - Schema
|
|
70
|
+
print("\n[LazyAnalyst] Step 2/11 — Detecting schema...")
|
|
71
|
+
try:
|
|
72
|
+
schema_dict = schema.detect(df)
|
|
73
|
+
all_results["schema"] = schema_dict
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"[LazyAnalyst] Warning: schema detection failed — {e}. Skipping this step.")
|
|
76
|
+
schema_dict = {}
|
|
77
|
+
all_results["schema"] = {}
|
|
78
|
+
|
|
79
|
+
# Module 3 - Quality
|
|
80
|
+
print("\n[LazyAnalyst] Step 3/11 — Auditing data quality...")
|
|
81
|
+
try:
|
|
82
|
+
quality_report = quality.audit(df, schema_dict)
|
|
83
|
+
all_results["quality_report"] = quality_report
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(f"[LazyAnalyst] Warning: quality audit failed — {e}. Skipping this step.")
|
|
86
|
+
quality_report = {"score": 0, "missing": {}, "duplicates": {"count":0,"pct":0}, "outliers": {}, "invalid": []}
|
|
87
|
+
all_results["quality_report"] = quality_report
|
|
88
|
+
|
|
89
|
+
# Module 4 - Cleaner
|
|
90
|
+
print("\n[LazyAnalyst] Step 4/11 — Cleaning dataset...")
|
|
91
|
+
try:
|
|
92
|
+
cleaned_df, actions = cleaner.clean(df, schema_dict, quality_report)
|
|
93
|
+
all_results["cleaned_df"] = cleaned_df
|
|
94
|
+
all_results["cleaning_actions"] = actions
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"[LazyAnalyst] Warning: cleaner failed — {e}. Skipping this step.")
|
|
97
|
+
cleaned_df = df.copy()
|
|
98
|
+
all_results["cleaned_df"] = cleaned_df
|
|
99
|
+
all_results["cleaning_actions"] = []
|
|
100
|
+
|
|
101
|
+
# Module 5 - EDA
|
|
102
|
+
print("\n[LazyAnalyst] Step 5/11 — Running exploratory analysis...")
|
|
103
|
+
try:
|
|
104
|
+
eda_results = eda.run(cleaned_df, schema_dict)
|
|
105
|
+
all_results["eda_results"] = eda_results
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"[LazyAnalyst] Warning: EDA failed — {e}. Skipping this step.")
|
|
108
|
+
all_results["eda_results"] = {"numerical": {}, "categorical": {}, "correlation": {}}
|
|
109
|
+
|
|
110
|
+
# Module 6 - Visualizer
|
|
111
|
+
print("\n[LazyAnalyst] Step 6/11 — Generating visualizations...")
|
|
112
|
+
try:
|
|
113
|
+
visualizer.generate(cleaned_df, schema_dict)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"[LazyAnalyst] Warning: visualizer failed — {e}. Skipping this step.")
|
|
116
|
+
|
|
117
|
+
# Module 7 - Features
|
|
118
|
+
print("\n[LazyAnalyst] Step 7/11 — Engineering features...")
|
|
119
|
+
try:
|
|
120
|
+
df_with_features, feat_list = features.engineer(cleaned_df, schema_dict)
|
|
121
|
+
all_results["cleaned_df"] = df_with_features # update df with new features
|
|
122
|
+
all_results["features_created"] = feat_list
|
|
123
|
+
# update schema for new columns? not necessary for v1
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"[LazyAnalyst] Warning: feature engineering failed — {e}. Skipping this step.")
|
|
126
|
+
all_results["features_created"] = []
|
|
127
|
+
|
|
128
|
+
# Module 8 - Stats
|
|
129
|
+
print("\n[LazyAnalyst] Step 8/11 — Running statistical tests...")
|
|
130
|
+
try:
|
|
131
|
+
stats_results = stats.run(all_results["cleaned_df"], schema_dict)
|
|
132
|
+
all_results["stats_results"] = stats_results
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"[LazyAnalyst] Warning: stats failed — {e}. Skipping this step.")
|
|
135
|
+
all_results["stats_results"] = []
|
|
136
|
+
|
|
137
|
+
# Module 9 - Insights
|
|
138
|
+
print("\n[LazyAnalyst] Step 9/11 — Generating insights...")
|
|
139
|
+
try:
|
|
140
|
+
insight_list = insights.generate(
|
|
141
|
+
all_results["eda_results"],
|
|
142
|
+
all_results["stats_results"],
|
|
143
|
+
all_results["quality_report"]
|
|
144
|
+
)
|
|
145
|
+
all_results["insights"] = insight_list
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(f"[LazyAnalyst] Warning: insights generation failed — {e}. Skipping this step.")
|
|
148
|
+
all_results["insights"] = []
|
|
149
|
+
|
|
150
|
+
# Module 10 - Dashboard - FIXED: check dashboard parameter
|
|
151
|
+
dashboard_path = None
|
|
152
|
+
if dashboard:
|
|
153
|
+
print("\n[LazyAnalyst] Step 10/11 — Building dashboard...")
|
|
154
|
+
dashboard_input = {
|
|
155
|
+
"cleaned_df": all_results["cleaned_df"],
|
|
156
|
+
"schema": all_results["schema"],
|
|
157
|
+
"quality_report": all_results["quality_report"],
|
|
158
|
+
"eda_results": all_results["eda_results"],
|
|
159
|
+
"stats_results": all_results["stats_results"],
|
|
160
|
+
"insights": all_results["insights"],
|
|
161
|
+
"filename": all_results["filename"]
|
|
162
|
+
}
|
|
163
|
+
try:
|
|
164
|
+
dashboard.build(dashboard_input)
|
|
165
|
+
dashboard_path = "outputs/dashboard.html"
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f"[LazyAnalyst] Warning: dashboard failed — {e}. Skipping this step.")
|
|
168
|
+
else:
|
|
169
|
+
print("\n[LazyAnalyst] Step 10/11 — Skipping dashboard (disabled)...")
|
|
170
|
+
|
|
171
|
+
# Module 11 - Reporter - FIXED: check report parameter
|
|
172
|
+
report_path = None
|
|
173
|
+
if report:
|
|
174
|
+
print("\n[LazyAnalyst] Step 11/11 — Generating HTML report...")
|
|
175
|
+
report_input = {
|
|
176
|
+
"cleaned_df": all_results["cleaned_df"],
|
|
177
|
+
"schema": all_results["schema"],
|
|
178
|
+
"quality_report": all_results["quality_report"],
|
|
179
|
+
"cleaning_actions": all_results["cleaning_actions"],
|
|
180
|
+
"eda_results": all_results["eda_results"],
|
|
181
|
+
"features_created": all_results["features_created"],
|
|
182
|
+
"stats_results": all_results["stats_results"],
|
|
183
|
+
"insights": all_results["insights"],
|
|
184
|
+
"filename": all_results["filename"]
|
|
185
|
+
}
|
|
186
|
+
try:
|
|
187
|
+
reporter.build(report_input)
|
|
188
|
+
report_path = "outputs/report.html"
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print(f"[LazyAnalyst] Warning: reporter failed — {e}. Skipping this step.")
|
|
191
|
+
else:
|
|
192
|
+
print("\n[LazyAnalyst] Step 11/11 — Skipping report (disabled)...")
|
|
193
|
+
|
|
194
|
+
print("\n[LazyAnalyst] Analysis complete!")
|
|
195
|
+
print(f"[LazyAnalyst] Outputs saved to ./outputs/")
|
|
196
|
+
|
|
197
|
+
# return result object
|
|
198
|
+
cleaned_data_path = "outputs/cleaned_data.csv"
|
|
199
|
+
return LazyAnalystResult(
|
|
200
|
+
cleaned_data_path=cleaned_data_path,
|
|
201
|
+
report_path=report_path if report_path else "",
|
|
202
|
+
dashboard_path=dashboard_path if dashboard_path else "",
|
|
203
|
+
cleaned_df=all_results["cleaned_df"]
|
|
204
|
+
)
|
lazyanalyst/cleaner.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
def clean(df, schema, quality_report):
|
|
6
|
+
"""cleans the dataset and returns cleaned df and actions list"""
|
|
7
|
+
|
|
8
|
+
actions = []
|
|
9
|
+
df_clean = df.copy()
|
|
10
|
+
|
|
11
|
+
# make outputs folder if needed
|
|
12
|
+
if not os.path.exists("outputs"):
|
|
13
|
+
os.makedirs("outputs")
|
|
14
|
+
|
|
15
|
+
# 1. Remove duplicate rows
|
|
16
|
+
before = len(df_clean)
|
|
17
|
+
df_clean = df_clean.drop_duplicates()
|
|
18
|
+
after = len(df_clean)
|
|
19
|
+
removed = before - after
|
|
20
|
+
if removed > 0:
|
|
21
|
+
actions.append(f"Removed {removed} duplicate rows")
|
|
22
|
+
print(f"[LazyAnalyst] Removed {removed} duplicate rows")
|
|
23
|
+
|
|
24
|
+
# 2. Fill missing values
|
|
25
|
+
for col in df_clean.columns:
|
|
26
|
+
missing_count = df_clean[col].isna().sum()
|
|
27
|
+
if missing_count == 0:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
col_type = schema.get(col, 'categorical')
|
|
31
|
+
|
|
32
|
+
# numerical -> median
|
|
33
|
+
if col_type == 'numerical':
|
|
34
|
+
median_val = df_clean[col].median()
|
|
35
|
+
# FIXED: use assignment instead of inplace=True for pandas 3.x CoW
|
|
36
|
+
df_clean[col] = df_clean[col].fillna(median_val)
|
|
37
|
+
actions.append(f"Filled {missing_count} missing values in '{col}' with median ({median_val:.2f})")
|
|
38
|
+
print(f"[LazyAnalyst] Filled {missing_count} missing in '{col}' with median")
|
|
39
|
+
|
|
40
|
+
# categorical -> mode
|
|
41
|
+
elif col_type == 'categorical':
|
|
42
|
+
mode_val = df_clean[col].mode()
|
|
43
|
+
if len(mode_val) > 0:
|
|
44
|
+
mode_val = mode_val[0]
|
|
45
|
+
# FIXED: use assignment instead of inplace=True
|
|
46
|
+
df_clean[col] = df_clean[col].fillna(mode_val)
|
|
47
|
+
actions.append(f"Filled {missing_count} missing values in '{col}' with mode ({mode_val})")
|
|
48
|
+
print(f"[LazyAnalyst] Filled {missing_count} missing in '{col}' with mode")
|
|
49
|
+
else:
|
|
50
|
+
# fallback if no mode
|
|
51
|
+
df_clean[col] = df_clean[col].fillna("unknown")
|
|
52
|
+
actions.append(f"Filled {missing_count} missing values in '{col}' with 'unknown'")
|
|
53
|
+
|
|
54
|
+
# datetime -> leave as-is (do nothing)
|
|
55
|
+
elif col_type == 'datetime':
|
|
56
|
+
actions.append(f"Left {missing_count} missing values in datetime column '{col}' as-is")
|
|
57
|
+
|
|
58
|
+
# 3. Parse datetime columns
|
|
59
|
+
for col in df_clean.columns:
|
|
60
|
+
if schema.get(col) == 'datetime':
|
|
61
|
+
try:
|
|
62
|
+
df_clean[col] = pd.to_datetime(df_clean[col], errors='coerce')
|
|
63
|
+
actions.append(f"Parsed datetime column: '{col}'")
|
|
64
|
+
print(f"[LazyAnalyst] Parsed datetime column: '{col}'")
|
|
65
|
+
except Exception as e:
|
|
66
|
+
actions.append(f"Warning: could not parse datetime column '{col}'")
|
|
67
|
+
print(f"[LazyAnalyst] Warning: could not parse datetime column '{col}'")
|
|
68
|
+
|
|
69
|
+
# 4. Flag outliers - FIXED: use is_numeric_dtype
|
|
70
|
+
for col in df_clean.columns:
|
|
71
|
+
if schema.get(col) == 'numerical' and pd.api.types.is_numeric_dtype(df_clean[col]):
|
|
72
|
+
q1 = df_clean[col].quantile(0.25)
|
|
73
|
+
q3 = df_clean[col].quantile(0.75)
|
|
74
|
+
iqr = q3 - q1
|
|
75
|
+
lower = q1 - 1.5 * iqr
|
|
76
|
+
upper = q3 + 1.5 * iqr
|
|
77
|
+
outlier_flag = (df_clean[col] < lower) | (df_clean[col] > upper)
|
|
78
|
+
col_name_outlier = f"{col}_is_outlier"
|
|
79
|
+
df_clean[col_name_outlier] = outlier_flag
|
|
80
|
+
outlier_count = outlier_flag.sum()
|
|
81
|
+
if outlier_count > 0:
|
|
82
|
+
actions.append(f"Flagged outliers in column: '{col}' ({outlier_count} rows)")
|
|
83
|
+
print(f"[LazyAnalyst] Flagged outliers in column: '{col}' ({outlier_count} rows)")
|
|
84
|
+
|
|
85
|
+
# save cleaned data
|
|
86
|
+
df_clean.to_csv("outputs/cleaned_data.csv", index=False)
|
|
87
|
+
print(f"[LazyAnalyst] Saved cleaned data to outputs/cleaned_data.csv")
|
|
88
|
+
|
|
89
|
+
return df_clean, actions
|
lazyanalyst/dashboard.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
import plotly.express as px
|
|
4
|
+
import plotly.graph_objects as go
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
def build(all_results):
|
|
8
|
+
"""builds and saves self-contained dashboard HTML"""
|
|
9
|
+
|
|
10
|
+
# unpack results (this is messy but works)
|
|
11
|
+
cleaned_df = all_results.get("cleaned_df")
|
|
12
|
+
schema = all_results.get("schema")
|
|
13
|
+
quality_report = all_results.get("quality_report")
|
|
14
|
+
eda_results = all_results.get("eda_results")
|
|
15
|
+
stats_results = all_results.get("stats_results")
|
|
16
|
+
insights_list = all_results.get("insights")
|
|
17
|
+
|
|
18
|
+
if cleaned_df is None:
|
|
19
|
+
print("[LazyAnalyst] Warning: no cleaned dataframe for dashboard")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# get dataset info
|
|
23
|
+
filename = all_results.get("filename", "dataset")
|
|
24
|
+
n_rows = len(cleaned_df)
|
|
25
|
+
n_cols = len(cleaned_df.columns)
|
|
26
|
+
quality_score = quality_report.get("score", 0) if quality_report else 0
|
|
27
|
+
|
|
28
|
+
# start building HTML
|
|
29
|
+
html_parts = []
|
|
30
|
+
html_parts.append("""
|
|
31
|
+
<!DOCTYPE html>
|
|
32
|
+
<html>
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="UTF-8">
|
|
35
|
+
<title>LazyAnalyst Dashboard</title>
|
|
36
|
+
<style>
|
|
37
|
+
body {
|
|
38
|
+
background-color: #0f1117;
|
|
39
|
+
color: #e0e0e0;
|
|
40
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
41
|
+
margin: 0;
|
|
42
|
+
padding: 20px;
|
|
43
|
+
}
|
|
44
|
+
.container {
|
|
45
|
+
max-width: 1400px;
|
|
46
|
+
margin: 0 auto;
|
|
47
|
+
}
|
|
48
|
+
h1, h2, h3 {
|
|
49
|
+
color: #4fc3f7;
|
|
50
|
+
}
|
|
51
|
+
h1 {
|
|
52
|
+
border-bottom: 2px solid #4fc3f7;
|
|
53
|
+
padding-bottom: 10px;
|
|
54
|
+
}
|
|
55
|
+
.section {
|
|
56
|
+
margin-bottom: 40px;
|
|
57
|
+
background: #1a1c23;
|
|
58
|
+
padding: 20px;
|
|
59
|
+
border-radius: 8px;
|
|
60
|
+
}
|
|
61
|
+
.metric {
|
|
62
|
+
display: inline-block;
|
|
63
|
+
background: #2a2c35;
|
|
64
|
+
padding: 10px 20px;
|
|
65
|
+
border-radius: 6px;
|
|
66
|
+
margin-right: 15px;
|
|
67
|
+
margin-bottom: 10px;
|
|
68
|
+
}
|
|
69
|
+
table {
|
|
70
|
+
width: 100%;
|
|
71
|
+
border-collapse: collapse;
|
|
72
|
+
margin-top: 15px;
|
|
73
|
+
}
|
|
74
|
+
th, td {
|
|
75
|
+
text-align: left;
|
|
76
|
+
padding: 10px;
|
|
77
|
+
border-bottom: 1px solid #333;
|
|
78
|
+
}
|
|
79
|
+
th {
|
|
80
|
+
background-color: #2a2c35;
|
|
81
|
+
color: #4fc3f7;
|
|
82
|
+
}
|
|
83
|
+
.insight-list {
|
|
84
|
+
list-style-type: none;
|
|
85
|
+
padding-left: 0;
|
|
86
|
+
}
|
|
87
|
+
.insight-list li {
|
|
88
|
+
background: #2a2c35;
|
|
89
|
+
margin: 10px 0;
|
|
90
|
+
padding: 12px;
|
|
91
|
+
border-radius: 6px;
|
|
92
|
+
border-left: 4px solid #4fc3f7;
|
|
93
|
+
}
|
|
94
|
+
.chart-container {
|
|
95
|
+
margin: 20px 0;
|
|
96
|
+
padding: 10px;
|
|
97
|
+
background: #1a1c23;
|
|
98
|
+
border-radius: 6px;
|
|
99
|
+
}
|
|
100
|
+
.two-column {
|
|
101
|
+
display: flex;
|
|
102
|
+
gap: 20px;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
}
|
|
105
|
+
.two-column > div {
|
|
106
|
+
flex: 1;
|
|
107
|
+
min-width: 300px;
|
|
108
|
+
}
|
|
109
|
+
</style>
|
|
110
|
+
</head>
|
|
111
|
+
<body>
|
|
112
|
+
<div class="container">
|
|
113
|
+
<h1>LazyAnalyst Dashboard</h1>
|
|
114
|
+
""")
|
|
115
|
+
|
|
116
|
+
# 1. Overview section
|
|
117
|
+
html_parts.append(f"""
|
|
118
|
+
<div class="section">
|
|
119
|
+
<h2>Overview</h2>
|
|
120
|
+
<div class="metric">📄 Dataset: {filename}</div>
|
|
121
|
+
<div class="metric">📊 Rows: {n_rows}</div>
|
|
122
|
+
<div class="metric">📋 Columns: {n_cols}</div>
|
|
123
|
+
<div class="metric">⭐ Quality Score: {quality_score}/100</div>
|
|
124
|
+
<h3>Columns & Types</h3>
|
|
125
|
+
<table>
|
|
126
|
+
<tr><th>Column Name</th><th>Detected Type</th></tr>
|
|
127
|
+
""")
|
|
128
|
+
|
|
129
|
+
if schema:
|
|
130
|
+
for col, col_type in schema.items():
|
|
131
|
+
html_parts.append(f"<tr><td>{col}</td><td>{col_type}</td></tr>")
|
|
132
|
+
html_parts.append("</table></div>")
|
|
133
|
+
|
|
134
|
+
# 2. Data Quality section
|
|
135
|
+
missing_data = quality_report.get("missing", {}) if quality_report else {}
|
|
136
|
+
dup_info = quality_report.get("duplicates", {"count":0, "pct":0}) if quality_report else {"count":0, "pct":0}
|
|
137
|
+
outliers_info = quality_report.get("outliers", {}) if quality_report else {}
|
|
138
|
+
|
|
139
|
+
# missing values bar chart (plotly)
|
|
140
|
+
if missing_data:
|
|
141
|
+
missing_cols = list(missing_data.keys())
|
|
142
|
+
missing_pcts = [missing_data[c]["pct"] for c in missing_cols]
|
|
143
|
+
fig_missing = px.bar(x=missing_cols, y=missing_pcts, labels={'x':'Column', 'y':'Missing (%)'}, title="Missing Values by Column")
|
|
144
|
+
fig_missing.update_layout(template="plotly_dark", paper_bgcolor="#1a1c23", plot_bgcolor="#1a1c23")
|
|
145
|
+
html_parts.append(f"""
|
|
146
|
+
<div class="section">
|
|
147
|
+
<h2>Data Quality</h2>
|
|
148
|
+
<div class="chart-container">
|
|
149
|
+
{fig_missing.to_html(full_html=False, include_plotlyjs='cdn')}
|
|
150
|
+
</div>
|
|
151
|
+
<p>Duplicate rows: {dup_info['count']} ({dup_info['pct']}%)</p>
|
|
152
|
+
<p>Outliers flagged in columns: {', '.join(outliers_info.keys()) if outliers_info else 'none'}</p>
|
|
153
|
+
</div>
|
|
154
|
+
""")
|
|
155
|
+
else:
|
|
156
|
+
html_parts.append(f"""
|
|
157
|
+
<div class="section">
|
|
158
|
+
<h2>Data Quality</h2>
|
|
159
|
+
<p>No missing values found.</p>
|
|
160
|
+
<p>Duplicate rows: {dup_info['count']} ({dup_info['pct']}%)</p>
|
|
161
|
+
<p>Outliers flagged in columns: {', '.join(outliers_info.keys()) if outliers_info else 'none'}</p>
|
|
162
|
+
</div>
|
|
163
|
+
""")
|
|
164
|
+
|
|
165
|
+
# 3. Distributions
|
|
166
|
+
num_cols = [col for col in schema if schema[col]=='numerical'] if schema else []
|
|
167
|
+
cat_cols = [col for col in schema if schema[col]=='categorical'] if schema else []
|
|
168
|
+
|
|
169
|
+
html_parts.append('<div class="section"><h2>Distributions</h2>')
|
|
170
|
+
# histograms for numerical
|
|
171
|
+
for col in num_cols[:5]: # limit to 5
|
|
172
|
+
fig_hist = px.histogram(cleaned_df, x=col, nbins=30, title=f"Distribution of {col}", template="plotly_dark")
|
|
173
|
+
fig_hist.update_layout(paper_bgcolor="#1a1c23", plot_bgcolor="#1a1c23")
|
|
174
|
+
html_parts.append(f'<div class="chart-container">{fig_hist.to_html(full_html=False, include_plotlyjs="cdn")}</div>')
|
|
175
|
+
|
|
176
|
+
# bar charts for categorical (top 10)
|
|
177
|
+
for col in cat_cols[:5]:
|
|
178
|
+
top10 = cleaned_df[col].value_counts().nlargest(10).reset_index()
|
|
179
|
+
top10.columns = [col, 'count']
|
|
180
|
+
fig_bar = px.bar(top10, x=col, y='count', title=f"Top 10 categories in {col}", template="plotly_dark")
|
|
181
|
+
fig_bar.update_layout(paper_bgcolor="#1a1c23", plot_bgcolor="#1a1c23")
|
|
182
|
+
html_parts.append(f'<div class="chart-container">{fig_bar.to_html(full_html=False, include_plotlyjs="cdn")}</div>')
|
|
183
|
+
html_parts.append('</div>')
|
|
184
|
+
|
|
185
|
+
# 4. Correlations (heatmap)
|
|
186
|
+
if len(num_cols) >= 2:
|
|
187
|
+
corr_matrix = cleaned_df[num_cols].corr()
|
|
188
|
+
fig_corr = go.Figure(data=go.Heatmap(z=corr_matrix.values, x=corr_matrix.columns, y=corr_matrix.columns, colorscale='RdBu', zmid=0))
|
|
189
|
+
fig_corr.update_layout(title="Correlation Heatmap", template="plotly_dark", paper_bgcolor="#1a1c23", plot_bgcolor="#1a1c23")
|
|
190
|
+
html_parts.append(f"""
|
|
191
|
+
<div class="section">
|
|
192
|
+
<h2>Correlations</h2>
|
|
193
|
+
<div class="chart-container">{fig_corr.to_html(full_html=False, include_plotlyjs='cdn')}</div>
|
|
194
|
+
</div>
|
|
195
|
+
""")
|
|
196
|
+
|
|
197
|
+
# 5. Statistical Tests (table)
|
|
198
|
+
html_parts.append('<div class="section"><h2>Statistical Tests</h2>')
|
|
199
|
+
if stats_results:
|
|
200
|
+
html_parts.append("""
|
|
201
|
+
<table>
|
|
202
|
+
<tr><th>Test</th><th>Columns</th><th>p-value</th><th>Significant</th><th>Interpretation</th></tr>
|
|
203
|
+
""")
|
|
204
|
+
for stat in stats_results:
|
|
205
|
+
test = stat.get("test", "")
|
|
206
|
+
cols = ", ".join(stat.get("columns", []))
|
|
207
|
+
p_val = stat.get("p_value", 1)
|
|
208
|
+
sig = "Yes" if stat.get("significant", False) else "No"
|
|
209
|
+
interp = stat.get("interpretation", "")
|
|
210
|
+
html_parts.append(f"<tr><td>{test}</td><td>{cols}</td><td>{p_val}</td><td>{sig}</td><td>{interp}</td></tr>")
|
|
211
|
+
html_parts.append("</table>")
|
|
212
|
+
else:
|
|
213
|
+
html_parts.append("<p>No statistical tests were run.</p>")
|
|
214
|
+
html_parts.append('</div>')
|
|
215
|
+
|
|
216
|
+
# 6. Insights
|
|
217
|
+
html_parts.append('<div class="section"><h2>Insights</h2><ul class="insight-list">')
|
|
218
|
+
if insights_list:
|
|
219
|
+
for ins in insights_list:
|
|
220
|
+
html_parts.append(f"<li>{ins}</li>")
|
|
221
|
+
else:
|
|
222
|
+
html_parts.append("<li>No insights generated.</li>")
|
|
223
|
+
html_parts.append("</ul></div>")
|
|
224
|
+
|
|
225
|
+
html_parts.append("</div></body></html>")
|
|
226
|
+
|
|
227
|
+
# write to file
|
|
228
|
+
full_html = "".join(html_parts)
|
|
229
|
+
if not os.path.exists("outputs"):
|
|
230
|
+
os.makedirs("outputs")
|
|
231
|
+
with open("outputs/dashboard.html", "w", encoding="utf-8") as f:
|
|
232
|
+
f.write(full_html)
|
|
233
|
+
print("[LazyAnalyst] Dashboard saved to outputs/dashboard.html")
|
lazyanalyst/eda.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.stats import skew
|
|
4
|
+
|
|
5
|
+
def run(df, schema):
|
|
6
|
+
"""generates summary statistics and correlation matrix"""
|
|
7
|
+
|
|
8
|
+
eda_results = {
|
|
9
|
+
"numerical": {},
|
|
10
|
+
"categorical": {},
|
|
11
|
+
"correlation": {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# get numerical columns
|
|
15
|
+
num_cols = [col for col in df.columns if schema.get(col) == 'numerical']
|
|
16
|
+
cat_cols = [col for col in df.columns if schema.get(col) == 'categorical']
|
|
17
|
+
|
|
18
|
+
# 1. Numerical summary - FIXED: use is_numeric_dtype
|
|
19
|
+
for col in num_cols:
|
|
20
|
+
if col in df.columns and pd.api.types.is_numeric_dtype(df[col]):
|
|
21
|
+
data = df[col].dropna()
|
|
22
|
+
if len(data) > 0:
|
|
23
|
+
# using .iloc and .loc mix
|
|
24
|
+
q1_val = data.quantile(0.25)
|
|
25
|
+
q3_val = data.quantile(0.75)
|
|
26
|
+
skewness = skew(data) if len(data) > 2 else 0
|
|
27
|
+
|
|
28
|
+
eda_results["numerical"][col] = {
|
|
29
|
+
"mean": round(data.mean(), 4),
|
|
30
|
+
"median": round(data.median(), 4),
|
|
31
|
+
"std": round(data.std(), 4),
|
|
32
|
+
"variance": round(data.var(), 4),
|
|
33
|
+
"min": round(data.min(), 4),
|
|
34
|
+
"max": round(data.max(), 4),
|
|
35
|
+
"Q1": round(q1_val, 4),
|
|
36
|
+
"Q3": round(q3_val, 4),
|
|
37
|
+
"skewness": round(skewness, 4)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# 2. Categorical summary
|
|
41
|
+
for col in cat_cols:
|
|
42
|
+
if col in df.columns:
|
|
43
|
+
data = df[col].dropna()
|
|
44
|
+
if len(data) > 0:
|
|
45
|
+
value_counts = data.value_counts()
|
|
46
|
+
top5 = value_counts.head(5).to_dict()
|
|
47
|
+
# convert top5 keys to string in case they are numbers
|
|
48
|
+
top5_str = {str(k): v for k, v in top5.items()}
|
|
49
|
+
|
|
50
|
+
eda_results["categorical"][col] = {
|
|
51
|
+
"unique": data.nunique(),
|
|
52
|
+
"top": top5_str
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# 3. Correlation matrix (Pearson) for numerical columns
|
|
56
|
+
if len(num_cols) >= 2:
|
|
57
|
+
# make sure all exist and are numeric
|
|
58
|
+
num_cols_exist = [c for c in num_cols if c in df.columns and pd.api.types.is_numeric_dtype(df[c])]
|
|
59
|
+
if len(num_cols_exist) >= 2:
|
|
60
|
+
corr_matrix = df[num_cols_exist].corr()
|
|
61
|
+
# convert to nested dict
|
|
62
|
+
corr_dict = {}
|
|
63
|
+
for i in corr_matrix.index:
|
|
64
|
+
corr_dict[i] = {}
|
|
65
|
+
for j in corr_matrix.columns:
|
|
66
|
+
if i != j:
|
|
67
|
+
corr_dict[i][j] = round(corr_matrix.loc[i, j], 4)
|
|
68
|
+
eda_results["correlation"] = corr_dict
|
|
69
|
+
|
|
70
|
+
print(f"[LazyAnalyst] EDA complete: {len(num_cols)} numerical, {len(cat_cols)} categorical columns analyzed")
|
|
71
|
+
|
|
72
|
+
return eda_results
|
lazyanalyst/features.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.stats import skew
|
|
4
|
+
|
|
5
|
+
def engineer(df, schema):
|
|
6
|
+
"""adds new features to the dataframe"""
|
|
7
|
+
|
|
8
|
+
df_new = df.copy()
|
|
9
|
+
features_created = []
|
|
10
|
+
|
|
11
|
+
# 1. From datetime columns
|
|
12
|
+
for col in df_new.columns:
|
|
13
|
+
if schema.get(col) == 'datetime':
|
|
14
|
+
try:
|
|
15
|
+
# ensure it's datetime
|
|
16
|
+
if not pd.api.types.is_datetime64_any_dtype(df_new[col]):
|
|
17
|
+
df_new[col] = pd.to_datetime(df_new[col], errors='coerce')
|
|
18
|
+
|
|
19
|
+
# extract components
|
|
20
|
+
df_new[f'{col}_year'] = df_new[col].dt.year
|
|
21
|
+
df_new[f'{col}_month'] = df_new[col].dt.month
|
|
22
|
+
df_new[f'{col}_day'] = df_new[col].dt.day
|
|
23
|
+
df_new[f'{col}_quarter'] = df_new[col].dt.quarter
|
|
24
|
+
df_new[f'{col}_weekday'] = df_new[col].dt.weekday
|
|
25
|
+
df_new[f'{col}_is_weekend'] = (df_new[col].dt.weekday >= 5).astype(int)
|
|
26
|
+
|
|
27
|
+
features_created.append(f"Created {col}_year")
|
|
28
|
+
features_created.append(f"Created {col}_month")
|
|
29
|
+
features_created.append(f"Created {col}_day")
|
|
30
|
+
features_created.append(f"Created {col}_quarter")
|
|
31
|
+
features_created.append(f"Created {col}_weekday")
|
|
32
|
+
features_created.append(f"Created {col}_is_weekend")
|
|
33
|
+
|
|
34
|
+
print(f"[LazyAnalyst] Added datetime features from '{col}'")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"[LazyAnalyst] Warning: could not extract datetime features from {col} - {e}")
|
|
37
|
+
|
|
38
|
+
# 2. From categorical columns (label encoding)
|
|
39
|
+
for col in df_new.columns:
|
|
40
|
+
if schema.get(col) == 'categorical':
|
|
41
|
+
try:
|
|
42
|
+
# use pd.factorize for label encoding
|
|
43
|
+
encoded, _ = pd.factorize(df_new[col])
|
|
44
|
+
df_new[f'{col}_encoded'] = encoded
|
|
45
|
+
features_created.append(f"Created {col}_encoded")
|
|
46
|
+
print(f"[LazyAnalyst] Label encoded '{col}'")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"[LazyAnalyst] Warning: could not encode {col} - {e}")
|
|
49
|
+
|
|
50
|
+
# 3. From numerical columns (log transform if skewed)
|
|
51
|
+
for col in df_new.columns:
|
|
52
|
+
if schema.get(col) == 'numerical':
|
|
53
|
+
# check skewness
|
|
54
|
+
data = df_new[col].dropna()
|
|
55
|
+
if len(data) > 2:
|
|
56
|
+
skew_val = skew(data)
|
|
57
|
+
if abs(skew_val) > 1.0:
|
|
58
|
+
try:
|
|
59
|
+
# using np.log1p to handle zeros
|
|
60
|
+
df_new[f'{col}_log'] = np.log1p(df_new[col])
|
|
61
|
+
features_created.append(f"Created {col}_log")
|
|
62
|
+
print(f"[LazyAnalyst] Added log transform for '{col}' (skewness={skew_val:.2f})")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"[LazyAnalyst] Warning: could not log transform {col} - {e}")
|
|
65
|
+
|
|
66
|
+
print(f"[LazyAnalyst] Total new features created: {len(features_created)}")
|
|
67
|
+
return df_new, features_created
|