SECTION 1: LEARNING OBJECTIVES

By the end of this lesson, you will be able to:

  1. Define data profiling and understand its role in assessing financial data quality.

  2. Implement comprehensive data profiling for banking datasets using Python.

  3. Understand the dimensions of data quality (accuracy, completeness, consistency, timeliness, validity, uniqueness).

  4. Build automated data quality checks for financial data.

  5. Create data quality dashboards for monitoring and reporting.

  6. Document data quality issues and recommendations for remediation.

  7. Understand regulatory requirements for data quality in banking.

  8. Implement data quality monitoring for ongoing compliance.


SECTION 2: INTRODUCTION TO DATA PROFILING

2.1 What Is Data Profiling?

Data profiling is the process of examining data to understand its structure, content, relationships, and quality. In banking, data profiling answers critical questions:

 
 
Question Banking Example
What data exists? What customer attributes do we have?
Where is the data? Which systems store customer data?
What is the data quality? Are credit scores complete and valid?
What are the relationships? How are customers linked to accounts?
What are the patterns? What is the typical transaction amount?

2.2 Data Profiling vs. EDA

 
 
Aspect Data Profiling Exploratory Data Analysis
Purpose Assess data quality and structure Discover patterns and insights
Focus Metadata, completeness, validity Relationships, trends, distributions
Output Quality report, data dictionary Visualizations, insights, hypotheses
Audience Data engineers, compliance Data scientists, business analysts
Frequency Regular monitoring Project-specific

SECTION 3: IMPLEMENTING DATA PROFILING

3.1 Building a Data Profiler Class

python
# ============= COMPREHENSIVE DATA PROFILER =============

import pandas as pd
import numpy as np
from datetime import datetime
import matplotlib.pyplot as plt
import seaborn as sns

class FinancialDataProfiler:
    """
    Comprehensive data profiler for financial datasets.
    """
    
    def __init__(self, data, dataset_name="Dataset"):
        """
        Initialize profiler with data.
        
        Parameters:
        -----------
        data : DataFrame
            Data to profile
        dataset_name : str
            Name of the dataset
        """
        self.data = data
        self.dataset_name = dataset_name
        self.profile = {}
        self.quality_issues = []
        
    def profile_basic(self):
        """Generate basic dataset profile."""
        self.profile['basic'] = {
            'rows': len(self.data),
            'columns': len(self.data.columns),
            'memory_usage_mb': self.data.memory_usage(deep=True).sum() / 1024**2,
            'column_names': list(self.data.columns),
            'dtypes': self.data.dtypes.to_dict()
        }
        return self.profile['basic']
    
    def profile_numeric(self):
        """Profile numeric columns."""
        numeric_cols = self.data.select_dtypes(include=[np.number]).columns
        numeric_profile = {}
        
        for col in numeric_cols:
            col_data = self.data[col].dropna()
            if len(col_data) > 0:
                numeric_profile[col] = {
                    'count': len(col_data),
                    'missing': len(self.data[col]) - len(col_data),
                    '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),
                    'skewness': col_data.skew(),
                    'kurtosis': col_data.kurtosis(),
                    'unique_count': self.data[col].nunique()
                }
        
        self.profile['numeric'] = numeric_profile
        return numeric_profile
    
    def profile_categorical(self):
        """Profile categorical columns."""
        categorical_cols = self.data.select_dtypes(include=['object', 'category']).columns
        categorical_profile = {}
        
        for col in categorical_cols:
            col_data = self.data[col].dropna()
            value_counts = self.data[col].value_counts()
            
            categorical_profile[col] = {
                'count': len(col_data),
                'missing': len(self.data[col]) - len(col_data),
                'unique_values': self.data[col].nunique(),
                'top_values': value_counts.head(5).to_dict(),
                'top_value': value_counts.index[0] if len(value_counts) > 0 else None,
                'top_value_pct': value_counts.iloc[0] / len(col_data) if len(col_data) > 0 else 0
            }
        
        self.profile['categorical'] = categorical_profile
        return categorical_profile
    
    def profile_datetime(self):
        """Profile datetime columns."""
        datetime_cols = self.data.select_dtypes(include=['datetime64']).columns
        datetime_profile = {}
        
        for col in datetime_cols:
            col_data = self.data[col].dropna()
            if len(col_data) > 0:
                datetime_profile[col] = {
                    'count': len(col_data),
                    'missing': len(self.data[col]) - len(col_data),
                    'min': col_data.min(),
                    'max': col_data.max(),
                    'unique_dates': col_data.nunique(),
                    'date_range_days': (col_data.max() - col_data.min()).days
                }
        
        self.profile['datetime'] = datetime_profile
        return datetime_profile
    
    def profile_missing(self):
        """Profile missing values."""
        missing_counts = self.data.isnull().sum()
        missing_pcts = (missing_counts / len(self.data) * 100).round(2)
        
        missing_profile = {}
        for col in self.data.columns:
            if missing_counts[col] > 0:
                missing_profile[col] = {
                    'count': missing_counts[col],
                    'percentage': missing_pcts[col]
                }
        
        self.profile['missing'] = missing_profile
        return missing_profile
    
    def profile_duplicates(self):
        """Profile duplicate records."""
        duplicate_counts = {}
        
        for col in self.data.columns:
            duplicates = self.data[col].duplicated().sum()
            if duplicates > 0:
                duplicate_counts[col] = {
                    'count': duplicates,
                    'percentage': duplicates / len(self.data) * 100
                }
        
        # Check for duplicate rows
        duplicate_rows = self.data.duplicated().sum()
        if duplicate_rows > 0:
            duplicate_counts['row_duplicates'] = {
                'count': duplicate_rows,
                'percentage': duplicate_rows / len(self.data) * 100
            }
        
        self.profile['duplicates'] = duplicate_counts
        return duplicate_counts
    
    def run_full_profile(self):
        """Run all profiling steps."""
        print(f"Running full profile for: {self.dataset_name}")
        print("-" * 40)
        
        self.profile_basic()
        print("✓ Basic profiling complete")
        
        self.profile_numeric()
        print("✓ Numeric profiling complete")
        
        self.profile_categorical()
        print("✓ Categorical profiling complete")
        
        self.profile_datetime()
        print("✓ Datetime profiling complete")
        
        self.profile_missing()
        print("✓ Missing value profiling complete")
        
        self.profile_duplicates()
        print("✓ Duplicate profiling complete")
        
        return self.profile
    
    def generate_report(self):
        """Generate comprehensive profiling report."""
        print("\n" + "="*80)
        print(f"DATA PROFILING REPORT: {self.dataset_name}")
        print("="*80)
        
        # Basic Information
        basic = self.profile.get('basic', {})
        print(f"\n📊 BASIC INFORMATION:")
        print(f"  Rows: {basic.get('rows', 0):,}")
        print(f"  Columns: {basic.get('columns', 0)}")
        print(f"  Memory Usage: {basic.get('memory_usage_mb', 0):.2f} MB")
        print(f"  Column Names: {basic.get('column_names', [])}")
        
        # Missing Values
        missing = self.profile.get('missing', {})
        if missing:
            print(f"\n⚠️ MISSING VALUES:")
            for col, info in missing.items():
                print(f"  {col}: {info['count']} missing ({info['percentage']}%)")
        else:
            print("\n✅ No missing values found")
        
        # Duplicates
        duplicates = self.profile.get('duplicates', {})
        if duplicates:
            print(f"\n📋 DUPLICATES:")
            for col, info in duplicates.items():
                print(f"  {col}: {info['count']} duplicates ({info['percentage']:.1f}%)")
        else:
            print("\n✅ No duplicates found")
        
        # Numeric Summary
        numeric = self.profile.get('numeric', {})
        if numeric:
            print(f"\n📈 NUMERIC SUMMARY:")
            for col, stats in list(numeric.items())[:5]:
                print(f"\n  {col}:")
                print(f"    Count: {stats['count']}")
                print(f"    Missing: {stats['missing']}")
                print(f"    Mean: {stats['mean']:.2f}")
                print(f"    Median: {stats['median']:.2f}")
                print(f"    Std: {stats['std']:.2f}")
                print(f"    Range: [{stats['min']:.2f}, {stats['max']:.2f}]")
                print(f"    Skewness: {stats['skewness']:.3f}")
        
        # Categorical Summary
        categorical = self.profile.get('categorical', {})
        if categorical:
            print(f"\n📋 CATEGORICAL SUMMARY:")
            for col, stats in categorical.items():
                print(f"\n  {col}:")
                print(f"    Unique values: {stats['unique_values']}")
                print(f"    Top value: {stats['top_value']} ({stats['top_value_pct']:.1f}%)")
                top_values = stats['top_values']
                for val, count in top_values.items():
                    print(f"      {val}: {count}")
        
        return self.profile
    
    def identify_quality_issues(self):
        """Identify data quality issues based on profiling."""
        issues = []
        
        # Check for high missing values (>20%)
        for col, info in self.profile.get('missing', {}).items():
            if info['percentage'] > 20:
                issues.append({
                    'severity': 'High',
                    'type': 'Missing Values',
                    'column': col,
                    'details': f"{info['percentage']:.1f}% missing"
                })
            elif info['percentage'] > 5:
                issues.append({
                    'severity': 'Medium',
                    'type': 'Missing Values',
                    'column': col,
                    'details': f"{info['percentage']:.1f}% missing"
                })
        
        # Check for high duplicates
        for col, info in self.profile.get('duplicates', {}).items():
            if info['percentage'] > 10:
                issues.append({
                    'severity': 'High',
                    'type': 'Duplicate Values',
                    'column': col,
                    'details': f"{info['percentage']:.1f}% duplicates"
                })
        
        # Check for skewness (indicating potential outliers)
        for col, stats in self.profile.get('numeric', {}).items():
            if abs(stats.get('skewness', 0)) > 2:
                issues.append({
                    'severity': 'Medium',
                    'type': 'High Skewness',
                    'column': col,
                    'details': f"Skewness: {stats['skewness']:.3f}"
                })
        
        self.quality_issues = issues
        return issues
    
    def generate_quality_report(self):
        """Generate data quality report."""
        issues = self.identify_quality_issues()
        
        print("\n" + "="*80)
        print("DATA QUALITY REPORT")
        print("="*80)
        
        if not issues:
            print("\n✅ No data quality issues identified!")
            return
        
        print(f"\n⚠️ {len(issues)} Data Quality Issues Identified:\n")
        
        for i, issue in enumerate(issues, 1):
            severity = issue['severity']
            severity_emoji = "🔴" if severity == "High" else "🟡" if severity == "Medium" else "🟢"
            
            print(f"{i}. {severity_emoji} {issue['severity']} - {issue['type']}")
            print(f"   Column: {issue['column']}")
            print(f"   Details: {issue['details']}")
            print()
        
        # Recommendations
        print("\n💡 RECOMMENDATIONS:")
        for issue in issues:
            if issue['type'] == 'Missing Values':
                if 'address' in issue['column'].lower():
                    print(f"  • Consider using address validation services for {issue['column']}")
                elif 'credit_score' in issue['column'].lower():
                    print(f"  • Consider imputing missing credit scores using bureau data")
                else:
                    print(f"  • Consider imputation strategies for {issue['column']}")
            elif issue['type'] == 'High Skewness':
                print(f"  • Consider log transformation for {issue['column']}")
            elif issue['type'] == 'Duplicate Values':
                print(f"  • Consider deduplication strategies for {issue['column']}")

# Create a profiler instance
profiler = FinancialDataProfiler(customers, "Customer Data")

# Run full profile
profiler.run_full_profile()

# Generate reports
profiler.generate_report()
profiler.generate_quality_report()

3.2 Advanced Data Quality Checks

python
# ============= ADVANCED DATA QUALITY CHECKS =============

class DataQualityChecker:
    """
    Advanced data quality checks for financial data.
    """
    
    def __init__(self, data):
        self.data = data
        self.checks = []
        self.results = []
    
    def check_completeness(self, column, threshold=0.95):
        """Check if column has sufficient completeness."""
        missing_pct = self.data[column].isnull().mean()
        passed = missing_pct <= (1 - threshold)
        
        self.results.append({
            'check': 'completeness',
            'column': column,
            'passed': passed,
            'value': f"{missing_pct:.1%} missing",
            'threshold': f"{threshold:.0%} complete"
        })
        
        return passed
    
    def check_validity(self, column, valid_values):
        """Check if all values are in valid set."""
        invalid_pct = (~self.data[column].isin(valid_values)).mean()
        passed = invalid_pct == 0
        
        self.results.append({
            'check': 'validity',
            'column': column,
            'passed': passed,
            'value': f"{invalid_pct:.1%} invalid",
            'threshold': "0% invalid"
        })
        
        return passed
    
    def check_numeric_range(self, column, min_val, max_val):
        """Check if values are within valid range."""
        outside_range = ((self.data[column] < min_val) | (self.data[column] > max_val)).mean()
        passed = outside_range == 0
        
        self.results.append({
            'check': 'range',
            'column': column,
            'passed': passed,
            'value': f"{outside_range:.1%} outside range",
            'threshold': f"Range [{min_val}, {max_val}]"
        })
        
        return passed
    
    def check_consistency(self, col1, col2, relationship):
        """
        Check consistency between two columns.
        relationship: 'equals', 'greater', 'less'
        """
        if relationship == 'equals':
            inconsistent = (self.data[col1] != self.data[col2]).mean()
        elif relationship == 'greater':
            inconsistent = (self.data[col1] <= self.data[col2]).mean()
        elif relationship == 'less':
            inconsistent = (self.data[col1] >= self.data[col2]).mean()
        else:
            inconsistent = 1
        
        passed = inconsistent == 0
        
        self.results.append({
            'check': 'consistency',
            'columns': f"{col1}, {col2}",
            'passed': passed,
            'value': f"{inconsistent:.1%} inconsistent",
            'threshold': f"{relationship} relationship"
        })
        
        return passed
    
    def check_uniqueness(self, column):
        """Check if column values are unique."""
        duplicate_pct = (self.data[column].duplicated()).mean()
        passed = duplicate_pct < 0.01  # Less than 1% duplicates
        
        self.results.append({
            'check': 'uniqueness',
            'column': column,
            'passed': passed,
            'value': f"{duplicate_pct:.1%} duplicates",
            'threshold': "< 1% duplicates"
        })
        
        return passed
    
    def check_timeliness(self, date_column, max_age_days=1):
        """Check if data is timely (not too old)."""
        if pd.api.types.is_datetime64_any_dtype(self.data[date_column]):
            max_date = self.data[date_column].max()
            age_days = (pd.Timestamp.now() - max_date).days
            passed = age_days <= max_age_days
            
            self.results.append({
                'check': 'timeliness',
                'column': date_column,
                'passed': passed,
                'value': f"{age_days} days old",
                'threshold': f"≤ {max_age_days} days old"
            })
            
            return passed
        else:
            self.results.append({
                'check': 'timeliness',
                'column': date_column,
                'passed': False,
                'value': 'Not datetime',
                'threshold': 'Date type required'
            })
            return False
    
    def run_all_checks(self):
        """Run all quality checks."""
        print("\n" + "="*60)
        print("DATA QUALITY CHECKS")
        print("="*60)
        
        # Completeness checks
        for col in ['credit_score', 'annual_income']:
            if col in self.data.columns:
                self.check_completeness(col, threshold=0.90)
        
        # Validity checks
        if 'state' in self.data.columns:
            valid_states = ['CA', 'NY', 'TX', 'FL', 'IL', 'PA', 'OH', 'GA', 'NC', 'MI']
            self.check_validity('state', valid_states)
        
        # Range checks
        if 'credit_score' in self.data.columns:
            self.check_numeric_range('credit_score', 300, 850)
        
        if 'age' in self.data.columns:
            self.check_numeric_range('age', 18, 100)
        
        # Uniqueness checks
        if 'customer_id' in self.data.columns:
            self.check_uniqueness('customer_id')
        
        # Generate report
        self.generate_quality_report()
        
        return self.results
    
    def generate_quality_report(self):
        """Generate comprehensive quality report."""
        print("\n" + "="*60)
        print("QUALITY CHECK RESULTS")
        print("="*60)
        
        passed = sum(1 for r in self.results if r['passed'])
        total = len(self.results)
        
        print(f"\n📊 Summary: {passed}/{total} checks passed")
        
        for result in self.results:
            status = "✅ PASS" if result['passed'] else "❌ FAIL"
            print(f"\n{status} - {result['check'].upper()}")
            print(f"  Column(s): {result.get('column', result.get('columns', 'N/A'))}")
            print(f"  Value: {result['value']}")
            print(f"  Threshold: {result['threshold']}")

# Run quality checks
quality_checker = DataQualityChecker(customers)
quality_checker.run_all_checks()

SECTION 4: DATA QUALITY DASHBOARD

4.1 Building a Quality Dashboard

python
# ============= DATA QUALITY DASHBOARD =============

class DataQualityDashboard:
    """
    Create a data quality dashboard for monitoring.
    """
    
    def __init__(self, data, dataset_name):
        self.data = data
        self.dataset_name = dataset_name
        self.metrics = {}
    
    def calculate_metrics(self):
        """Calculate quality metrics."""
        
        # Completeness
        completeness = 1 - (self.data.isnull().sum() / len(self.data))
        self.metrics['completeness'] = completeness.to_dict()
        
        # Uniqueness
        self.metrics['duplicate_rate'] = self.data.duplicated().mean()
        
        # Numeric metrics
        numeric_cols = self.data.select_dtypes(include=[np.number]).columns
        for col in numeric_cols:
            if col in self.data.columns:
                col_data = self.data[col].dropna()
                if len(col_data) > 0:
                    self.metrics[f'mean_{col}'] = col_data.mean()
                    self.metrics[f'std_{col}'] = col_data.std()
                    self.metrics[f'skew_{col}'] = col_data.skew()
        
        # Categorical metrics
        cat_cols = self.data.select_dtypes(include=['object']).columns
        for col in cat_cols:
            if col in self.data.columns:
                self.metrics[f'unique_{col}'] = self.data[col].nunique()
                top_val = self.data[col].value_counts().index[0]
                top_pct = self.data[col].value_counts().iloc[0] / len(self.data)
                self.metrics[f'top_{col}'] = top_val
                self.metrics[f'top_pct_{col}'] = top_pct
    
    def create_dashboard(self):
        """Create visual dashboard."""
        self.calculate_metrics()
        
        fig, axes = plt.subplots(2, 2, figsize=(14, 10))
        
        # 1. Completeness Heatmap
        completeness_df = pd.DataFrame({
            'Column': list(self.metrics['completeness'].keys()),
            'Completeness': list(self.metrics['completeness'].values())
        })
        completeness_df['Completeness'] = completeness_df['Completeness'] * 100
        
        colors = ['#2ecc71' if x > 95 else '#f1c40f' if x > 80 else '#e74c3c' 
                 for x in completeness_df['Completeness']]
        completeness_df.plot(kind='barh', x='Column', y='Completeness', 
                            ax=axes[0, 0], color=colors, legend=False)
        axes[0, 0].set_title('Data Completeness by Column', fontsize=12)
        axes[0, 0].set_xlabel('Completeness (%)')
        axes[0, 0].axvline(95, color='green', linestyle='--', alpha=0.5, label='95% target')
        axes[0, 0].axvline(80, color='red', linestyle='--', alpha=0.5, label='80% minimum')
        axes[0, 0].legend()
        
        # 2. Column Quality Summary
        quality_scores = []
        for col in self.data.columns:
            missing_pct = self.data[col].isnull().mean()
            unique_pct = self.data[col].nunique() / len(self.data)
            score = (1 - missing_pct) * 0.7 + min(unique_pct * 10, 1) * 0.3
            quality_scores.append({
                'column': col,
                'score': score * 100
            })
        
        quality_df = pd.DataFrame(quality_scores)
        quality_df = quality_df.sort_values('score')
        
        colors = ['#2ecc71' if x > 80 else '#f1c40f' if x > 60 else '#e74c3c' 
                 for x in quality_df['score']]
        quality_df.plot(kind='barh', x='column', y='score', 
                       ax=axes[0, 1], color=colors, legend=False)
        axes[0, 1].set_title('Data Quality Score by Column', fontsize=12)
        axes[0, 1].set_xlabel('Quality Score (%)')
        axes[0, 1].axvline(80, color='green', linestyle='--', alpha=0.5, label='Good')
        axes[0, 1].axvline(60, color='red', linestyle='--', alpha=0.5, label='Poor')
        axes[0, 1].legend()
        
        # 3. Missing Values Overview
        missing_counts = self.data.isnull().sum()
        missing_counts = missing_counts[missing_counts > 0]
        
        if len(missing_counts) > 0:
            missing_counts.plot(kind='bar', ax=axes[1, 0], color='#e74c3c')
            axes[1, 0].set_title('Missing Values by Column', fontsize=12)
            axes[1, 0].set_xlabel('Column')
            axes[1, 0].set_ylabel('Missing Count')
            axes[1, 0].tick_params(axis='x', rotation=45)
        else:
            axes[1, 0].text(0.5, 0.5, '✅ No Missing Values', 
                           ha='center', va='center', fontsize=14)
            axes[1, 0].set_title('Missing Values', fontsize=12)
            axes[1, 0].axis('off')
        
        # 4. Data Type Distribution
        dtype_counts = self.data.dtypes.value_counts()
        dtype_counts.plot(kind='pie', ax=axes[1, 1], autopct='%1.1f%%', startangle=90)
        axes[1, 1].set_title('Data Type Distribution', fontsize=12)
        axes[1, 1].set_ylabel('')
        
        plt.tight_layout()
        plt.savefig(f'{self.dataset_name}_quality_dashboard.png', dpi=300)
        plt.show()
        
        return self.metrics
    
    def generate_summary(self):
        """Generate quality summary."""
        self.calculate_metrics()
        
        print("\n" + "="*60)
        print(f"QUALITY SUMMARY: {self.dataset_name}")
        print("="*60)
        
        # Completeness
        avg_completeness = sum(self.metrics['completeness'].values()) / len(self.metrics['completeness'])
        print(f"\n📊 Completeness:")
        print(f"  Average: {avg_completeness:.1%}")
        print(f"  Columns with >95%: {sum(1 for v in self.metrics['completeness'].values() if v > 0.95)}")
        print(f"  Columns with <80%: {sum(1 for v in self.metrics['completeness'].values() if v < 0.80)}")
        
        # Duplicates
        print(f"\n📋 Duplicates:")
        print(f"  Duplicate rate: {self.metrics['duplicate_rate']:.1%}")
        
        # Categorical summary
        cat_cols = self.data.select_dtypes(include=['object']).columns
        if len(cat_cols) > 0:
            print(f"\n📋 Categorical Summary:")
            for col in cat_cols:
                unique = self.metrics.get(f'unique_{col}', 0)
                top = self.metrics.get(f'top_{col}', 'N/A')
                top_pct = self.metrics.get(f'top_pct_{col}', 0)
                print(f"  {col}: {unique} unique values, top '{top}' ({top_pct:.1%})")

# Create dashboard
dashboard = DataQualityDashboard(customers, "Customer_Data")
dashboard.create_dashboard()
dashboard.generate_summary()

SECTION 5: REGULATORY COMPLIANCE & DATA QUALITY

5.1 Regulatory Requirements for Data Quality

python
# ============= REGULATORY COMPLIANCE CHECK =============

def check_regulatory_compliance(profiler):
    """
    Check if data meets regulatory requirements.
    """
    
    print("\n" + "="*80)
    print("REGULATORY COMPLIANCE CHECK")
    print("="*80)
    
    requirements = {
        'Completeness': {
            'threshold': 0.95,
            'description': '95% completeness required for regulatory reporting'
        },
        'Uniqueness': {
            'threshold': 0.99,
            'description': '99% uniqueness for customer identifiers'
        },
        'Validity': {
            'threshold': 1.0,
            'description': '100% validity for regulatory fields'
        },
        'Timeliness': {
            'threshold': 1,
            'description': 'Data must be current (within 1 day)'
        }
    }
    
    compliance_status = []
    
    # Check each requirement
    for req, details in requirements.items():
        status = "✅ PASS"  # Placeholder
        note = ""
        
        if req == 'Completeness':
            missing_pcts = [info['percentage'] for info in profiler.profile.get('missing', {}).values()]
            if missing_pcts:
                avg_missing = sum(missing_pcts) / len(missing_pcts)
                if avg_missing / 100 <= details['threshold']:
                    status = "✅ PASS"
                    note = f"Average completeness: {(1 - avg_missing/100):.1%}"
                else:
                    status = "❌ FAIL"
                    note = f"Average completeness: {(1 - avg_missing/100):.1%} (below {details['threshold']:.0%})"
            else:
                status = "✅ PASS"
                note = "100% complete"
        
        elif req == 'Uniqueness':
            duplicates = profiler.profile.get('duplicates', {})
            if duplicates:
                max_dup_pct = max(info['percentage'] for info in duplicates.values())
                if 1 - max_dup_pct/100 >= details['threshold']:
                    status = "✅ PASS"
                    note = f"Uniqueness: {(1 - max_dup_pct/100):.1%}"
                else:
                    status = "❌ FAIL"
                    note = f"Uniqueness: {(1 - max_dup_pct/100):.1%} (below {details['threshold']:.0%})"
            else:
                status = "✅ PASS"
                note = "100% unique"
        
        compliance_status.append({
            'requirement': req,
            'status': status,
            'description': details['description'],
            'note': note
        })
    
    # Display compliance report
    print("\n" + "="*80)
    print("COMPLIANCE REPORT")
    print("="*80)
    
    for item in compliance_status:
        print(f"\n📋 {item['requirement']}")
        print(f"  Status: {item['status']}")
        print(f"  Description: {item['description']}")
        print(f"  Note: {item['note']}")
    
    # Overall compliance
    all_passed = all(item['status'] == "✅ PASS" for item in compliance_status)
    print("\n" + "-"*40)
    if all_passed:
        print("✅ OVERALL: All regulatory requirements met!")
    else:
        print("❌ OVERALL: Some regulatory requirements not met. Please address issues.")
        print("   Recommend reviewing data quality before regulatory submission.")
    
    return compliance_status

# Run compliance check
compliance_results = check_regulatory_compliance(profiler)

SECTION 6: BUSINESS RISK & FINANCIAL IMPACT

6.1 Cost of Poor Data Quality

python
# ============= DATA QUALITY BUSINESS IMPACT =============

def calculate_impact_of_data_quality(profiler):
    """
    Calculate the potential business impact of data quality issues.
    """
    
    print("\n" + "="*80)
    print("BUSINESS IMPACT OF DATA QUALITY")
    print("="*80)
    
    issues = profiler.identify_quality_issues()
    
    if not issues:
        print("\n✅ No quality issues found. Data is ready for use.")
        return
    
    impacts = {
        'Missing Values': {
            'risk': 'High',
            'impact': 'Regulatory fines, incorrect decisions',
            'estimated_cost': '$50M - $200M'
        },
        'Duplicate Values': {
            'risk': 'Medium',
            'impact': 'Operational inefficiency, customer confusion',
            'estimated_cost': '$10M - $50M'
        },
        'High Skewness': {
            'risk': 'Medium',
            'impact': 'Model accuracy issues, risk misassessment',
            'estimated_cost': '$25M - $100M'
        },
        'Invalid Values': {
            'risk': 'High',
            'impact': 'Regulatory non-compliance, operational errors',
            'estimated_cost': '$100M - $500M'
        }
    }
    
    print("\n⚠️ Data Quality Risks Identified:")
    
    for issue in issues:
        impact_info = impacts.get(issue['type'], {
            'risk': 'Unknown',
            'impact': 'Unknown',
            'estimated_cost': 'Unknown'
        })
        
        print(f"\n🔴 Issue: {issue['type']}")
        print(f"   Column: {issue['column']}")
        print(f"   Risk Level: {impact_info['risk']}")
        print(f"   Business Impact: {impact_info['impact']}")
        print(f"   Estimated Cost: {impact_info['estimated_cost']}")
        print(f"   Details: {issue['details']}")
    
    print("\n" + "-"*40)
    print("💡 Recommendations:")
    print("  1. Prioritize fixing High-risk issues first")
    print("  2. Implement automated data quality monitoring")
    print("  3. Establish data quality SLAs (Service Level Agreements)")
    print("  4. Create data quality remediation plan")
    print("  5. Train teams on data quality best practices")

# Calculate impact
calculate_impact_of_data_quality(profiler)

SECTION 7: SUMMARY FOR THE DATA PRACTITIONER

7.1 The 1-Minute Elevator Pitch

“Data profiling is the systematic assessment of data quality in banking datasets. We analyze completeness, validity, uniqueness, consistency, and timeliness of data. A comprehensive profiler identifies missing values, duplicates, outliers, and patterns. Data quality dashboards provide ongoing monitoring for compliance. In banking, data profiling is essential for regulatory compliance, risk management, and building trust in analytics. Poor data quality can cost banks millions in fines and lost revenue.”

7.2 Key Takeaways

  1. Data profiling assesses data structure, content, and quality.

  2. Completeness measures how much data is present.

  3. Validity checks if data conforms to expected formats and ranges.

  4. Uniqueness identifies duplicate records.

  5. Consistency checks relationships between columns.

  6. Timeliness ensures data is up-to-date.

  7. Automated quality checks enable ongoing monitoring.

  8. Quality dashboards visualize data quality for stakeholders.

  9. Regulatory compliance requires documented data quality.

  10. Poor data quality has significant financial impact.

7.3 Recommended Next Steps

  1. Implement data profiling for your banking datasets

  2. Set up automated quality monitoring

  3. Document data quality issues and remediation plans

  4. Create quality dashboards for stakeholders

  5. Establish data quality SLAs


[END OF LESSON 2]