Datavision / backend /utils /smart_column_detector.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
10.5 kB
"""
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