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.
@@ -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
@@ -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
@@ -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