DataMind-AI / eda.py
samuelalex37's picture
V2 Release: Complete UI overhaul and new AI Analytics engine
6193995
Raw
History Blame Contribute Delete
10.7 kB
"""
DataMind AI — EDA Pipeline Module
Comprehensive automated Exploratory Data Analysis.
"""
import pandas as pd
import numpy as np
from typing import Dict, Any, List, Tuple
def _detect_date_columns(df: pd.DataFrame) -> List[str]:
"""Detect columns that can be parsed as dates."""
date_cols = []
for col in df.columns:
if pd.api.types.is_datetime64_any_dtype(df[col]):
date_cols.append(col)
continue
if df[col].dtype == object:
sample = df[col].dropna().head(20)
if len(sample) == 0:
continue
try:
parsed = pd.to_datetime(sample, errors='coerce')
if parsed.notna().sum() >= len(sample) * 0.8:
date_cols.append(col)
except (ValueError, TypeError):
continue
return date_cols
def _detect_numeric_string_columns(df: pd.DataFrame) -> List[str]:
"""Detect object columns that should be numeric."""
numeric_str_cols = []
for col in df.columns:
if df[col].dtype == object:
sample = df[col].dropna().head(30)
if len(sample) == 0:
continue
try:
converted = pd.to_numeric(sample, errors='coerce')
if converted.notna().sum() >= len(sample) * 0.8:
numeric_str_cols.append(col)
except (ValueError, TypeError):
continue
return numeric_str_cols
def run_full_eda(df: pd.DataFrame) -> Dict[str, Any]:
"""
Run a thorough EDA pipeline on the given DataFrame.
Returns a comprehensive results dictionary.
"""
results = {}
df = df.copy()
# =========================================================================
# 1. SHAPE & OVERVIEW
# =========================================================================
dtypes_dict = df.dtypes.astype(str).to_dict()
dtype_counts = {}
for dtype_str in dtypes_dict.values():
key = str(dtype_str)
dtype_counts[key] = dtype_counts.get(key, 0) + 1
results["shape"] = {
"rows": int(df.shape[0]),
"columns": int(df.shape[1]),
"column_names": list(df.columns),
"dtypes": dtypes_dict,
"dtype_counts": dtype_counts,
"memory_usage_mb": round(df.memory_usage(deep=True).sum() / (1024 * 1024), 2)
}
# =========================================================================
# 2. DATA TYPE FIXING (before missing value analysis)
# =========================================================================
type_fixes = []
# Detect and convert date columns
date_cols = _detect_date_columns(df)
for col in date_cols:
if not pd.api.types.is_datetime64_any_dtype(df[col]):
try:
df[col] = pd.to_datetime(df[col], errors='coerce')
type_fixes.append({"column": col, "from": "object", "to": "datetime64"})
except Exception:
pass
# Detect and convert numeric string columns
numeric_str_cols = _detect_numeric_string_columns(df)
for col in numeric_str_cols:
if col not in date_cols:
try:
df[col] = pd.to_numeric(df[col], errors='coerce')
type_fixes.append({"column": col, "from": "object", "to": "float64"})
except Exception:
pass
results["type_fixes"] = type_fixes
# =========================================================================
# 3. MISSING VALUES — BEFORE
# =========================================================================
missing_before = df.isnull().sum().to_dict()
missing_before = {k: int(v) for k, v in missing_before.items() if v > 0}
total_missing_before = int(df.isnull().sum().sum())
# Fill strategies
fill_strategies = {}
for col in df.columns:
if df[col].isnull().sum() == 0:
continue
if pd.api.types.is_datetime64_any_dtype(df[col]):
df[col] = df[col].ffill()
if df[col].isnull().sum() > 0:
df[col] = df[col].bfill()
fill_strategies[col] = "forward-fill (then back-fill)"
elif pd.api.types.is_numeric_dtype(df[col]):
median_val = df[col].median()
df[col] = df[col].fillna(median_val)
fill_strategies[col] = f"median ({median_val:.2f})"
else:
mode_vals = df[col].mode()
if len(mode_vals) > 0:
df[col] = df[col].fillna(mode_vals[0])
fill_strategies[col] = f"mode ('{mode_vals[0]}')"
else:
df[col] = df[col].fillna("Unknown")
fill_strategies[col] = "fallback ('Unknown')"
missing_after = df.isnull().sum().to_dict()
missing_after = {k: int(v) for k, v in missing_after.items() if v > 0}
total_missing_after = int(df.isnull().sum().sum())
results["missing_values"] = {
"before": missing_before,
"after": missing_after,
"total_before": total_missing_before,
"total_after": total_missing_after,
"strategies": fill_strategies
}
# =========================================================================
# 4. DUPLICATES
# =========================================================================
dup_count = int(df.duplicated().sum())
if dup_count > 0:
df = df.drop_duplicates().reset_index(drop=True)
results["duplicates"] = {
"found": dup_count,
"removed": dup_count,
"rows_after": int(len(df))
}
# =========================================================================
# 5. CAPITALISATION NORMALISATION
# =========================================================================
normalised_cols = []
for col in df.columns:
if df[col].dtype == object:
try:
# Skip free text or high-cardinality columns for performance
if df[col].nunique() > 1000:
continue
before_sample = df[col].dropna().head(5).tolist()
# Optimised transformation using categories (instant for millions of rows)
cat_col = df[col].astype('category')
new_categories = [str(x).strip().title() if pd.notna(x) else x for x in cat_col.cat.categories]
cat_col = cat_col.cat.rename_categories(new_categories)
df[col] = cat_col.astype(object)
after_sample = df[col].dropna().head(5).tolist()
if before_sample != after_sample:
normalised_cols.append(col)
except Exception:
pass
results["capitalisation"] = {
"normalised_columns": normalised_cols,
"method": "Title Case"
}
# =========================================================================
# 6. OUTLIER DETECTION (IQR method)
# =========================================================================
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
outlier_info = {}
for col in numeric_cols:
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outlier_mask = (df[col] < lower_bound) | (df[col] > upper_bound)
outlier_count = int(outlier_mask.sum())
if outlier_count > 0:
outlier_info[col] = {
"count": outlier_count,
"percentage": round(outlier_count / len(df) * 100, 1),
"lower_bound": round(float(lower_bound), 2),
"upper_bound": round(float(upper_bound), 2),
"Q1": round(float(Q1), 2),
"Q3": round(float(Q3), 2),
"IQR": round(float(IQR), 2)
}
# Flag but do NOT remove
df[f"_{col}_outlier"] = outlier_mask
results["outliers"] = outlier_info
# =========================================================================
# 7. SUMMARY STATISTICS
# =========================================================================
summary_stats = {}
for col in numeric_cols:
stats = df[col].describe()
summary_stats[col] = {
"count": int(stats.get("count", 0)),
"mean": round(float(stats.get("mean", 0)), 2),
"std": round(float(stats.get("std", 0)), 2),
"min": round(float(stats.get("min", 0)), 2),
"25%": round(float(stats.get("25%", 0)), 2),
"median": round(float(df[col].median()), 2),
"75%": round(float(stats.get("75%", 0)), 2),
"max": round(float(stats.get("max", 0)), 2)
}
results["summary_stats"] = summary_stats
# =========================================================================
# 8. CORRELATION MATRIX
# =========================================================================
if len(numeric_cols) >= 2:
corr = df[numeric_cols].corr()
corr_dict = {}
for col in corr.columns:
corr_dict[col] = {k: round(float(v), 3) for k, v in corr[col].items()}
results["correlation"] = corr_dict
else:
results["correlation"] = {}
# =========================================================================
# Store categorical column info
# =========================================================================
cat_cols = df.select_dtypes(include=["object"]).columns.tolist()
cat_info = {}
for col in cat_cols:
value_counts = df[col].value_counts().head(10).to_dict()
cat_info[col] = {
"unique_count": int(df[col].nunique()),
"top_values": {str(k): int(v) for k, v in value_counts.items()}
}
results["categorical_info"] = cat_info
# Store date column info
date_cols_final = [c for c in df.columns if pd.api.types.is_datetime64_any_dtype(df[c])]
date_info = {}
for col in date_cols_final:
date_info[col] = {
"min": str(df[col].min()),
"max": str(df[col].max()),
"range_days": int((df[col].max() - df[col].min()).days) if pd.notna(df[col].min()) and pd.notna(df[col].max()) else 0
}
results["date_info"] = date_info
# Drop outlier flag columns before returning cleaned df
outlier_flag_cols = [c for c in df.columns if c.startswith("_") and c.endswith("_outlier")]
df_clean = df.drop(columns=outlier_flag_cols, errors='ignore')
return results, df_clean