# pip install seaborn mlxtend # app.py # app.py import os import io import tempfile import json import math import traceback from typing import Optional, List, Tuple, Dict, Any, Union import pandas as pd import numpy as np import gradio as gr import seaborn as sns from scipy import stats import warnings warnings.filterwarnings('ignore') import plotly.express as px import plotly.graph_objects as go import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score from sklearn.preprocessing import StandardScaler, MinMaxScaler, OneHotEncoder, LabelEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer from sklearn.feature_selection import RFECV from sklearn.linear_model import LinearRegression, Ridge, Lasso, LogisticRegression from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier, GradientBoostingRegressor, GradientBoostingClassifier from sklearn.svm import SVR, SVC from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor from sklearn.naive_bayes import GaussianNB from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score, accuracy_score, classification_report, confusion_matrix, roc_auc_score, roc_curve, silhouette_score, davies_bouldin_score, calinski_harabasz_score from sklearn.cluster import KMeans, DBSCAN from sklearn.decomposition import PCA from sklearn.exceptions import NotFittedError import scipy.stats as stats import matplotlib matplotlib.use("Agg") # for headless plotting import matplotlib.pyplot as plt # --- lightweight password hashing (stdlib) --- import os as _os import hashlib, binascii, hmac as _hmac def generate_password_hash(password: str) -> str: salt = _os.urandom(16) dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000) return binascii.hexlify(salt).decode() + ':' + binascii.hexlify(dk).decode() def check_password_hash(stored_hash: str, password: str) -> bool: try: salt_hex, dk_hex = stored_hash.split(':') except ValueError: return False salt = binascii.unhexlify(salt_hex) dk = binascii.unhexlify(dk_hex) new_dk = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000) return _hmac.compare_digest(new_dk, dk) # ------------------------------------------------ # ----------------------------- # Simple in-repo auth store (file-backed) # ----------------------------- AUTH_STORE = "auth_store.json" def _ensure_auth(): if not os.path.exists(AUTH_STORE): admin_pass = generate_password_hash("password") with open(AUTH_STORE, "w") as f: json.dump({"admin": admin_pass}, f) def authenticate(username: str, password: str) -> bool: _ensure_auth() with open(AUTH_STORE, "r") as f: data = json.load(f) stored = data.get(username) if not stored: return False return check_password_hash(stored, password) # seed auth _ensure_auth() # ----------------------------- # App constants & helpers # ----------------------------- HIGH_CARD_THRESHOLD_COUNT = 50 HIGH_CARD_THRESHOLD_RATIO = 0.10 LOGO_PATH = "DataSynth.png" # ----------------------------- # Small utility functions # ----------------------------- def read_file_to_df(uploaded) -> pd.DataFrame: if uploaded is None: return pd.DataFrame() try: if hasattr(uploaded, "name") and os.path.exists(uploaded.name): path = uploaded.name elif isinstance(uploaded, dict) and "name" in uploaded: path = uploaded["name"] else: content = uploaded.read() return _read_bytes_content(content) return _read_file_path(path) except Exception as e: print(f"read_file_to_df error: {e}") try: if 'path' in locals(): return pd.read_csv(path, encoding_errors='ignore', engine='python') except Exception: pass raise def _read_bytes_content(content) -> pd.DataFrame: formats_to_try = [ ('csv', lambda: pd.read_csv(io.BytesIO(content))), ('csv_utf8', lambda: pd.read_csv(io.BytesIO(content), encoding='utf-8')), ('csv_latin1', lambda: pd.read_csv(io.BytesIO(content), encoding='latin1')), ('csv_ignore', lambda: pd.read_csv(io.BytesIO(content), encoding_errors='ignore')), ('excel', lambda: pd.read_excel(io.BytesIO(content))), ('json', lambda: pd.read_json(io.BytesIO(content))), ] for format_name, reader_func in formats_to_try: try: return reader_func() except Exception as e: print(f"Failed to read as {format_name}: {e}") continue raise ValueError("Could not read file with any supported format") def _read_file_path(path) -> pd.DataFrame: file_ext = os.path.splitext(path)[1].lower() if file_ext in ['.csv', '.txt', '.tsv']: encodings = [None, 'utf-8', 'latin1', 'cp1252', 'iso-8859-1'] for encoding in encodings: try: if file_ext == '.tsv': return pd.read_csv(path, sep='\t', encoding=encoding, encoding_errors='ignore') else: return pd.read_csv(path, encoding=encoding, encoding_errors='ignore') except Exception: continue elif file_ext in ['.xlsx', '.xls', '.xlsm', '.xlsb']: return pd.read_excel(path) elif file_ext == '.json': return pd.read_json(path) elif file_ext in ['.parquet']: return pd.read_parquet(path) elif file_ext in ['.feather']: return pd.read_feather(path) try: return pd.read_csv(path, encoding_errors='ignore', engine='python') except Exception: return pd.read_excel(path) def comprehensive_data_profile(df: pd.DataFrame) -> Dict[str, Any]: if df is None or df.empty: return {} profile = {} profile["rows"], profile["columns"] = df.shape profile["memory_usage"] = df.memory_usage(deep=True).sum() / 1024**2 dtypes = df.dtypes.apply(lambda x: x.name).to_dict() profile["dtypes"] = dtypes nulls = df.isnull().sum().to_dict() profile["nulls"] = nulls profile["null_pct"] = {k: (v / len(df)) for k, v in nulls.items()} unique_counts = df.nunique(dropna=False).to_dict() profile["unique"] = unique_counts profile["duplicate_rows"] = df.duplicated().sum() profile["duplicate_pct"] = profile["duplicate_rows"] / len(df) numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist() categorical_cols = df.select_dtypes(include=["object", "category"]).columns.tolist() datetime_cols = df.select_dtypes(include=["datetime64"]).columns.tolist() profile["numeric_cols"] = numeric_cols profile["categorical_cols"] = categorical_cols profile["datetime_cols"] = datetime_cols high_cardinality = [] for col, cnt in unique_counts.items(): if col in categorical_cols and (cnt > HIGH_CARD_THRESHOLD_COUNT or (cnt / len(df) > HIGH_CARD_THRESHOLD_RATIO)): high_cardinality.append(col) profile["high_cardinality"] = high_cardinality quantitative_stats = {} for col in numeric_cols: col_data = df[col].dropna() if len(col_data) > 0: quantitative_stats[col] = { "mean": col_data.mean(), "median": col_data.median(), "std": col_data.std(), "min": col_data.min(), "max": col_data.max(), "q1": col_data.quantile(0.25), "q3": col_data.quantile(0.75), "skew": col_data.skew(), "kurtosis": col_data.kurtosis(), "zeros": (col_data == 0).sum(), "zeros_pct": (col_data == 0).sum() / len(col_data), "outliers": detect_outliers_iqr(col_data) } profile["quantitative_stats"] = quantitative_stats qualitative_stats = {} for col in categorical_cols: col_data = df[col].dropna() if len(col_data) > 0: value_counts = col_data.value_counts() qualitative_stats[col] = { "top_value": value_counts.index[0] if len(value_counts) > 0 else None, "top_freq": value_counts.iloc[0] if len(value_counts) > 0 else 0, "top_freq_pct": value_counts.iloc[0] / len(col_data) if len(value_counts) > 0 else 0, "unique_values": len(value_counts), "entropy": stats.entropy(value_counts.values) if len(value_counts) > 0 else 0 } profile["qualitative_stats"] = qualitative_stats quality_metrics = { "completeness": (len(df) - df.isnull().sum().sum()) / (len(df) * len(df.columns)), "uniqueness": 1 - (profile["duplicate_pct"]), "validity": {} } profile["quality_metrics"] = quality_metrics if len(numeric_cols) > 1: profile["correlation_matrix"] = df[numeric_cols].corr().round(3).to_dict() else: profile["correlation_matrix"] = {} profile["head"] = df.head(10).to_dict(orient="records") return profile def detect_outliers_iqr(series): Q1 = series.quantile(0.25) Q3 = series.quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR return ((series < lower_bound) | (series > upper_bound)).sum() def profile_to_enhanced_markdown(profile: Dict[str, Any]) -> str: if not profile: return "No data loaded." md = [] md.append("# 📊 Comprehensive Data Profile Report") md.append("---") md.append("## 🎯 Dataset Overview") with io.StringIO() as overview: overview.write(f"**Rows:** {profile['rows']:,} | ") overview.write(f"**Columns:** {profile['columns']} | ") overview.write(f"**Memory:** {profile['memory_usage']:.2f} MB | ") overview.write(f"**Duplicates:** {profile['duplicate_rows']} ({profile['duplicate_pct']:.2%})") md.append(overview.getvalue()) md.append("") md.append("## 📈 Data Quality Scorecard") quality = profile["quality_metrics"] md.append(f"**Completeness:** {quality['completeness']:.2%} | ") md.append(f"**Uniqueness:** {quality['uniqueness']:.2%}") md.append("") md.append("## 🗂️ Column Type Summary") md.append(f"**Numeric:** {len(profile['numeric_cols'])} | ") md.append(f"**Categorical:** {len(profile['categorical_cols'])} | ") md.append(f"**DateTime:** {len(profile['datetime_cols'])}") md.append("") md.append("### 🔍 Detailed Column Analysis") md.append("| Column | Type | Nulls | Null % | Unique | Completeness | Issues |") md.append("|--------|------|-------|---------|--------|--------------|--------|") for col in profile["dtypes"].keys(): dtype = profile["dtypes"][col] nulls = profile["nulls"].get(col, 0) null_pct = profile["null_pct"].get(col, 0) uniq = profile["unique"].get(col, 0) completeness = 1 - null_pct issues = [] if null_pct > 0.5: issues.append("🔴 High nulls") elif null_pct > 0.2: issues.append("🟡 Medium nulls") if col in profile["high_cardinality"]: issues.append("🔵 High cardinality") if col in profile["numeric_cols"]: stats = profile["quantitative_stats"].get(col, {}) outliers = stats.get("outliers", 0) if outliers > 0: issues.append("⚫ Outliers") issues_str = ", ".join(issues) if issues else "✅ Good" md.append(f"| {col} | {dtype} | {nulls} | {null_pct:.2%} | {uniq} | {completeness:.2%} | {issues_str} |") md.append("") if profile["quantitative_stats"]: md.append("### 📈 Quantitative Columns Analysis") md.append("| Column | Mean | Std | Min | Max | Skew | Outliers | Zeros |") md.append("|--------|------|-----|-----|-----|------|----------|-------|") for col, stats in profile["quantitative_stats"].items(): md.append(f"| {col} | {stats['mean']:.2f} | {stats['std']:.2f} | {stats['min']:.2f} | {stats['max']:.2f} | {stats['skew']:.2f} | {stats['outliers']} | {stats['zeros']} |") md.append("") if profile["qualitative_stats"]: md.append("### 📊 Qualitative Columns Analysis") md.append("| Column | Top Value | Top Freq | Top % | Unique | Entropy |") md.append("|--------|-----------|----------|-------|--------|---------|") for col, stats in profile["qualitative_stats"].items(): top_value = str(stats["top_value"])[:20] + "..." if len(str(stats["top_value"])) > 20 else str(stats["top_value"]) md.append(f"| {col} | {top_value} | {stats['top_freq']} | {stats['top_freq_pct']:.2%} | {stats['unique_values']} | {stats['entropy']:.2f} |") md.append("") if profile["high_cardinality"]: md.append("### ⚠️ High Cardinality Columns") md.append("The following columns have high cardinality (may impact modeling):") for col in profile["high_cardinality"]: md.append(f"- **{col}**: {profile['unique'][col]} unique values") md.append("") if profile["correlation_matrix"]: md.append("### 🔗 Correlation Highlights") corr_matrix = profile["correlation_matrix"] numeric_cols = list(corr_matrix.keys()) strong_corrs = [] for i, col1 in enumerate(numeric_cols): for j, col2 in enumerate(numeric_cols): if i < j: corr = abs(corr_matrix[col1][col2]) if corr > 0.7: strong_corrs.append((col1, col2, corr_matrix[col1][col2])) if strong_corrs: md.append("**Strong Correlations (|r| > 0.7):**") for col1, col2, corr in sorted(strong_corrs, key=lambda x: abs(x[2]), reverse=True): md.append(f"- {col1} ↔ {col2}: {corr:.3f}") else: md.append("No strong correlations found among numeric columns.") md.append("") return "\n".join(md) def create_distribution_plots(df: pd.DataFrame, profile: Dict[str, Any]): numeric_cols = profile.get("numeric_cols", []) categorical_cols = profile.get("categorical_cols", []) plots = {} for col in numeric_cols[:4]: try: fig = px.histogram(df, x=col, title=f"Distribution of {col}", marginal="box", nbins=50) plots[f"num_{col}"] = fig except Exception as e: print(f"Plot error for {col}: {e}") for col in categorical_cols[:3]: try: value_counts = df[col].value_counts().head(10) fig = px.bar(x=value_counts.index, y=value_counts.values, title=f"Top 10 Values in {col}") fig.update_layout(xaxis_title=col, yaxis_title="Count") plots[f"cat_{col}"] = fig except Exception as e: print(f"Plot error for {col}: {e}") return plots def create_correlation_plot(df: pd.DataFrame, profile: Dict[str, Any]): numeric_cols = profile.get("numeric_cols", []) if len(numeric_cols) < 2: return None try: corr_matrix = df[numeric_cols].corr() fig = px.imshow(corr_matrix, title="Correlation Matrix", color_continuous_scale="RdBu_r", aspect="auto") return fig except Exception as e: print(f"Correlation plot error: {e}") return None # ----------------------------- # Data cleaning & feature engineering helpers # ----------------------------- def drop_high_cardinality(df: pd.DataFrame, threshold_count=HIGH_CARD_THRESHOLD_COUNT, threshold_ratio=HIGH_CARD_THRESHOLD_RATIO): n = len(df) cols_to_drop = [] for c in df.columns: if df[c].nunique(dropna=False) > threshold_count or (df[c].nunique(dropna=False)/max(1,n) > threshold_ratio): if df[c].dtype == "object" or str(df[c].dtype).startswith("category"): cols_to_drop.append(c) return df.drop(columns=cols_to_drop, errors='ignore'), cols_to_drop def impute_df(df: pd.DataFrame, numeric_strategy="mean", categorical_strategy="most_frequent", fill_value: Optional[str]=None): df = df.copy() num_cols = df.select_dtypes(include=[np.number]).columns.tolist() cat_cols = df.select_dtypes(include=["object", "category"]).columns.tolist() if num_cols: imp = SimpleImputer(strategy=numeric_strategy) df[num_cols] = imp.fit_transform(df[num_cols]) if cat_cols: if categorical_strategy == "constant" and fill_value is not None: imp2 = SimpleImputer(strategy="constant", fill_value=fill_value) else: imp2 = SimpleImputer(strategy=categorical_strategy) df[cat_cols] = imp2.fit_transform(df[cat_cols]) return df def treat_outliers_iqr(df: pd.DataFrame, cols: List[str], method="cap"): df = df.copy() for c in cols: if c not in df.columns: continue if not np.issubdtype(df[c].dtype, np.number): continue q1 = df[c].quantile(0.25) q3 = df[c].quantile(0.75) iqr = q3 - q1 lower = q1 - 1.5 * iqr upper = q3 + 1.5 * iqr if method == "remove": df = df[(df[c] >= lower) & (df[c] <= upper)] elif method == "cap": df[c] = np.where(df[c] < lower, lower, df[c]) df[c] = np.where(df[c] > upper, upper, df[c]) return df def parse_dates(df: pd.DataFrame, col: str, fmt: Optional[str]=None): df = df.copy() try: if fmt: df[col] = pd.to_datetime(df[col], format=fmt, errors="coerce") else: df[col] = pd.to_datetime(df[col], errors="coerce", infer_datetime_format=True) except Exception as e: print("parse_dates", e) return df def text_clean(df: pd.DataFrame, cols: List[str], lower=True, strip=True): df = df.copy() for c in cols: if c not in df.columns: continue df[c] = df[c].astype(str) if strip: df[c] = df[c].str.strip() if lower: df[c] = df[c].str.lower() return df def transform_cols(df: pd.DataFrame, cols: List[str], method="log"): df = df.copy() for c in cols: if c in df.columns and np.issubdtype(df[c].dtype, np.number): if method == "log": df[c] = df[c].apply(lambda x: np.log(x) if x>0 else x) elif method == "sqrt": df[c] = df[c].apply(lambda x: np.sqrt(x) if x>=0 else x) return df # ----------------------------- # Enhanced Data Preparation Functions # ----------------------------- def update_column_lists(df): def get_columns_safe(df): if df is None: return [] cols = list(df.columns) if hasattr(df.columns, "levels") and hasattr(df.columns, "names"): try: cols = ['_'.join(map(str, c)) if isinstance(c, (list, tuple)) else str(c) for c in cols] except Exception: cols = [str(c) for c in cols] else: cols = [str(c) for c in cols] return cols cols = get_columns_safe(df) single_value = cols[0] if cols else None multi_value = cols if cols else [] return ( gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=single_value), gr.update(choices=cols, value=single_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=single_value), gr.update(choices=cols, value=single_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=multi_value), gr.update(choices=cols, value=single_value) ) def apply_column_operations(df, selected_cols, rename_mapping, dtype_conversions): df = df.copy() if selected_cols: df = df[selected_cols] for old_name, new_name in rename_mapping.items(): if old_name in df.columns: df = df.rename(columns={old_name: new_name}) for col, target_dtype in dtype_conversions.items(): if col in df.columns: try: if target_dtype == "numeric": df[col] = pd.to_numeric(df[col], errors='coerce') elif target_dtype == "integer": df[col] = pd.to_numeric(df[col], errors='coerce').astype('Int64') elif target_dtype == "float": df[col] = pd.to_numeric(df[col], errors='coerce').astype(float) elif target_dtype == "datetime": df[col] = pd.to_datetime(df[col], errors='coerce') elif target_dtype == "category": df[col] = df[col].astype('category') elif target_dtype == "boolean": df[col] = df[col].astype(bool) except Exception as e: print(f"Error converting {col} to {target_dtype}: {e}") return df def detect_high_cardinality_cols(df, threshold): high_card_cols = [] for col in df.columns: if df[col].dtype in ['object', 'category']: unique_count = df[col].nunique() if unique_count > threshold: high_card_cols.append((col, unique_count)) return high_card_cols def advanced_imputation(df, num_cols, num_method, num_custom, cat_cols, cat_method, cat_custom): df = df.copy() if num_cols: for col in num_cols: if col in df.columns: if num_method == "Mean": df[col].fillna(df[col].mean(), inplace=True) elif num_method == "Median": df[col].fillna(df[col].median(), inplace=True) elif num_method == "Mode": df[col].fillna(df[col].mode()[0] if not df[col].mode().empty else 0, inplace=True) elif num_method == "Zero": df[col].fillna(0, inplace=True) elif num_method == "Custom Value" and num_custom: try: custom_val = float(num_custom) df[col].fillna(custom_val, inplace=True) except ValueError: df[col].fillna(0, inplace=True) elif num_method in ["Forward Fill", "LOCF"]: df[col].fillna(method='ffill', inplace=True) elif num_method in ["Backward Fill", "NOCB"]: df[col].fillna(method='bfill', inplace=True) elif num_method == "Linear Interpolation": df[col].interpolate(method='linear', inplace=True) if cat_cols: for col in cat_cols: if col in df.columns: if cat_method == "Mode": df[col].fillna(df[col].mode()[0] if not df[col].mode().empty else "Unknown", inplace=True) elif cat_method == "Most Frequent": df[col].fillna(df[col].value_counts().index[0] if len(df[col].value_counts()) > 0 else "Unknown", inplace=True) elif cat_method == "Arbitrary ('Unknown')": df[col].fillna("Unknown", inplace=True) elif cat_method == "Constant Value" and cat_custom: df[col].fillna(cat_custom, inplace=True) return df def apply_text_operations(df, text_cols, operations): df = df.copy() for col in text_cols: if col in df.columns: df[col] = df[col].astype(str) for op in operations: if op == "Remove leading/trailing whitespace": df[col] = df[col].str.strip() elif op == "Convert to lowercase": df[col] = df[col].str.lower() elif op == "Convert to uppercase": df[col] = df[col].str.upper() elif op == "Remove special characters": df[col] = df[col].str.replace(r'[^\w\s]', '', regex=True) elif op == "Remove numbers": df[col] = df[col].str.replace(r'\d+', '', regex=True) elif op == "Remove extra spaces": df[col] = df[col].str.replace(r'\s+', ' ', regex=True) return df def split_text_column(df, col, delimiter): if col not in df.columns: return df, "Column not found" df = df.copy() try: split_df = df[col].str.split(delimiter, expand=True) new_col_names = [f"{col}_{i+1}" for i in range(split_df.shape[1])] split_df.columns = new_col_names df = pd.concat([df, split_df], axis=1) return df, f"Successfully split {col} into {len(new_col_names)} columns" except Exception as e: return df, f"Error splitting column: {str(e)}" def create_formula_column(df, formula, new_col_name): df = df.copy() try: if "=" in formula: formula = formula.split("=")[1].strip() formula = formula.replace("CURRENT_YEAR", str(pd.Timestamp.now().year)) try: df[new_col_name] = df.eval(formula) return df, f"Successfully created column '{new_col_name}'" except: df[new_col_name] = formula return df, f"Created column '{new_col_name}' with constant value" except Exception as e: return df, f"Error creating formula column: {str(e)}" # ----------------------------- # Visualization NLP # ----------------------------- def nlp_to_chart_instruction(query: str, df: pd.DataFrame): q = query.lower() numeric = df.select_dtypes(include=[np.number]).columns.tolist() categorical = df.select_dtypes(include=["object", "category"]).columns.tolist() tokens = q.split() if "hist" in q or "histogram" in q or "distribution" in q: for c in numeric: if c.lower() in q: return ("hist", [c]) if numeric: return ("hist", [numeric[0]]) if "scatter" in q or "vs" in q or "versus" in q: for c in numeric: if c.lower() in q: x = c for d in numeric: if d!=c and d.lower() in q: return ("scatter", [x, d]) if len(numeric)>1: return ("scatter", [numeric[0], numeric[1]]) if len(numeric)>=2: return ("scatter", [numeric[0], numeric[1]]) if "bar" in q or "count" in q or "counts" in q or "value counts" in q: for c in categorical: if c.lower() in q: return ("bar", [c]) if categorical: return ("bar", [categorical[0]]) if "box" in q or "outlier" in q: for c in numeric: if c.lower() in q: return ("box", [c]) if numeric: return ("box", [numeric[0]]) return ("table", []) def render_chart_from_instruction(instruction: Tuple[str, List[str]], df: pd.DataFrame): typ, cols = instruction if typ == "hist": c = cols[0] fig = px.histogram(df, x=c, title=f"Distribution of {c}") return fig if typ == "scatter": x, y = cols[:2] fig = px.scatter(df, x=x, y=y, title=f"{y} vs {x}") return fig if typ == "bar": c = cols[0] vc = df[c].value_counts().reset_index() vc.columns = [c, "count"] fig = px.bar(vc, x=c, y="count", title=f"Counts of {c}") return fig if typ == "box": c = cols[0] fig = px.box(df, y=c, title=f"Box plot of {c}") return fig return None # ----------------------------- # Modeling - Regression & Classification # ----------------------------- REGRESSION_MODELS = { "Linear Regression": LinearRegression, "Ridge Regression": Ridge, "Lasso Regression": Lasso, "Decision Tree Regressor": DecisionTreeRegressor, "Random Forest Regressor": RandomForestRegressor, "Gradient Boosting Regressor": GradientBoostingRegressor, "Support Vector Regressor": SVR, "K-Neighbors Regressor": KNeighborsRegressor } CLASSIFICATION_MODELS = { "Logistic Regression": LogisticRegression, "Decision Tree Classifier": DecisionTreeClassifier, "Random Forest Classifier": RandomForestClassifier, "Gradient Boosting Classifier": GradientBoostingClassifier, "Support Vector Classifier": SVC, "Gaussian Naive Bayes": GaussianNB, "K-Neighbors Classifier": KNeighborsClassifier } # Model hyperparameter grids for tuning MODEL_PARAM_GRIDS = { "Random Forest Regressor": { 'model__n_estimators': [50, 100, 200], 'model__max_depth': [None, 10, 20], 'model__min_samples_split': [2, 5, 10] }, "Random Forest Classifier": { 'model__n_estimators': [50, 100, 200], 'model__max_depth': [None, 10, 20], 'model__min_samples_split': [2, 5, 10] }, "Gradient Boosting Regressor": { 'model__n_estimators': [50, 100], 'model__learning_rate': [0.01, 0.1, 0.2], 'model__max_depth': [3, 5, 7] }, "Gradient Boosting Classifier": { 'model__n_estimators': [50, 100], 'model__learning_rate': [0.01, 0.1, 0.2], 'model__max_depth': [3, 5, 7] }, "Logistic Regression": { 'model__C': [0.1, 1, 10], 'model__penalty': ['l2', 'none'] } } def prepare_features_targets(df: pd.DataFrame, target: str, drop_cols: List[str]=None, drop_high_card=True): df = df.copy() if drop_cols: df = df.drop(columns=drop_cols, errors='ignore') if drop_high_card: df, dropped = drop_high_cardinality(df) if target not in df.columns: raise ValueError("Target column not in dataframe") X = df.drop(columns=[target]) y = df[target] # For classification, check if y needs encoding if y.dtype == 'object' or y.dtype.name == 'category': le = LabelEncoder() y = le.fit_transform(y) return X, y def auto_build_preprocessor(X: pd.DataFrame, scaler_choice: str="standard", onehot=True): num_cols = X.select_dtypes(include=[np.number]).columns.tolist() cat_cols = X.select_dtypes(include=["object", "category"]).columns.tolist() transformers = [] if num_cols: if scaler_choice == "standard": num_pipeline = Pipeline([("imputer", SimpleImputer(strategy="mean")), ("scaler", StandardScaler())]) elif scaler_choice == "minmax": num_pipeline = Pipeline([("imputer", SimpleImputer(strategy="mean")), ("scaler", MinMaxScaler())]) else: num_pipeline = Pipeline([("imputer", SimpleImputer(strategy="mean"))]) transformers.append(("num", num_pipeline, num_cols)) if cat_cols and onehot: cat_pipeline = Pipeline([("imputer", SimpleImputer(strategy="most_frequent")), ("onehot", OneHotEncoder(drop='first', sparse=False, handle_unknown='ignore'))]) transformers.append(("cat", cat_pipeline, cat_cols)) preprocessor = ColumnTransformer(transformers=transformers, remainder='drop') return preprocessor def run_regression_classification(task: str, model_name: str, X, y, scaler_choice="standard", test_size=0.2, random_state=42, do_rfecv=False, do_hyperparameter_tuning=False): if task == "regression": ModelClass = REGRESSION_MODELS.get(model_name) else: ModelClass = CLASSIFICATION_MODELS.get(model_name) if ModelClass is None: raise ValueError(f"Model {model_name} not found for task {task}") X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=random_state) preprocessor = auto_build_preprocessor(X_train, scaler_choice=scaler_choice, onehot=True) model = ModelClass() # Create pipeline pipe = Pipeline([("pre", preprocessor), ("model", model)]) # Hyperparameter tuning if requested if do_hyperparameter_tuning and model_name in MODEL_PARAM_GRIDS: param_grid = MODEL_PARAM_GRIDS[model_name] gs = GridSearchCV(pipe, param_grid, cv=3, n_jobs=1, scoring='r2' if task=='regression' else 'accuracy') gs.fit(X_train, y_train) trained = gs.best_estimator_ y_pred = trained.predict(X_test) best_params = gs.best_params_ else: pipe.fit(X_train, y_train) trained = pipe y_pred = pipe.predict(X_test) best_params = None results = {} # Calculate metrics if task == "regression": results["mse"] = mean_squared_error(y_test, y_pred) results["rmse"] = math.sqrt(results["mse"]) results["mae"] = mean_absolute_error(y_test, y_pred) results["r2"] = r2_score(y_test, y_pred) # Cross-validation scores cv_scores = cross_val_score(pipe, X, y, cv=5, scoring='r2') results["cv_r2_mean"] = cv_scores.mean() results["cv_r2_std"] = cv_scores.std() # Residuals resid = y_test - y_pred results["residuals"] = resid else: # classification results["accuracy"] = accuracy_score(y_test, y_pred) results["report"] = classification_report(y_test, y_pred, output_dict=True) results["confusion_matrix"] = confusion_matrix(y_test, y_pred).tolist() # Cross-validation scores cv_scores = cross_val_score(pipe, X, y, cv=5, scoring='accuracy') results["cv_accuracy_mean"] = cv_scores.mean() results["cv_accuracy_std"] = cv_scores.std() # ROC AUC if applicable try: if hasattr(trained.named_steps['model'], "predict_proba"): y_score = trained.predict_proba(X_test)[:,1] results["roc_auc"] = roc_auc_score(y_test, y_score) fpr, tpr, _ = roc_curve(y_test, y_score) results["roc_curve"] = (fpr.tolist(), tpr.tolist()) except: pass # Feature importance feat_importance = None try: model_obj = trained.named_steps['model'] if hasattr(model_obj, "feature_importances_"): feature_names = [] pre = trained.named_steps['pre'] if hasattr(pre, 'transformers_'): for name, trans, cols in pre.transformers_: if name == "num": feature_names += cols elif name == "cat": ohe = trans.named_steps['onehot'] if hasattr(ohe, 'get_feature_names_out'): names = list(ohe.get_feature_names_out(cols)) feature_names += names else: feature_names += cols importances = model_obj.feature_importances_ feat_importance = list(zip(feature_names, importances)) feat_importance.sort(key=lambda x: x[1], reverse=True) except: pass results["feature_importance"] = feat_importance results["trained_model"] = trained results["best_params"] = best_params results["y_test"] = y_test results["y_pred"] = y_pred return results # ----------------------------- # Clustering Functions # ----------------------------- def run_clustering(clustering_method: str, X, n_clusters=3, eps=0.5, min_samples=5): X_scaled = StandardScaler().fit_transform(X.select_dtypes(include=[np.number])) if clustering_method == "K-Means": model = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) labels = model.fit_predict(X_scaled) centroids = model.cluster_centers_ # Calculate metrics try: silhouette = silhouette_score(X_scaled, labels) davies_bouldin = davies_bouldin_score(X_scaled, labels) calinski_harabasz = calinski_harabasz_score(X_scaled, labels) except: silhouette = None davies_bouldin = None calinski_harabasz = None results = { "method": "K-Means", "labels": labels, "centroids": centroids, "inertia": model.inertia_, "silhouette_score": silhouette, "davies_bouldin_score": davies_bouldin, "calinski_harabasz_score": calinski_harabasz, "model": model } elif clustering_method == "DBSCAN": model = DBSCAN(eps=eps, min_samples=min_samples) labels = model.fit_predict(X_scaled) # Calculate metrics (only if more than 1 cluster found) n_clusters_found = len(set(labels)) - (1 if -1 in labels else 0) if n_clusters_found > 1: try: silhouette = silhouette_score(X_scaled, labels) davies_bouldin = davies_bouldin_score(X_scaled, labels) calinski_harabasz = calinski_harabasz_score(X_scaled, labels) except: silhouette = None davies_bouldin = None calinski_harabasz = None else: silhouette = None davies_bouldin = None calinski_harabasz = None results = { "method": "DBSCAN", "labels": labels, "core_sample_indices": model.core_sample_indices_, "n_clusters_found": n_clusters_found, "silhouette_score": silhouette, "davies_bouldin_score": davies_bouldin, "calinski_harabasz_score": calinski_harabasz, "model": model } return results # ----------------------------- # Rule-Based (Apriori) Functions # ----------------------------- def prepare_transaction_data(df, transaction_col=None, threshold=0.5): """ Prepare data for association rule mining. For categorical columns, create binary indicators. For numeric columns, bin them into categories. """ df_prep = df.copy() # Convert numeric columns to categorical by binning for col in df_prep.select_dtypes(include=[np.number]).columns: if len(df_prep[col].unique()) > 10: # Only bin if many unique values # Create 5 equal-width bins df_prep[col] = pd.qcut(df_prep[col], q=5, duplicates='drop') df_prep[col] = df_prep[col].astype(str) # Convert all to string for consistency for col in df_prep.columns: df_prep[col] = df_prep[col].astype(str) # If transaction column is specified, use it if transaction_col and transaction_col in df_prep.columns: transactions = [] for items in df_prep[transaction_col]: if pd.isna(items): transactions.append([]) else: # Split by common delimiters if isinstance(items, str): split_items = [item.strip() for item in str(items).replace(';', ',').split(',')] transactions.append([f"{transaction_col}={item}" for item in split_items if item]) else: transactions.append([f"{transaction_col}={items}"]) else: # Create transactions from entire dataframe (one-hot like) transactions = [] for _, row in df_prep.iterrows(): transaction = [] for col in df_prep.columns: if col == transaction_col: continue value = str(row[col]) if value and value.lower() not in ['nan', 'null', 'none', '']: transaction.append(f"{col}={value}") if transaction: transactions.append(transaction) return transactions def simple_apriori(transactions, min_support=0.1): """ A simplified Apriori algorithm implementation. Returns frequent itemsets and their support. """ from collections import defaultdict import itertools # Count item frequencies item_counts = defaultdict(int) for transaction in transactions: for item in set(transaction): # Use set to avoid duplicate items in same transaction item_counts[item] += 1 total_transactions = len(transactions) # Get frequent 1-itemsets frequent_1_itemsets = {} for item, count in item_counts.items(): support = count / total_transactions if support >= min_support: frequent_1_itemsets[frozenset([item])] = support frequent_itemsets = {1: frequent_1_itemsets} # Generate larger itemsets (up to 3-itemsets for simplicity) k = 2 while True: candidate_itemsets = set() prev_itemsets = list(frequent_itemsets[k-1].keys()) # Generate candidates for i in range(len(prev_itemsets)): for j in range(i+1, len(prev_itemsets)): itemset1 = prev_itemsets[i] itemset2 = prev_itemsets[j] union_set = itemset1.union(itemset2) if len(union_set) == k: candidate_itemsets.add(union_set) if not candidate_itemsets: break # Count support for candidates candidate_counts = defaultdict(int) for transaction in transactions: trans_set = set(transaction) for candidate in candidate_itemsets: if candidate.issubset(trans_set): candidate_counts[candidate] += 1 # Filter by minimum support frequent_k_itemsets = {} for itemset, count in candidate_counts.items(): support = count / total_transactions if support >= min_support: frequent_k_itemsets[itemset] = support if not frequent_k_itemsets: break frequent_itemsets[k] = frequent_k_itemsets k += 1 if k > 3: # Limit to 3-itemsets for performance break return frequent_itemsets def generate_association_rules(frequent_itemsets, min_confidence=0.5, min_lift=1.0): """Generate association rules from frequent itemsets.""" rules = [] for k, itemsets in frequent_itemsets.items(): if k < 2: continue for itemset, support_itemset in itemsets.items(): itemset_list = list(itemset) # Generate all non-empty proper subsets for i in range(1, k): for antecedent in itertools.combinations(itemset_list, i): antecedent_set = frozenset(antecedent) consequent_set = itemset - antecedent_set if not consequent_set: continue # Find support of antecedent antecedent_support = None for size in range(1, k): if size in frequent_itemsets and antecedent_set in frequent_itemsets[size]: antecedent_support = frequent_itemsets[size][antecedent_set] break if antecedent_support is None or antecedent_support == 0: continue # Calculate confidence and lift confidence = support_itemset / antecedent_support consequent_support = None # Find support of consequent for size in range(1, k): if size in frequent_itemsets and consequent_set in frequent_itemsets[size]: consequent_support = frequent_itemsets[size][consequent_set] break if consequent_support is None or consequent_support == 0: continue lift = confidence / consequent_support if confidence >= min_confidence and lift >= min_lift: rules.append({ 'antecedents': set(antecedent), 'consequents': set(consequent_set), 'support': support_itemset, 'confidence': confidence, 'lift': lift }) # Sort by confidence, then lift rules.sort(key=lambda x: (x['confidence'], x['lift']), reverse=True) return rules def run_association_mining(transactions, min_support=0.1, min_confidence=0.5, min_lift=1.0): """Run association rule mining using our simple implementation.""" frequent_itemsets = simple_apriori(transactions, min_support) rules = generate_association_rules(frequent_itemsets, min_confidence, min_lift) return frequent_itemsets, rules def run_frequency_based_rules(df, categorical_cols=None, min_frequency=0.1, min_cooccurrence=0.5): """ An alternative rule discovery method based on frequency and co-occurrence. Simpler but effective for many use cases. """ if categorical_cols is None: categorical_cols = df.select_dtypes(include=['object', 'category']).columns.tolist() rules = [] for col1 in categorical_cols: for col2 in categorical_cols: if col1 == col2: continue # Calculate contingency table contingency = pd.crosstab(df[col1], df[col2], normalize='all') # Find strong associations for val1 in contingency.index: for val2 in contingency.columns: p_val1_val2 = contingency.loc[val1, val2] p_val1 = df[col1].value_counts(normalize=True).get(val1, 0) p_val2 = df[col2].value_counts(normalize=True).get(val2, 0) if p_val1 > 0 and p_val2 > 0 and p_val1_val2 > 0: confidence = p_val1_val2 / p_val1 lift = p_val1_val2 / (p_val1 * p_val2) if p_val1_val2 >= min_frequency and confidence >= min_cooccurrence: rules.append({ 'rule': f"If {col1} = {val1} then {col2} = {val2}", 'support': p_val1_val2, 'confidence': confidence, 'lift': lift, 'antecedent': f"{col1}={val1}", 'consequent': f"{col2}={val2}" }) # Sort by confidence and lift rules.sort(key=lambda x: (x['confidence'], x['lift']), reverse=True) return rules def run_apriori(df_encoded, min_support=0.1, min_confidence=0.5, min_lift=1.0, max_length=4): # Find frequent itemsets frequent_itemsets = apriori(df_encoded, min_support=min_support, use_colnames=True, max_len=max_length) if frequent_itemsets.empty: return None, None # Generate association rules rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=min_confidence) rules = rules[rules['lift'] >= min_lift] # Sort by confidence and lift rules = rules.sort_values(['confidence', 'lift'], ascending=[False, False]) return frequent_itemsets, rules def run_fp_growth(df_encoded, min_support=0.1, min_confidence=0.5, min_lift=1.0, max_length=4): # Find frequent itemsets using FP-Growth frequent_itemsets = fpgrowth(df_encoded, min_support=min_support, use_colnames=True, max_len=max_length) if frequent_itemsets.empty: return None, None # Generate association rules rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=min_confidence) rules = rules[rules['lift'] >= min_lift] # Sort by confidence and lift rules = rules.sort_values(['confidence', 'lift'], ascending=[False, False]) return frequent_itemsets, rules # ----------------------------- # Gradio UI with Enhanced Model Tab # ----------------------------- css = """ /* Vanta background container */ #vanta-bg { width: 100%; height: 380px; position: relative; overflow: hidden; border-radius: 12px; margin-bottom: 8px; } .login-card { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 420px; max-width: calc(100% - 24px); background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(245,245,255,0.95)); border-radius: 14px; box-shadow: 0 12px 36px rgba(0,0,0,0.18); padding: 22px; z-index: 999; border: 1px solid rgba(0,0,0,0.06); } .login-logo { display:flex; align-items:center; gap:12px; margin-bottom:8px; } .brand-title { font-weight:700; font-size:18px; color:#2b2b6b; } .btn-animate { transition: transform 0.12s ease-in-out, box-shadow 0.12s; } .btn-animate:active { transform: translateY(2px) scale(0.995); box-shadow: 0 6px 18px rgba(0,0,0,0.12) inset; } .app-desc { font-size: 13px; color: #444; margin-top: 8px; text-align: center; } #profile-report table { width: 100%; border-collapse: collapse; font-size: 14px; table-layout: fixed; } #profile-report th, #profile-report td { padding: 8px 12px; border: 1px solid #ddd; text-align: left; word-wrap: break-word; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #profile-report th { background-color: #f5f5f5; font-weight: 600; } #profile-report tr:nth-child(even) { background-color: #f9f9f9; } .markdown-body { max-width: 100% !important; } .gr-markdown { max-height: 70vh; overflow-y: auto; } .dataframe-container { max-height: 500px; overflow-y: auto; border: 1px solid #ddd; border-radius: 8px; padding: 10px; } .model-section { padding: 15px; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 15px; background-color: #f9f9f9; } .model-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin: 15px 0; } .metric-card { background: white; padding: 12px; border-radius: 6px; border-left: 4px solid #2b2b6b; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .metric-value { font-size: 24px; font-weight: bold; color: #2b2b6b; } .metric-label { font-size: 12px; color: #666; text-transform: uppercase; } """ vanta_html = """