institutional-trader/backend/python_service/utils/validation.py

194 lines
6.2 KiB
Python

"""
Validation utilities for comparing Python vs SQL output
"""
import pandas as pd
from typing import Dict, List, Optional, Tuple
from utils.logger import logger
def compare_outputs(
python_output: List[Dict],
sql_output: List[Dict],
key_columns: Optional[List[str]] = None
) -> Dict:
"""
Compare Python output with SQL output
Args:
python_output: List of dicts from Python service
sql_output: List of dicts from SQL query
key_columns: Columns to use for matching rows
Returns:
Comparison report dict
"""
python_df = pd.DataFrame(python_output)
sql_df = pd.DataFrame(sql_output)
report = {
'python_count': len(python_output),
'sql_count': len(sql_output),
'count_match': len(python_output) == len(sql_output),
'differences': [],
'missing_in_python': [],
'missing_in_sql': [],
'column_differences': {}
}
if python_df.empty and sql_df.empty:
report['status'] = 'both_empty'
return report
if python_df.empty:
report['status'] = 'python_empty'
return report
if sql_df.empty:
report['status'] = 'sql_empty'
return report
# Compare columns
python_cols = set(python_df.columns)
sql_cols = set(sql_df.columns)
missing_in_python = sql_cols - python_cols
missing_in_sql = python_cols - sql_cols
if missing_in_python:
report['missing_in_python'] = list(missing_in_python)
if missing_in_sql:
report['missing_in_sql'] = list(missing_in_sql)
# Compare common columns
common_cols = python_cols & sql_cols
if key_columns:
# Match rows by key columns
for key_col in key_columns:
if key_col not in common_cols:
logger.warning(f"Key column {key_col} not found in both outputs")
key_columns = None
break
if key_columns:
# Merge on key columns
python_key = python_df[key_columns].copy()
sql_key = sql_df[key_columns].copy()
merged = python_key.merge(
sql_key,
on=key_columns,
how='outer',
indicator=True
)
report['only_in_python'] = len(merged[merged['_merge'] == 'left_only'])
report['only_in_sql'] = len(merged[merged['_merge'] == 'right_only'])
report['in_both'] = len(merged[merged['_merge'] == 'both'])
else:
# Compare by index (assume same order)
min_len = min(len(python_df), len(sql_df))
report['compared_rows'] = min_len
# Compare values in common columns
numeric_cols = []
text_cols = []
for col in common_cols:
if python_df[col].dtype in ['int64', 'float64'] or sql_df[col].dtype in ['int64', 'float64']:
numeric_cols.append(col)
else:
text_cols.append(col)
differences = []
# Compare numeric columns
for col in numeric_cols:
if col in python_df.columns and col in sql_df.columns:
python_vals = python_df[col].fillna(0)
sql_vals = sql_df[col].fillna(0)
# Handle different lengths
min_len = min(len(python_vals), len(sql_vals))
python_vals = python_vals[:min_len]
sql_vals = sql_vals[:min_len]
diff = (python_vals - sql_vals).abs()
max_diff = diff.max()
mean_diff = diff.mean()
if max_diff > 0.01: # Tolerance for floating point
differences.append({
'column': col,
'type': 'numeric',
'max_difference': float(max_diff),
'mean_difference': float(mean_diff),
'different_count': int((diff > 0.01).sum())
})
# Compare text columns
for col in text_cols:
if col in python_df.columns and col in sql_df.columns:
python_vals = python_df[col].fillna('').astype(str)
sql_vals = sql_df[col].fillna('').astype(str)
min_len = min(len(python_vals), len(sql_vals))
python_vals = python_vals[:min_len]
sql_vals = sql_vals[:min_len]
different = (python_vals != sql_vals).sum()
if different > 0:
differences.append({
'column': col,
'type': 'text',
'different_count': int(different),
'total_compared': int(min_len)
})
report['differences'] = differences
report['status'] = 'compared'
return report
def print_comparison_report(report: Dict):
"""Print a formatted comparison report"""
print("\n" + "="*60)
print("PYTHON vs SQL OUTPUT COMPARISON")
print("="*60)
print(f"\nRow Counts:")
print(f" Python: {report['python_count']}")
print(f" SQL: {report['sql_count']}")
print(f" Match: {'' if report['count_match'] else ''}")
if report.get('only_in_python'):
print(f"\n Only in Python: {report['only_in_python']}")
if report.get('only_in_sql'):
print(f" Only in SQL: {report['only_in_sql']}")
if report.get('in_both'):
print(f" In both: {report['in_both']}")
if report.get('missing_in_python'):
print(f"\n⚠️ Columns missing in Python: {report['missing_in_python']}")
if report.get('missing_in_sql'):
print(f"⚠️ Columns missing in SQL: {report['missing_in_sql']}")
if report.get('differences'):
print(f"\n📊 Column Differences:")
for diff in report['differences']:
print(f"\n Column: {diff['column']} ({diff['type']})")
if diff['type'] == 'numeric':
print(f" Max difference: {diff['max_difference']:.6f}")
print(f" Mean difference: {diff['mean_difference']:.6f}")
print(f" Different rows: {diff['different_count']}")
else:
print(f" Different rows: {diff['different_count']} / {diff['total_compared']}")
else:
print("\n✅ No significant differences found!")
print("\n" + "="*60 + "\n")