""" Smart Column Detector - AI-powered column mapping for any business data Enterprise-grade $500K product feature """ from typing import Dict, List, Optional, Tuple import pandas as pd import re from difflib import SequenceMatcher # Column type patterns with priority weights COLUMN_PATTERNS = { 'amount': { 'exact': ['amount', 'revenue', 'sales', 'total', 'price', 'value', 'cost', 'payment', 'income', 'profit'], 'contains': ['amount', 'revenue', 'sales', 'total', 'price', 'value', 'cost', 'usd', 'inr', 'eur', 'gbp', 'contract'], 'regex': [r'.*_amount$', r'.*_price$', r'.*_value$', r'.*_total$', r'.*_revenue$', r'annual.*value'], 'weight': 1.0 }, 'customer': { 'exact': ['customer', 'client', 'company', 'buyer', 'account', 'customer_name', 'client_name'], 'contains': ['customer', 'client', 'company', 'buyer', 'account', 'name'], 'regex': [r'.*customer.*', r'.*client.*', r'.*company.*name.*'], 'weight': 0.9 }, 'product': { 'exact': ['product', 'item', 'sku', 'service', 'goods', 'product_name', 'item_name'], 'contains': ['product', 'item', 'sku', 'service', 'goods', 'description'], 'regex': [r'.*product.*', r'.*item.*', r'.*service.*'], 'weight': 0.9 }, 'date': { 'exact': ['date', 'order_date', 'transaction_date', 'created_at', 'timestamp', 'created', 'ordered'], 'contains': ['date', 'timestamp', 'created', 'ordered', 'time'], 'regex': [r'.*date$', r'.*_at$', r'.*time.*'], 'weight': 0.8 }, 'quantity': { 'exact': ['quantity', 'qty', 'units', 'count', 'volume', 'num', 'number'], 'contains': ['quantity', 'qty', 'units', 'count', 'volume'], 'regex': [r'.*qty.*', r'.*quantity.*', r'.*count.*', r'.*units.*'], 'weight': 0.7 }, 'category': { 'exact': ['category', 'type', 'segment', 'industry', 'sector', 'group'], 'contains': ['category', 'type', 'segment', 'industry', 'sector'], 'regex': [r'.*category.*', r'.*segment.*', r'.*industry.*'], 'weight': 0.6 }, 'region': { 'exact': ['region', 'country', 'location', 'city', 'state', 'area', 'territory'], 'contains': ['region', 'country', 'location', 'city', 'state', 'geo'], 'regex': [r'.*region.*', r'.*country.*', r'.*location.*'], 'weight': 0.6 }, 'id': { 'exact': ['id', 'invoice_id', 'order_id', 'transaction_id', 'customer_id', 'product_id'], 'contains': ['_id', 'number', 'no.', 'num'], 'regex': [r'.*_id$', r'.*_no$', r'.*_number$'], 'weight': 0.5 } } # Currency symbols and patterns CURRENCY_INDICATORS = ['$', '€', '£', '₹', '¥', 'USD', 'EUR', 'GBP', 'INR', 'JPY'] def similarity_score(str1: str, str2: str) -> float: """Calculate string similarity using SequenceMatcher""" return SequenceMatcher(None, str1.lower(), str2.lower()).ratio() def detect_column_type(column_name: str, sample_values: List) -> Tuple[str, float]: """ Detect the semantic type of a column based on name and sample values Returns: (column_type, confidence_score) """ col_lower = column_name.lower().strip().replace(' ', '_') best_match = ('unknown', 0.0) for col_type, patterns in COLUMN_PATTERNS.items(): score = 0.0 # Exact match (highest priority) if col_lower in patterns['exact']: score = 1.0 * patterns['weight'] # Contains match elif any(p in col_lower for p in patterns['contains']): score = 0.8 * patterns['weight'] # Regex match elif any(re.match(r, col_lower) for r in patterns['regex']): score = 0.7 * patterns['weight'] # Fuzzy similarity match else: max_similarity = max(similarity_score(col_lower, p) for p in patterns['exact']) if max_similarity > 0.6: score = max_similarity * 0.6 * patterns['weight'] # Boost score based on value analysis if sample_values and score > 0: value_boost = analyze_values_for_type(col_type, sample_values) score = min(1.0, score + value_boost) if score > best_match[1]: best_match = (col_type, score) return best_match def analyze_values_for_type(col_type: str, sample_values: List) -> float: """Analyze sample values to boost confidence for a column type""" if not sample_values: return 0.0 # Filter out None/NaN values valid_values = [v for v in sample_values if v is not None and str(v).strip() != '' and str(v).lower() != 'nan'] if not valid_values: return 0.0 if col_type == 'amount': # Check for numeric values or currency symbols numeric_count = 0 currency_count = 0 for v in valid_values[:20]: str_v = str(v) if any(c in str_v for c in CURRENCY_INDICATORS): currency_count += 1 try: cleaned = re.sub(r'[^\d.-]', '', str_v) if cleaned and float(cleaned) > 0: numeric_count += 1 except: pass if currency_count > len(valid_values[:20]) * 0.3: return 0.3 if numeric_count > len(valid_values[:20]) * 0.8: return 0.15 elif col_type == 'date': # Check for date-like values date_count = 0 for v in valid_values[:20]: str_v = str(v) if re.match(r'\d{4}[-/]\d{1,2}[-/]\d{1,2}', str_v) or \ re.match(r'\d{1,2}[-/]\d{1,2}[-/]\d{2,4}', str_v): date_count += 1 if date_count > len(valid_values[:20]) * 0.5: return 0.2 elif col_type == 'quantity': # Check for small integers int_count = 0 for v in valid_values[:20]: try: num = float(v) if num == int(num) and 0 < num < 10000: int_count += 1 except: pass if int_count > len(valid_values[:20]) * 0.8: return 0.1 return 0.0 def smart_detect_columns(df: pd.DataFrame) -> Dict[str, str]: """ Intelligently detect and map columns to standard business fields Returns: {'standard_field': 'actual_column_name', ...} """ if df is None or df.empty: return {} results = {} column_scores = {} # First pass: Score all columns for all types for col in df.columns: sample_values = df[col].head(20).tolist() col_type, confidence = detect_column_type(col, sample_values) if confidence > 0.3: # Minimum confidence threshold if col_type not in column_scores: column_scores[col_type] = [] column_scores[col_type].append((col, confidence)) # Second pass: Assign best match for each type (avoid duplicates) used_columns = set() for col_type in ['amount', 'customer', 'product', 'date', 'quantity', 'category', 'region']: if col_type in column_scores: # Sort by confidence, highest first candidates = sorted(column_scores[col_type], key=lambda x: x[1], reverse=True) for col, confidence in candidates: if col not in used_columns: results[col_type] = col used_columns.add(col) break print(f"📊 Smart Column Detection Results:") for field, col in results.items(): print(f" {field}: {col}") return results def get_data_profile(df: pd.DataFrame) -> Dict: """ Generate a comprehensive data profile for the uploaded data Returns profile with detected columns, data quality, and recommendations """ if df is None or df.empty: return {'error': 'No data available', 'has_data': False} column_mapping = smart_detect_columns(df) # Determine data type has_amount = 'amount' in column_mapping has_customer = 'customer' in column_mapping has_product = 'product' in column_mapping has_date = 'date' in column_mapping has_quantity = 'quantity' in column_mapping # Classify data type if has_amount and (has_customer or has_product): data_type = 'sales_data' analysis_mode = 'revenue' elif has_quantity and has_product: data_type = 'inventory_data' analysis_mode = 'quantity' elif has_customer and not has_amount: data_type = 'customer_data' analysis_mode = 'count' elif has_product and not has_amount: data_type = 'product_catalog' analysis_mode = 'count' else: data_type = 'general_data' analysis_mode = 'count' # Calculate data quality score total_cells = df.shape[0] * df.shape[1] null_cells = df.isnull().sum().sum() quality_score = max(0, min(100, int((1 - null_cells / total_cells) * 100))) # Generate recommendations recommendations = [] if not has_amount: recommendations.append("Add a revenue/amount column for financial analysis") if not has_date: recommendations.append("Add date column to enable trend analysis") if not has_customer and not has_product: recommendations.append("Add customer or product columns for segmentation") return { 'has_data': True, 'row_count': len(df), 'column_count': len(df.columns), 'columns': list(df.columns), 'detected_mapping': column_mapping, 'data_type': data_type, 'analysis_mode': analysis_mode, 'quality_score': quality_score, 'recommendations': recommendations, 'has_amount': has_amount, 'has_customer': has_customer, 'has_product': has_product, 'has_date': has_date, 'has_quantity': has_quantity } def apply_column_mapping(df: pd.DataFrame, mapping: Dict[str, str]) -> pd.DataFrame: """ Create a standardized DataFrame with mapped columns """ result_df = pd.DataFrame() for standard_name, actual_column in mapping.items(): if actual_column in df.columns: result_df[standard_name] = df[actual_column] # Keep original columns too for reference for col in df.columns: if col not in result_df.columns: result_df[f'_original_{col}'] = df[col] return result_df