Back to Blog

Explainable AI for Financial Decisions: Why Transparency Matters in Behavioural Prediction

Black box AI might work for recommending movies, but financial decisions demand transparency. Discover how Whistl uses explainable AI techniques to show you exactly why you're receiving an intervention, building trust and enabling meaningful behaviour change.

The Problem with Black Box AI

Modern machine learning models can achieve remarkable accuracy—but often at the cost of interpretability. Deep neural networks with millions of parameters make predictions through complex, non-linear transformations that even their creators struggle to explain.

For entertainment recommendations, this opacity is acceptable. If Netflix can't explain why it suggested a particular show, the stakes are low. But for financial interventions—where an app is essentially telling you "don't spend money right now"—explanations are essential.

Without understanding why a prediction was made, users can't:

Explainability as a Design Principle

At Whistl, explainability isn't an afterthought—it's built into our AI architecture from the ground up. We use a combination of inherently interpretable models and post-hoc explanation techniques to ensure every prediction comes with a clear, actionable explanation.

Explainable AI Techniques in Whistl

Whistl employs multiple XAI techniques, each serving different explanatory purposes:

1. SHAP Values for Feature Importance

SHAP (SHapley Additive exPlanations) values provide a game-theoretic approach to explaining model predictions. Based on cooperative game theory, SHAP values fairly distribute the "credit" for a prediction among all input features.

import shap
import numpy as np

class SHAPExplainer:
    def __init__(self, model, background_data):
        """
        Initialize SHAP explainer with background data.
        Background data represents typical user behaviour.
        """
        self.explainer = shap.KernelExplainer(
            model.predict_proba, 
            background_data,
            feature_names=[
                'stress_level', 'time_since_payday', 'location_risk',
                'category_momentum', 'sleep_quality', 'social_context',
                'spending_velocity', 'accountability_active'
            ]
        )
    
    def explain_prediction(self, user_features):
        """
        Generate SHAP explanation for a single prediction.
        Returns feature contributions that sum to the prediction.
        """
        shap_values = self.explainer.shap_values(user_features)
        
        # Format explanation
        explanation = {
            'base_probability': self._get_base_rate(),
            'feature_contributions': [],
            'total_risk': shap_values.sum() + self._get_base_rate()
        }
        
        for i, feature_name in enumerate(self.explainer.feature_names):
            contribution = shap_values[i]
            explanation['feature_contributions'].append({
                'feature': feature_name,
                'value': user_features[i],
                'contribution': contribution,
                'direction': 'increases' if contribution > 0 else 'decreases'
            })
        
        # Sort by absolute contribution
        explanation['feature_contributions'].sort(
            key=lambda x: abs(x['contribution']), 
            reverse=True
        )
        
        return explanation

# Example output:
# {
#     'base_probability': 0.15,
#     'feature_contributions': [
#         {'feature': 'stress_level', 'value': 0.82, 'contribution': 0.23, 'direction': 'increases'},
#         {'feature': 'time_since_payday', 'value': 0.1, 'contribution': 0.18, 'direction': 'increases'},
#         {'feature': 'location_risk', 'value': 0.9, 'contribution': 0.12, 'direction': 'increases'},
#         {'feature': 'accountability_active', 'value': 0.0, 'contribution': -0.08, 'direction': 'decreases'},
#     ],
#     'total_risk': 0.60  # 60% impulse risk
# }

This explanation tells the user: "Your risk is 60% (vs. 15% baseline) primarily because your stress level is elevated, it's right after payday, and you're in a high-risk location. Having an accountability partner would reduce your risk."

2. Counterfactual Explanations

Counterfactual explanations answer the question: "What would need to change for the prediction to be different?" This is particularly actionable for users who want to reduce their risk.

class CounterfactualExplainer:
    def __init__(self, model, feature_ranges):
        self.model = model
        self.feature_ranges = feature_ranges  # Min/max for each feature
    
    def find_counterfactual(self, user_features, target_risk=0.3):
        """
        Find minimal changes needed to achieve target risk level.
        Uses gradient-based optimization for efficiency.
        """
        current_risk = self.model.predict_proba(user_features)[0, 1]
        
        if current_risk <= target_risk:
            return None  # Already below target
        
        # Optimize to find counterfactual
        best_counterfactual = None
        best_distance = float('inf')
        
        for _ in range(100):  # Multiple restarts
            counterfactual = self._optimize_counterfactual(
                user_features, target_risk
            )
            distance = np.linalg.norm(counterfactual - user_features)
            
            if distance < best_distance:
                best_distance = distance
                best_counterfactual = counterfactual
        
        # Generate human-readable explanation
        return self._format_counterfactual(
            user_features, best_counterfactual, current_risk, target_risk
        )
    
    def _format_counterfactual(self, original, counterfactual, current_risk, target_risk):
        """Convert numerical changes to natural language."""
        changes = []
        feature_names = [
            'stress level', 'time since payday', 'location risk',
            'category momentum', 'sleep quality', 'social support'
        ]
        
        for i, (orig, cf) in enumerate(zip(original, counterfactual)):
            if abs(orig - cf) > 0.1:  # Meaningful change
                direction = 'decrease' if cf < orig else 'increase'
                changes.append(f"{direction} {feature_names[i]}")
        
        return {
            'current_risk': current_risk,
            'target_risk': target_risk,
            'required_changes': changes,
            'feasibility': self._assess_feasibility(original, counterfactual)
        }

Example counterfactual: "To reduce your impulse risk from 60% to 30%, you could: wait 24 hours (reduces time pressure), move to a different location (avoid trigger environment), or activate your accountability partner (add social support)."

3. Attention Visualisation

For our Transformer-based models, attention weights provide natural explanations by showing which historical events the model considers most relevant.

def visualize_attention(attention_weights, transactions, top_k=5):
    """
    Visualize which past transactions the model is attending to.
    
    Args:
        attention_weights: Attention matrix from Transformer
        transactions: List of past transactions with metadata
        top_k: Number of top attended transactions to show
    
    Returns:
        Human-readable explanation of attention pattern
    """
    # Get attention to current prediction (last row)
    current_attention = attention_weights[-1, :]
    
    # Get top-k attended time steps
    top_indices = np.argsort(current_attention)[-top_k:][::-1]
    
    explanation = []
    for idx in top_indices:
        tx = transactions[idx]
        attention_score = current_attention[idx]
        
        explanation.append({
            'transaction': f"{tx.category} at {tx.merchant}",
            'amount': tx.amount,
            'time_ago': tx.time_ago,
            'attention_score': attention_score,
            'relevance': _explain_relevance(tx, attention_score)
        })
    
    return explanation

def _explain_relevance(transaction, attention_score):
    """Generate natural language explanation of why this transaction matters."""
    if transaction.category in ['entertainment', 'dining']:
        return f"Similar leisure spending {transaction.time_ago} predicts current impulse"
    elif transaction.amount > transaction.average_amount * 1.5:
        return f"Large purchase {transaction.time_ago} indicates elevated spending mood"
    elif transaction.time_of_day == 'late_night':
        return f"Late-night transaction {transaction.time_ago} correlates with reduced impulse control"
    else:
        return f"Pattern from {transaction.time_ago} informs current risk assessment"

4. Rule Extraction from Neural Networks

While neural networks are inherently complex, we can extract approximate rules that capture their decision logic:

from sklearn.tree import DecisionTreeClassifier

class RuleExtractor:
    def __init__(self, neural_model, feature_names):
        self.neural_model = neural_model
        self.feature_names = feature_names
        self.surrogate_model = DecisionTreeClassifier(
            max_depth=4,  # Keep tree shallow for interpretability
            min_samples_leaf=50
        )
    
    def extract_rules(self, data):
        """
        Train a decision tree to approximate neural network behavior.
        The tree's rules approximate the neural net's decision logic.
        """
        # Get neural network predictions
        predictions = self.neural_model.predict(data)
        
        # Train decision tree to mimic neural net
        self.surrogate_model.fit(data, predictions)
        
        # Extract rules from tree
        rules = self._extract_tree_rules(self.surrogate_model)
        
        return rules
    
    def _extract_tree_rules(self, tree):
        """Convert decision tree to human-readable rules."""
        rules = []
        tree_ = tree.tree_
        
        def traverse(node, conditions):
            if tree_.feature[node] != -2:  # Not a leaf
                feature_name = self.feature_names[tree_.feature[node]]
                threshold = tree_.threshold[node]
                
                # Left child (feature <= threshold)
                traverse(
                    tree_.children_left[node],
                    conditions + [f"{feature_name} <= {threshold:.2f}"]
                )
                
                # Right child (feature > threshold)
                traverse(
                    tree_.children_right[node],
                    conditions + [f"{feature_name} > {threshold:.2f}"]
                )
            else:
                # Leaf node
                value = tree_.value[node][0][1]  # Probability of positive class
                if value > 0.5:
                    rules.append({
                        'conditions': conditions,
                        'prediction': 'HIGH RISK',
                        'confidence': value
                    })
        
        traverse(0, [])
        return rules

# Example extracted rules:
# [
#   {'conditions': ['stress_level > 0.65', 'time_since_payday <= 0.15'], 
#    'prediction': 'HIGH RISK', 'confidence': 0.87},
#   {'conditions': ['location_risk <= 0.30', 'accountability_active = 1'], 
#    'prediction': 'LOW RISK', 'confidence': 0.91}
# ]

Presenting Explanations to Users

Technical explanations must be translated into user-friendly language. Whistl uses several presentation strategies:

Natural Language Summaries

Instead of showing raw SHAP values, we generate natural language:

"Your impulse risk is currently high (68%). This is primarily because:

  • Your stress level is elevated (you reported feeling overwhelmed 2 hours ago)
  • It's only 3 days since payday (historically a high-risk period for you)
  • You're currently near a shopping centre you frequently overspend at

What would help: Waiting 24 hours would reduce your risk to 35%. Activating your accountability partner would reduce it further to 22%."

Visual Risk Decomposition

Whistl shows a visual breakdown of risk factors with colour-coded contributions:

Interactive "What If" Explorer

Users can adjust sliders to see how different changes would affect their risk:

Building Trust Through Transparency

Explainability serves multiple purposes beyond mere information:

Calibrating Trust

Users learn when to trust the model and when to question it. If the explanation doesn't resonate ("I'm not stressed, the model is wrong"), users can override the intervention. This feedback loop actually improves the model over time.

Enabling Learning

Understanding patterns helps users internalise insights: "I always overspend three days after payday" becomes actionable self-knowledge that persists even without the app.

Supporting Autonomy

Explanations preserve user agency. Instead of "don't spend," the message becomes "here's what's happening, here's your risk, here are your options." Users make informed decisions rather than following blind commands.

"The explanations made all the difference. When Whistl told me I was high-risk because I was stressed and near a trigger location, I actually believed it. I could feel the stress, I knew that location was problematic. The app wasn't controlling me—it was helping me see what I already knew but was ignoring."
— Emma K., Whistl user since 2025

Evaluating Explanation Quality

Not all explanations are equally useful. Whistl evaluates explanations on multiple dimensions:

Dimension Description Measurement
Fidelity How accurately does the explanation reflect the model? Correlation between explanation and prediction
Comprehensibility How easily do users understand the explanation? User comprehension tests
Actionability Does the explanation suggest concrete actions? User action completion rate
Trust Does the explanation build appropriate trust? User trust surveys
Satisfaction Are users satisfied with the explanation? User satisfaction ratings

The Future of Explainable AI in Finance

Explainable AI is rapidly evolving. Whistl is researching:

Getting Started with Whistl

Experience AI that explains itself. Whistl's explainable predictions help you understand your financial behaviour while building the self-awareness needed for lasting change.

Transparent AI for Your Financial wellbeing

Join thousands of Australians using Whistl's explainable AI to understand and improve their spending patterns with complete transparency.

Crisis Support Resources

If you're experiencing severe financial distress or gambling-related harm, professional support is available:

Related Articles