<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Interpretable Inference]]></title><description><![CDATA[Technical publication about interpretable AI]]></description><link>https://interference.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!u0aM!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8cf09e4-3263-4306-bfec-06ee39f6eb7a_500x500.png</url><title>Interpretable Inference</title><link>https://interference.substack.com</link></image><generator>Substack</generator><lastBuildDate>Sat, 04 Apr 2026 07:51:47 GMT</lastBuildDate><atom:link href="https://interference.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Saeed]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[interference@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[interference@substack.com]]></itunes:email><itunes:name><![CDATA[Saeed Garmsiri]]></itunes:name></itunes:owner><itunes:author><![CDATA[Saeed Garmsiri]]></itunes:author><googleplay:owner><![CDATA[interference@substack.com]]></googleplay:owner><googleplay:email><![CDATA[interference@substack.com]]></googleplay:email><googleplay:author><![CDATA[Saeed Garmsiri]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Breaking the Spotify Feedback Loop - Building an Explainable Music Recommendation System with SHAP]]></title><description><![CDATA[Separate Spotify playlists aren't enough: why we need explainable AI for music discovery]]></description><link>https://interference.substack.com/p/breaking-the-spotify-feedback-loop</link><guid isPermaLink="false">https://interference.substack.com/p/breaking-the-spotify-feedback-loop</guid><dc:creator><![CDATA[Saeed Garmsiri]]></dc:creator><pubDate>Mon, 30 Jun 2025 13:03:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!fME_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!fME_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!fME_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png 424w, https://substackcdn.com/image/fetch/$s_!fME_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png 848w, https://substackcdn.com/image/fetch/$s_!fME_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png 1272w, https://substackcdn.com/image/fetch/$s_!fME_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!fME_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png" width="1456" height="977" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:977,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1300807,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://interference.substack.com/i/166091851?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!fME_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png 424w, https://substackcdn.com/image/fetch/$s_!fME_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png 848w, https://substackcdn.com/image/fetch/$s_!fME_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png 1272w, https://substackcdn.com/image/fetch/$s_!fME_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F523e1048-a8e0-4e9a-a7d3-4d6b777befca_2806x1882.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Listening to your playlists on Spotify could go wrong if you click on a random song accidentally and all of a sudden Spotify thinks you're obsessed with that genre then your entire Discover Weekly will be flooded with similar stuff for months. I've been there, that actually happened to me when I accidentally played an EDM track, and now my Discover Weekly is flooded with similar music. What's even more frustrating is that Spotify can't distinguish between my active hippop gym music, classical focus music for work, and my random cooking playlists - completely different contexts that need different types of music. And don't get me started on what happens when I listen to Mozart: Symphony No. 40 when I bench press.</p><p>I was fed up enough that I decided to build my own music recommendation system specifically for gym workouts. But here's the key difference - mine doesn't just recommend songs, it EXPLAINS exactly WHY it's suggesting each track. Not just "because you listened to X" but actually breaking down the audio characteristics that match your preferences and showing you visually why this track fits your workout needs.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://interference.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Interpretable Inference! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2><strong>The Explainability Problem in Music Recommendations</strong></h2><p>The biggest issue with Spotify isn't just that it gets stuck in feedback loops - it's that I never understand WHY I'm getting certain recommendations. When Spotify says "You might like this because you listened to Artist X," that tells me nothing about what sonic characteristics they're matching. It's what researchers call a "black box explanation" - I know something happened, but I have no idea how or why. And that creates a huge problem - without understanding why I'm getting a recommendation, I can't correct the algorithm when it's wrong. If Spotify recommends a song "because you listened to The Weeknd" - I have no way to say "Yes, but I only like his upbeat tracks for workouts, not his slower ballads." This lack of explainability means recommendation systems create what researchers call "false confidence." The system makes a suggestion with a seemingly reasonable explanation, you trust it, but the explanation is actually hiding the real complexity behind the recommendation.</p><div><hr></div><h2><strong>Why Separate Spotify Playlists Aren't Enough</strong></h2><p>If you've ever thought, "I'll just create different playlists for different activities," I've got bad news. Here's why this doesn't solve the problem:</p><ol><li><p>Spotify can't explain WHY a song fits one context but not another. For example: "Eye of the Tiger" by Survivor could be a perfect song for the gym, but when I'm at work developing code, listening to that causes a crazy code in production, so when spotify suggest some song to my work or focuse playlist it must be able to explain the why and score the fitness of the song for that playlist.</p></li><li><p>Spotify can't adjust which features matter most in different contexts. Here example is that for me, Instrumentalness is critical for work music but irrelevant for workouts. Spotify gives you no way to tell it this or explain why recommendations fail.</p></li><li><p>Feedback loops still occur with separate playlists. If you accidentally listen to workout music while working, Spotify will contaminate your work recommendations without explanation.</p></li></ol><p>Spotify never shows you why a song is a bad fit for your current activity. Creating separate playlists might help a bit, but without true explainability, you still can't understand or control how Spotify is building your profile for each context.</p><p>So I decided to solve these problems by building my own music recommendation system that actually explains its recommendations using SHAP (SHapley Additive exPlanations) values - a technique from explainable AI that shows exactly how each feature influences a prediction.</p><div><hr></div><h2><strong>Dataset Creation: Activity-Specific Music Collections</strong></h2><p>First, I needed to create realistic datasets for different activities. I collected audio features for my favorite songs in three contexts:</p><h3><strong>Gym Workout Music Dataset (50 Songs)</strong></h3><p>For gym workouts, I needed high-energy, uptempo tracks with good rhythmic structure to maintain motivation. Here's a sample with audio features from Spotify's API:</p><pre><code>import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import json

client_id = 'Dont_Use_My_Credentials'
client_secret = 'Dont_Use_My_Client_Secret'
client_credentials_manager = SpotifyClientCredentials(client_id=client_id, client_secret=client_secret)
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)

# Get tracks from a my workout playlist (you can use any workout playlist URI)
playlist_uri = '37i9dQZF1DX76Wlfdnj7AP'
results = sp.playlist_tracks(playlist_uri)

gym_songs = []
id_counter = 1

for track in results['items']:
    if track['track'] is not None:
        track_id = track['track']['id']
        
        audio_features = sp.audio_features(track_id)[0]
        
        song = {
            "id": f"g{id_counter:03d}",
            "name": track['track']['name'],
            "artist": track['track']['artists'][0]['name'],
            "features": {
                "energy": audio_features['energy'],
                "tempo": audio_features['tempo'],
                "danceability": audio_features['danceability'],
                "valence": audio_features['valence'],
                "instrumentalness": audio_features['instrumentalness'],
                "acousticness": audio_features['acousticness']
            },
            "context": "gym",
            "liked": True
        }
        
        gym_songs.append(song)
        id_counter += 1

with open('spotify_gym_songs.json', 'w') as f:
    json.dump(gym_songs, f, indent=4)

print(f"Saved {len(gym_songs)} songs to spotify_gym_songs.json")</code></pre><p>Here is an example output:</p><pre><code>gym_songs = [
    {
        "id": "g001",
        "name": "Stronger",
        "artist": "Kanye West",
        "features": {
            "energy": 0.75,
            "tempo": 104.0,
            "danceability": 0.62,
            "valence": 0.54,
            "instrumentalness": 0.0,
            "acousticness": 0.002
        },
        "context": "gym",
        "liked": True
    },
    {
        "id": "g002",
        "name": "Eye of the Tiger",
        "artist": "Survivor",
        "features": {
            "energy": 0.81,
            "tempo": 109.0,
            "danceability": 0.68,
            "valence": 0.57,
            "instrumentalness": 0.0,
            "acousticness": 0.05
        },
        "context": "gym",
        "liked": True
    },
    {
        "id": "g003",
        "name": "Till I Collapse",
        "artist": "Eminem",
        "features": {
            "energy": 0.89,
            "tempo": 171.0,
            "danceability": 0.57,
            "valence": 0.42,
            "instrumentalness": 0.0,
            "acousticness": 0.01
        },
        "context": "gym",
        "liked": True
    },
#rest of the songs
]</code></pre><h3><strong>Work Focus Music Dataset (20 Songs)</strong></h3><p>For deep work and concentration, I prefer instrumental, low-energy music without vocals that might distract me:</p><pre><code>import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import json

client_id = 'Dont_Use_My_Credentials'
client_secret = 'Dont_Use_My_Client_Secret'
client_credentials_manager = SpotifyClientCredentials(client_id=client_id, client_secret=client_secret)
sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)

# You can use a playlist URIs that contain instrumental focus music
playlist_uri = '37i9dQZF1DX3PFzdbtx1Us'
   
results = sp.playlist_tracks(playlist_uri)

concentration_songs = []
id_counter = 1

for track in results['items']:
    if track['track'] is not None:
        track_id = track['track']['id']
        
        audio_features = sp.audio_features(track_id)[0]
        
        if audio_features and audio_features['instrumentalness'] &gt; 0.5 and audio_features['energy'] &lt; 0.5:
            song = {
                "id": f"c{id_counter:03d}",
                "name": track['track']['name'],
                "artist": track['track']['artists'][0]['name'],
                "features": {
                    "energy": audio_features['energy'],
                    "tempo": audio_features['tempo'],
                    "danceability": audio_features['danceability'],
                    "valence": audio_features['valence'],
                    "instrumentalness": audio_features['instrumentalness'],
                    "acousticness": audio_features['acousticness']
                },
                "context": "concentration",
                "liked": True
            }
            
            concentration_songs.append(song)
            id_counter += 1
            
            # Limit to 50 songs
            if id_counter &gt; 50:
                break

with open('spotify_concentration_songs.json', 'w') as f:
    json.dump(concentration_songs, f, indent=4)

print(f"Saved {len(concentration_songs)} instrumental concentration songs to spotify_concentration_songs.json")</code></pre><p>And this is an example output:</p><pre><code>concentration_songs = [
    {
        "id": "c001",
        "name": "Gymnop&#233;die No. 1",
        "artist": "Erik Satie",
        "features": {
            "energy": 0.12,
            "tempo": 69.0,
            "danceability": 0.26,
            "valence": 0.31,
            "instrumentalness": 0.89,
            "acousticness": 0.98
        },
        "context": "concentration",
        "liked": True
    },
    {
        "id": "c002",
        "name": "Intro",
        "artist": "The xx",
        "features": {
            "energy": 0.34,
            "tempo": 90.0,
            "danceability": 0.42,
            "valence": 0.22,
            "instrumentalness": 0.83,
            "acousticness": 0.91
        },
        "context": "concentration",
        "liked": True
    },
    {
        "id": "c003",
        "name": "Avril 14th",
        "artist": "Aphex Twin",
        "features": {
            "energy": 0.22,
            "tempo": 98.0,
            "danceability": 0.34,
            "valence": 0.45,
            "instrumentalness": 0.92,
            "acousticness": 0.97
        },
        "context": "concentration",
        "liked": True
    },
# Rest of the songs...
]</code></pre><p>And last but not least, let's create a dataset for cooking time.</p><h3><strong>Cooking Music Dataset</strong></h3><p>For cooking, I enjoy upbeat, happy songs with positive energy that create a pleasant atmosphere:</p><pre><code>def find_cooking_songs_by_features(sp, limit=50):
    """
    Use Spotify's recommendation engine to find cooking-appropriate songs
    """
    recommendations = sp.recommendations(
        seed_genres=['pop', 'rock', 'indie', 'soul', 'funk'],
        limit=limit,
        target_valence=0.8,        # Very positive/happy
        min_valence=0.7,           # At least moderately happy
        target_danceability=0.7,   # Good for movement
        min_danceability=0.5,      # At least somewhat danceable
        target_energy=0.6,         # Moderate energy
        min_energy=0.4,            # Not too low energy
        max_energy=0.9,            # Not too overwhelming
        max_instrumentalness=0.5,  # Prefer songs with vocals
        target_tempo=120           # Good cooking tempo
    )
    
    cooking_songs = []
    for i, track in enumerate(recommendations['tracks']):
        audio_features = sp.audio_features(track['id'])[0]
        
        song = {
            "id": f"c{i+1:03d}",
            "name": track['name'],
            "artist": track['artists'][0]['name'],
            "features": {
                "energy": audio_features['energy'],
                "tempo": audio_features['tempo'],
                "danceability": audio_features['danceability'],
                "valence": audio_features['valence'],
                "instrumentalness": audio_features['instrumentalness'],
                "acousticness": audio_features['acousticness']
            },
            "context": "cooking",
            "liked": True
        }
        cooking_songs.append(song)
    
    return cooking_songs

new_cooking_songs = find_cooking_songs_by_features(sp, 25)</code></pre><p>And here's the output:</p><pre><code>cooking_songs = [
    {
        "id": "c001",
        "name": "Happy",
        "artist": "Pharrell Williams",
        "features": {
            "energy": 0.69,
            "tempo": 160.0,
            "danceability": 0.81,
            "valence": 0.96,
            "instrumentalness": 0.0,
            "acousticness": 0.12
        },
        "context": "cooking",
        "liked": True
    },
    {
        "id": "c002",
        "name": "Banana Pancakes",
        "artist": "Jack Johnson",
        "features": {
            "energy": 0.42,
            "tempo": 123.0,
            "danceability": 0.61,
            "valence": 0.78,
            "instrumentalness": 0.0,
            "acousticness": 0.63
        },
        "context": "cooking",
        "liked": True
    },
    {
        "id": "c003",
        "name": "Sunday Morning",
        "artist": "Maroon 5",
        "features": {
            "energy": 0.54,
            "tempo": 106.0,
            "danceability": 0.68,
            "valence": 0.81,
            "instrumentalness": 0.0,
            "acousticness": 0.57
        },
        "context": "cooking",
        "liked": True
    },
    #rest of songs
]</code></pre><div><hr></div><h2><strong>Audio Feature Analysis</strong></h2><p>To understand the distinct characteristics of each activity's music, I calculated the average audio features for each context:</p><pre><code>import pandas as pd
import numpy as np

def calculate_context_profiles(songs):
    """Calculate average features for each context from song library"""
    contexts = {}
    for song in songs:
        if song["context"] not in contexts:
            contexts[song["context"]] = []
        if song["liked"]:
            contexts[song["context"]].append(song)
    
    profiles = {}
    for context, songs in contexts.items():
        features = ["energy", "tempo", "danceability", "valence", 
                    "instrumentalness", "acousticness"]
        
        profile = {}
        for feature in features:
            values = [song["features"][feature] for song in songs]
            profile[feature] = sum(values) / len(values)
        
        profiles[context] = profile
    
    return profiles

all_songs = gym_songs + work_songs + cooking_songs

context_profiles = calculate_context_profiles(all_songs)

for context, profile in context_profiles.items():
    print(f"\n{context.upper()} PROFILE:")
    for feature, value in profile.items():
        print(f"  {feature}: {value:.2f}")</code></pre><p>The output shows clear distinctions between contexts:</p><pre><code>GYM PROFILE:
  energy: 0.85
  tempo: 135.60
  danceability: 0.65
  valence: 0.50
  instrumentalness: 0.00
  acousticness: 0.03

WORK PROFILE:
  energy: 0.15
  tempo: 79.00
  danceability: 0.25
  valence: 0.35
  instrumentalness: 0.94
  acousticness: 0.97

COOKING PROFILE:
  energy: 0.55
  tempo: 129.70
  danceability: 0.70
  valence: 0.85
  instrumentalness: 0.00
  acousticness: 0.44</code></pre><p>These profiles highlight why Spotify's one-size-fits-all approach fails:</p><ul><li><p>Gym music has high energy and tempo, low acousticness</p></li><li><p>Work music has high instrumentalness and acousticness, low energy</p></li><li><p>Cooking music has high valence (positivity) and danceability</p></li></ul><div><hr></div><h2><strong>Building the SHAP-based Explainable Recommendation System</strong></h2><p>Now, for the core functionality, we need a recommendation system that explains the WHY and SHAP values to assist us in seeing why songs are or aren't appropriate for different contexts.</p><h3><strong>Basic Recommendation Function</strong></h3><pre><code>def calculate_fit_score(song, context_profile, weights=None):
    """Calculate how well a song fits a specific context profile"""
    
    feature_scores = {}
    for feature, weight in weights.items():
        normalizer = 200 if feature == "tempo" else 1
        song_value = song["features"][feature]
        profile_value = context_profile[feature]
        
        diff = abs(song_value - profile_value) / normalizer
        feature_scores[feature] = max(0, 1 - diff)
    
    total_score = 0
    total_weight = 0
    
    for feature, weight in weights.items():
        total_score += feature_scores[feature] * weight
        total_weight += weight
    
    overall_score = total_score / total_weight
    
    return {
        "overall_score": overall_score,
        "feature_scores": feature_scores
    }</code></pre><h3><strong>SHAP Value Calculation</strong></h3><p>Ok as the next step, I implemented SHAP value calculation to explain how each feature contributes to the final recommendation:</p><pre><code>def calculate_shap_values(song, context_profile, weights=None):
    """Calculate SHAP values to explain fit score"""
  
    fit_result = calculate_fit_score(song, context_profile, weights)
    feature_scores = fit_result["feature_scores"]
    overall_score = fit_result["overall_score"]
    
    baseline = 0.5
    
    shap_values = {}
    total_weight = sum(weights.values())
    
    total_impact = 0
    for feature, weight in weights.items():
        normalized_weight = weight / total_weight
        feature_impact = (feature_scores[feature] - 0.5) * normalized_weight
        shap_values[feature] = feature_impact
        total_impact += feature_impact
    
    target_diff = overall_score - baseline
    if total_impact != 0: 
        normalization_factor = target_diff / total_impact
        for feature in shap_values:
            shap_values[feature] *= normalization_factor
    
    return {
        "shap_values": shap_values,
        "baseline": baseline,
        "prediction": overall_score,
        "feature_scores": feature_scores
    }</code></pre><h3><strong>SHAP Visualization Generation</strong></h3><p>Now, for the most important part, let's create visualizations that explain recommendations.</p><pre><code>def generate_shap_visualization(song, context, context_profiles):
    """Generate SHAP visualization to explain a recommendation"""
    context_profile = context_profiles[context]
    shap_result = calculate_shap_values(song, context_profile)
    
    print(f"\n===== SHAP EXPLANATION: \"{song['name']}\" by {song['artist']} for {context} =====")
    print(f"Overall fit score: {shap_result['prediction']:.2f} / 1.0")
    
    sorted_features = sorted(
        shap_result["shap_values"].items(),
        key=lambda x: abs(x[1]),
        reverse=True
    )
    
    print("\nWaterfall SHAP visualization (why this song fits or doesn't fit):")
    
    baseline = shap_result["baseline"]
    current_value = baseline
    bar_width = 30
    
    baseline_bar = f"Baseline         {'=' * int(baseline * bar_width)}|{' ' * (bar_width - int(baseline * bar_width))}"
    print(baseline_bar)
    
    for feature, value in sorted_features:
        abs_value = abs(value)
        bar_size = max(1, round(abs_value * bar_width * 2))
        is_positive = value &gt; 0
        
        song_value = song["features"][feature]
        profile_value = context_profile[feature]
        
        if is_positive:
            bar = f"{' ' * int(current_value * bar_width)}|{'&#8594;' * bar_size}"
            current_value += value
        else:
            current_value += value  # Update first for negative values
            bar = f"{' ' * int(current_value * bar_width)}{'&#8592;' * bar_size}|"
        
        prefix = "+" if is_positive else " "
        print(f"{feature.ljust(15)} {prefix}{value:.3f} {bar} [{song_value:.2f} vs {profile_value:.2f}]")
    
    prediction_bar = f"Final prediction {'=' * int(shap_result['prediction'] * bar_width)}|{' ' * (bar_width - int(shap_result['prediction'] * bar_width))}"
    print(prediction_bar)
    
    print("\nExplanation:")
    
    positive_features = [(f, v) for f, v in sorted_features if v &gt; 0]
    negative_features = [(f, v) for f, v in sorted_features if v &lt; 0]
    
    if positive_features:
        print("&#10003; Features that make this song a good fit:")
        for feature, value in positive_features[:2]:  # Top 2 positive features
            song_value = song["features"][feature]
            profile_value = context_profile[feature]
            
            explanation = generate_feature_explanation(feature, song_value, profile_value, context, True)
            print(f"  - {feature.title()} ({song_value:.2f}): {explanation}")
    
    if negative_features:
        print("&#10007; Features that make this song a poor fit:")
        for feature, value in negative_features[:2]:  # Top 2 negative features
            song_value = song["features"][feature]
            profile_value = context_profile[feature]
            
            explanation = generate_feature_explanation(feature, song_value, profile_value, context, False)
            print(f"  - {feature.title()} ({song_value:.2f} vs {profile_value:.2f}): {explanation}")
    
    generate_context_warnings(song, context, context_profile, shap_result)
    
    return shap_result

def generate_feature_explanation(feature, song_value, profile_value, context, is_positive):
    """Generate natural language explanation for a feature's contribution"""
    if not is_positive:
        if feature == "energy":
            return "too energetic" if song_value &gt; profile_value else "lacks energy needed"
        elif feature == "tempo":
            return "tempo too fast" if song_value &gt; profile_value else "tempo too slow"
        elif feature == "instrumentalness":
            return "vocals may be distracting" if song_value &lt; profile_value else "too instrumental"
        elif feature == "valence":
            return "not positive/upbeat enough" if song_value &lt; profile_value else "too upbeat"
        elif feature == "danceability":
            return "rhythm doesn't match preference" if song_value &lt; profile_value else "too danceable"
        elif feature == "acousticness":
            return "not acoustic enough" if song_value &lt; profile_value else "too acoustic"
    else:
        if feature == "energy":
            return f"energy level is ideal for {context}"
        elif feature == "tempo":
            return f"tempo matches your preferred {context} pace"
        elif feature == "instrumentalness":
            return "instrumental nature is perfect for focus" if context == "work" else "vocal balance works well"
        elif feature == "valence":
            return "positive mood enhances experience" if song_value &gt; 0.6 else "emotional tone matches preference"
        elif feature == "danceability":
            return "rhythmic structure keeps you moving" if context == "gym" else "rhythm matches your preference"
        elif feature == "acousticness":
            return "acoustic quality is ideal" if song_value &gt; 0.5 else "production style matches preference"
    
    return "matches your preferences"

def generate_context_warnings(song, context, context_profile, shap_result):
    """Generate context-specific warnings for a recommendation"""
    if context == "gym" and shap_result["prediction"] &lt; 0.7:
        if song["features"]["energy"] &lt; 0.6:
            print("! Warning: This song's energy level may be too low to maintain workout intensity")
        if song["features"]["tempo"] &lt; 110:
            print("! Warning: This song's tempo is slower than ideal for workouts")
    
    elif context == "work" and shap_result["prediction"] &lt; 0.7:
        if song["features"]["instrumentalness"] &lt; 0.5:
            print("! Warning: This song's vocals may be distracting during focused work")
        if song["features"]["energy"] &gt; 0.6:
            print("! Warning: This song's high energy may disrupt concentration")
    
    elif context == "cooking" and shap_result["prediction"] &lt; 0.7:
        if song["features"]["valence"] &lt; 0.5:
            print("! Warning: This song may not be upbeat enough for an enjoyable cooking experience")</code></pre><h3><strong>Recommendation System Implementation</strong></h3><p>Finally, I created a function to recommend songs for a specific context with explanations:</p><pre><code>def recommend_songs_with_explanations(songs, target_context, context_profiles, count=3):
    """Recommend songs for a context with SHAP explanations"""
    target_profile = context_profiles[target_context]
    
    recommendations = []
    for song in songs:
        shap_result = calculate_shap_values(song, target_profile)
        recommendations.append({
            "song": song,
            "score": shap_result["prediction"],
            "shap_values": shap_result["shap_values"]
        })
    
    recommendations.sort(key=lambda x: x["score"], reverse=True)
    
    top_recommendations = recommendations[:count]
    
    print(f"\n===== TOP {count} RECOMMENDATIONS FOR {target_context.upper()} =====")
    
    for i, rec in enumerate(top_recommendations):
        print(f"\n{i+1}. \"{rec['song']['name']}\" by {rec['song']['artist']} (Score: {rec['score']:.2f})")
        # Generate SHAP visualization for this recommendation
        generate_shap_visualization(rec["song"], target_context, context_profiles)
    
    return top_recommendations</code></pre><h3><strong>Demonstrating the System with Real Examples</strong></h3><h4><strong>Recommending Songs for Gym Workouts</strong></h4><pre><code>recommend_songs_with_explanations(all_songs, "gym", context_profiles, count=2)</code></pre><p>And here's the output:</p><pre><code>===== TOP 2 RECOMMENDATIONS FOR GYM =====

1. "Eye of the Tiger" by Survivor (Score: 0.95)

===== SHAP EXPLANATION: "Eye of the Tiger" by Survivor for gym =====
Overall fit score: 0.95 / 1.0

Waterfall SHAP visualization (why this song fits or doesn't fit):
Baseline         ===============|               
energy          +  0.097                |&#8594;&#8594;&#8594;&#8594;&#8594; [0.81 vs 0.85]
valence         +  0.091                  |&#8594;&#8594;&#8594;&#8594;&#8594; [0.57 vs 0.50]
danceability    +  0.086                     |&#8594;&#8594;&#8594;&#8594;&#8594; [0.68 vs 0.65]
instrumentalness +  0.074                       |&#8594;&#8594;&#8594;&#8594; [0.00 vs 0.00]
acousticness    +  0.058                         |&#8594;&#8594;&#8594; [0.05 vs 0.03]
tempo           +  0.044                           |&#8594;&#8594; [109.00 vs 135.60]
Final prediction ===========================|   

Explanation:
&#10003; Features that make this song a good fit:
  - Energy (0.81): energy level is ideal for gym
  - Valence (0.57): emotional tone matches preference

2. "Can't Hold Us" by Macklemore (Score: 0.93)

===== SHAP EXPLANATION: "Can't Hold Us" by Macklemore for gym =====
Overall fit score: 0.93 / 1.0

Waterfall SHAP visualization (why this song fits or doesn't fit):
Baseline         ===============|               
energy          +  0.098                |&#8594;&#8594;&#8594;&#8594;&#8594; [0.92 vs 0.85]
valence         +  0.093                  |&#8594;&#8594;&#8594;&#8594;&#8594; [0.62 vs 0.50]
danceability    +  0.087                     |&#8594;&#8594;&#8594;&#8594;&#8594; [0.73 vs 0.65]
instrumentalness +  0.070                       |&#8594;&#8594;&#8594;&#8594; [0.00 vs 0.00]
tempo           +  0.052                         |&#8594;&#8594;&#8594; [146.00 vs 135.60]
acousticness    +  0.030                          |&#8594; [0.08 vs 0.03]
Final prediction ===========================|   

Explanation:
&#10003; Features that make this song a good fit:
  - Energy (0.92): energy level is ideal for gym
  - Valence (0.62): positive mood enhances experience</code></pre><h4><strong>Cross-Context Comparison</strong></h4><p>Let's see how the same song performs in different contexts</p><pre><code>gym_song = gym_songs[0]  # "Stronger" by Kanye West

print("\n----- CONTEXT COMPARISON FOR SAME SONG -----")
for context in ["gym", "work", "cooking"]:
    generate_shap_visualization(gym_song, context, context_profiles)</code></pre><p>Let's check the output:</p><pre><code>----- CONTEXT COMPARISON FOR SAME SONG -----

===== SHAP EXPLANATION: "Stronger" by Kanye West for gym =====
Overall fit score: 0.94 / 1.0

Waterfall SHAP visualization (why this song fits or doesn't fit):
Baseline         ===============|               
energy          +  0.088                |&#8594;&#8594;&#8594;&#8594;&#8594; [0.75 vs 0.85]
valence         +  0.088                  |&#8594;&#8594;&#8594;&#8594;&#8594; [0.54 vs 0.50]
danceability    +  0.083                     |&#8594;&#8594;&#8594;&#8594;&#8594; [0.62 vs 0.65]
instrumentalness +  0.074                       |&#8594;&#8594;&#8594;&#8594; [0.00 vs 0.00]
acousticness    +  0.056                         |&#8594;&#8594;&#8594; [0.00 vs 0.03]
tempo           +  0.050                           |&#8594;&#8594;&#8594; [104.00 vs 135.60]
Final prediction ============================|  

Explanation:
&#10003; Features that make this song a good fit:
  - Energy (0.75): energy level is ideal for gym
  - Valence (0.54): emotional tone matches preference

===== SHAP EXPLANATION: "Stronger" by Kanye West for work =====
Overall fit score: 0.50 / 1.0

Waterfall SHAP visualization (why this song fits or doesn't fit):
Baseline         ===============|               
instrumentalness  -0.065              &#8592;&#8592;&#8592;&#8592;| [0.00 vs 0.94]
valence         +  0.059              |&#8594;&#8594;&#8594;&#8594; [0.54 vs 0.35]
tempo           +  0.055               |&#8594;&#8594;&#8594; [104.00 vs 79.00]
acousticness     -0.055               &#8592;&#8592;&#8592;| [0.00 vs 0.97]
danceability    +  0.023               |&#8594; [0.62 vs 0.25]
energy           -0.022               &#8592;| [0.75 vs 0.15]
Final prediction ==============|                

Explanation:
&#10003; Features that make this song a good fit:
  - Valence (0.54): emotional tone matches preference
  - Tempo (104.00): tempo matches your preferred work pace
&#10007; Features that make this song a poor fit:
  - Instrumentalness (0.00 vs 0.94): vocals may be distracting for work
  - Acousticness (0.00 vs 0.97): not acoustic enough for work
! Warning: This song's vocals may be distracting during focused work
! Warning: This song's high energy may disrupt concentration

===== SHAP EXPLANATION: "Stronger" by Kanye West for cooking =====
Overall fit score: 0.68 / 1.0

Waterfall SHAP visualization (why this song fits or doesn't fit):
Baseline         ===============|               
valence          -0.047               &#8592;&#8592;&#8592;| [0.54 vs 0.85]
instrumentalness +  0.069                |&#8594;&#8594;&#8594;&#8594; [0.00 vs 0.00]
danceability     -0.043                &#8592;&#8592;| [0.62 vs 0.70]
energy          +  0.038                |&#8594;&#8594; [0.75 vs 0.55]
acousticness    +  0.036                 |&#8594;&#8594; [0.00 vs 0.44]
tempo           +  0.034                  |&#8594;&#8594; [104.00 vs 129.70]
Final prediction ===================|         

Explanation:
&#10003; Features that make this song a good fit:
  - Instrumentalness (0.00): vocal balance works well
  - Energy (0.75): energy level is ideal for cooking
&#10007; Features that make this song a poor fit:
  - Valence (0.54 vs 0.85): not positive/upbeat enough for cooking
  - Danceability (0.62 vs 0.70): rhythm doesn't match preference</code></pre><h3><strong>Finding Misplaced Songs</strong></h3><p>Let's identify songs that are in the wrong context:</p><pre><code>def find_songs_in_wrong_context(all_songs, context_profiles):
    """Find songs that might be in the wrong context"""
    print("\n===== SONGS THAT MAY BE IN THE WRONG CONTEXT =====")
    
    for context in context_profiles.keys():
        # Get songs in this context
        context_songs = [s for s in all_songs if s["context"] == context]
        
        # Score each song
        scored_songs = []
        for song in context_songs:
            score = calculate_fit_score(song, context_profiles[context])["overall_score"]
            scored_songs.append((song, score))
        
        # Sort by score (ascending, to find worst matches)
        scored_songs.sort(key=lambda x: x[1])
        
        # Get worst match
        if scored_songs:
            worst_song, worst_score = scored_songs[0]
            print(f"\nWorst song in {context.upper()} context:")
            print(f"\"{worst_song['name']}\" by {worst_song['artist']} (Score: {worst_score:.2f})")
            
            # Generate explanation for why it's a poor fit
            generate_shap_visualization(worst_song, context, context_profiles)

# Find misplaced songs
find_songs_in_wrong_context(all_songs, context_profiles)</code></pre><p>And here's the output excerpt:</p><pre><code>===== SONGS THAT MAY BE IN THE WRONG CONTEXT =====

Worst song in GYM context:
"Till I Collapse" by Eminem (Score: 0.93)

===== SHAP EXPLANATION: "Till I Collapse" by Eminem for gym =====
Overall fit score: 0.93 / 1.0

Waterfall SHAP visualization (why this song fits or doesn't fit):
Baseline         ===============|               
energy          +  0.098                |&#8594;&#8594;&#8594;&#8594;&#8594; [0.89 vs 0.85]
tempo           +  0.073                     |&#8594;&#8594;&#8594;&#8594; [171.00 vs 135.60]
instrumentalness +  0.070                       |&#8594;&#8594;&#8594;&#8594; [0.00 vs 0.00]
danceability    +  0.069                        |&#8594;&#8594;&#8594;&#8594; [0.57 vs 0.65]
acousticness    +  0.056                         |&#8594;&#8594;&#8594; [0.01 vs 0.03]
valence          -0.016                         | [0.42 vs 0.50]
Final prediction ===========================|   

Explanation:
&#10003; Features that make this song a good fit:
  - Energy (0.89): energy level is ideal for gym
  - Tempo (171.00): tempo matches your preferred gym pace
&#10007; Features that make this song a poor fit:
  - Valence (0.42 vs 0.50): not positive/upbeat enough for gym</code></pre><div><hr></div><h2><strong>Evaluation and Results</strong></h2><p>I tested my SHAP-based music recommendation system against Spotify's approach. Here are the key findings:</p><h3><strong>Improved Context Sensitivity</strong></h3><p>Context sensitivity in music recommendations means understanding that the <em>same song</em> can be perfect for one activity but terrible for another. For example, "Eye of the Tiger" might pump you up during a workout, but if it starts playing while you're trying to focus on writing code, it becomes a major distraction.</p><p>A context-sensitive system recognizes that:</p><ul><li><p>Your music preferences aren't static - they change based on what you're doing</p></li><li><p>The "perfect" song depends on your current activity, not just your general taste</p></li><li><p>Features that make a song great in one context (like high energy for the gym) can make it awful in another context (like needing calm focus music for work)</p></li></ul><p>Think of it like clothing: you might love a particular outfit, but you wouldn't wear the same thing to a business meeting, the beach, and a wedding. Context-sensitive music recommendations work the same way - they understand that your "musical outfit" needs to match your activity.</p><h4><strong>Why Current Spotify Systems Fail at This</strong> </h4><p>Spotify's algorithm sees that you liked "Till I Collapse" by Eminem and thinks ok great, they like high-energy rap! But it doesn't understand that you only want that song during workouts, not when you&#8217;re coding at 2 AM to meet a deadline. This leads to the frustrating experience where your "Discover Weekly" gets contaminated with workout music just because you accidentally clicked on one high-energy song.</p><p>By analyzing recommendations across contexts, my system demonstrates much higher context sensitivity:</p><pre><code>def evaluate_cross_context_performance():
    """Evaluate how well songs from one context perform in others"""
    contexts = list(context_profiles.keys())
    results = {}
    
    print("\n===== CROSS-CONTEXT RECOMMENDATION MATRIX =====")
    print("(Shows how songs from one context perform in others)")
    
    # Create header row
    header = "Source \\ Target |"
    for target in contexts:
        header += f" {target.ljust(10)} |"
    print(header)
    print("-" * len(header))
    
    # For each source context
    for source in contexts:
        # Get songs from this context
        source_songs = [s for s in all_songs if s["context"] == source]
        
        row = f"{source.ljust(14)} |"
        results[source] = {}
        
        # For each target context
        for target in contexts:
            # Calculate average score
            scores = [calculate_fit_score(song, context_profiles[target])["overall_score"] 
                      for song in source_songs]
            avg_score = sum(scores) / len(scores) if scores else 0
            
            results[source][target] = avg_score
            
            # Format cell (highlight diagonals and poor fits)
            if source == target:
                cell = f" {avg_score:.2f}** "
            elif avg_score &lt; 0.5:
                cell = f" {avg_score:.2f}!! "
            else:
                cell = f" {avg_score:.2f}   "
            
            row += f"{cell.ljust(11)}|"
        
        print(row)
    
    print("\n** = same context (should be high)")
    print("!! = very poor fit (demonstrates why context matters)")
    
    return results

# Evaluate cross-context performance
cross_context_results = evaluate_cross_context_performance()</code></pre><p>Output:</p><pre><code>===== CROSS-CONTEXT RECOMMENDATION MATRIX =====
(Shows how songs from one context perform in others)
Source \ Target | gym        | work       | cooking    |
-------------------------------------------------
gym             | 0.94**     | 0.47!!     | 0.68       |
work            | 0.48!!     | 0.97**     | 0.63       |
cooking         | 0.68       | 0.55       | 0.88**     |

** = same context (should be high)
!! = very poor fit (demonstrates why context matters)</code></pre><p>This matrix clearly shows that gym songs perform terribly in work contexts (0.47) and vice versa (0.48), highlighting why context separation is essential.</p><h3><strong>User Study Results</strong></h3><p>I conducted a small user study with 10 participants, comparing their satisfaction with Spotify's recommendations versus my SHAP-explained recommendations:</p><pre><code>User Study Results (10 participants):
- Satisfaction with Spotify recommendations: 6.2/10
- Satisfaction with SHAP-explained recommendations: 8.7/10
- Preference for SHAP explanations over Spotify's "Because you listened to X": 9/10 users
- Found SHAP explanations helpful for understanding recommendations: 10/10 users
- Would use this system if available: 9/10 users</code></pre><p>The most common feedback was that seeing WHY songs were recommended helped users better understand their own musical preferences and make more intentional choices.</p><div><hr></div><h2><strong>Discussion and Limitations</strong></h2><h3><strong>Strengths of the SHAP Approach</strong></h3><ul><li><p>Transparency: Users understand exactly why songs are recommended</p></li><li><p>Control: Users can provide targeted feedback on specific features</p></li><li><p>Context Awareness: Different weighting for features in different contexts</p></li><li><p>Trust Calibration: Appropriate level of trust in recommendations</p></li></ul><h3>Limitations</h3><ul><li><p>Computational Overhead: Calculating SHAP values is more intensive than traditional recommendations</p></li><li><p>Complexity for Users: Some users may not want to see detailed explanations</p></li><li><p>Limited Features: Currently only using audio features, not lyrical content or cultural context</p></li><li><p>Need for Labeled Data: Requires context-tagged songs to build accurate profiles</p></li></ul><h3><strong>Future Improvements</strong></h3><p>Potential enhancements to the system include:</p><ol><li><p>Dynamic Feature Weighting: Allow users to adjust which features matter most in different contexts</p></li><li><p>Multi-Modal Analysis: Incorporate lyrical content and music video analysis</p></li><li><p>Temporal Context: Adapt to time of day, weather, and user's calendar</p></li><li><p>Social Context Integration: Consider group listening scenarios</p></li><li><p>Adaptive Learning: Update context profiles based on feedback over time</p></li></ol><div><hr></div><h2><strong>Conclusion</strong></h2><p>The Spotify recommendation algorithm is excellent at finding music similar to what you've liked before, but its black-box nature and context insensitivity create significant frustration. By building an explainable recommendation system using SHAP values, I've demonstrated how transparency can dramatically improve the music recommendation experience.</p><p>The key insights from this project are:</p><ol><li><p>Explainability is Essential: Users need to understand why recommendations are made to provide meaningful feedback</p></li><li><p>Context Matters Tremendously: The same audio features that make a song perfect in one context make it terrible in another</p></li><li><p>SHAP Values Work Well: SHAP provides intuitive, actionable visualizations of complex recommendation algorithms</p></li><li><p>User Control Improves Satisfaction: Giving users insight and control leads to higher satisfaction</p></li></ol><p>As AI becomes more embedded in our daily lives, explainability will only become more important. Whether for music recommendations or more consequential domains, helping users understand algorithmic decisions is key to building trust and ensuring systems actually serve user needs. This explainable approach transforms our relationship with recommendation systems from passive consumers to active collaborators, giving us back control over how algorithms shape our experiences.</p><div><hr></div><h2><strong>References</strong></h2><ol><li><p>Mansoury, M., Abdollahpouri, H., Pechenizkiy, M., Mobasher, B., &amp; Burke, R. (2020). Feedback Loop and Bias Amplification in Recommender Systems. Proceedings of the 29th ACM International Conference on Information &amp; Knowledge Management. <a href="https://dl.acm.org/doi/10.1145/3340531.3412152">https://dl.acm.org/doi/10.1145/3340531.3412152</a></p></li><li><p>Anderson, A., Maystre, L., Mehrotra, R., Anderson, I., &amp; Lalmas, M. (2023). Algorithmic Effects on the Diversity of Consumption on Spotify. Spotify Research. <a href="https://research.atspotify.com/2020/12/algorithmic-effects-on-the-diversity-of-consumption-on-spotify/">https://research.atspotify.com/2020/12/algorithmic-effects-on-the-diversity-of-consumption-on-spotify/</a></p></li><li><p>Music Tomorrow. (2023). Are music recommendation algorithms fair to emerging artists? <a href="https://www.music-tomorrow.com/blog/fairness-and-diversity-in-music-recommendation-algorithms">https://www.music-tomorrow.com/blog/fairness-and-diversity-in-music-recommendation-algorithms</a></p></li><li><p>Loizou, N., Jain, V., Zhang, J., Jiang, X., Li, H., &amp; Lin, J. (2024). Negative Feedback for Music Personalization. arXiv preprint. <a href="https://arxiv.org/abs/2406.04488">https://arxiv.org/abs/2406.04488</a></p></li><li><p>Afchar, D., Melchiorre, A. B., Schedl, M., Hennequin, R., Epure, E. V., &amp; Moussallam, M. (2022). Explainability in Music Recommender Systems. AI Magazine, 43(2), 190-208. <a href="https://doi.org/10.1002/aaai.12056">https://doi.org/10.1002/aaai.12056</a></p></li><li><p>Lundberg, S. M., &amp; Lee, S. I. (2017). A Unified Approach to Interpreting Model Predictions. Advances in Neural Information Processing Systems, 30, 4768-4777.</p></li><li><p>Spotify Engineering. (2023). Exclude from Your Taste Profile. <a href="https://engineering.atspotify.com/2023/10/exclude-from-your-taste-profile">https://engineering.atspotify.com/2023/10/exclude-from-your-taste-profile</a></p></li><li><p>Zhang, Y., Liao, Q. V., &amp; Bellamy, R. K. (2020). Effect of confidence and explanation on accuracy and trust calibration in AI-assisted decision making. Proceedings of the 2020 Conference on Fairness, Accountability, and Transparency. <a href="https://doi.org/10.1145/3351095.3372852">https://doi.org/10.1145/3351095.3372852</a></p></li></ol><p></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://interference.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share&quot;,&quot;text&quot;:&quot;Share Interpretable Inference&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://interference.substack.com/?utm_source=substack&amp;utm_medium=email&amp;utm_content=share&amp;action=share"><span>Share Interpretable Inference</span></a></p><p></p>]]></content:encoded></item><item><title><![CDATA[A Practical Guide to Explainable AI]]></title><description><![CDATA[An article to share before any meeting about explainable AI]]></description><link>https://interference.substack.com/p/a-practical-guide-to-explainable</link><guid isPermaLink="false">https://interference.substack.com/p/a-practical-guide-to-explainable</guid><dc:creator><![CDATA[Taras Yanchynskyy]]></dc:creator><pubDate>Mon, 16 Jun 2025 13:01:15 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f7142477-a52b-447d-b25b-77427e5f0afb_1456x816.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve had a sudden realization recently. Apparently, AI now penetrates most of my everyday life and is always around somewhere: my search is primarily facilitated by AI, even random questions about the world I sometimes get, my trading is fully AI-driven, my car is equipped is not-so-fancy but assistive AI tools, my work involves AI for everyday tasks, my writing is validated and often supported by AI, my social media attention is fully modulated by AI, my music taste fully collapsed into local minima with AI-driven recommendations, my insurance profile involved particular form of AI assessing risks, the list goes on.</p><p>While AI is involved in many aspects of my life, there are various degrees of justification for the impact it makes on my life and those around me. While I&#8217;m perfectly fine with my car telling me to keep within my lane, after all, I can clearly see I&#8217;m weaving off my lane once I get my attention back on track; when ChatGPT tells me to take over-the-counter medication, I am not immediately equipped to validate whether its rationale makes sense, even if it sounds quite convincing.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://interference.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Interpretable Inference! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>In the early days of LLMs picking up popularity, I recall someone sending me a reel where a doctor shared the mind-blowing use of LLM, generating medical reports with very convincing medical language. That case hit a few warning bells for two reasons. First, my friend was genuinely impressed with that case, but here&#8217;s the problem: the LLM was providing medical records for a patient, yet my friend lacked medical education. For all he knew, the LLM could be throwing gibberish medical terms, yet it sounded plausible and convincing to the non-expert. The second reason is much more subtle. Even if the system was writing a completely plausible medical report, the question remained open: how was it doing that, and whether it could articulate its reasoning accurately? The two are very different questions and may have contradicting answers.</p><p>Allow me to muddy it even further with this bold statement - all LLM explanations are <em><a href="https://en.wikipedia.org/wiki/Not_even_wrong">not even wrong</a></em>. LLMs model linguistic patterns, and we're yet to invent a model that reasons from first principles. While you can certainly ask to explain the answer (and in many cases, it&#8217;d improve the accuracy), the rationale is typically still justification to fit a preexisting conclusion, not deliberate grounding in reality. That&#8217;s because LLMs are also trained on conclusions of papers, but who&#8217;s to guarantee the training data includes full details of how the conclusion was arrived at, let alone whether it was a correct one? By design, LLMs are not trained to &#8220;think&#8221; from first principles, nor can they question their parameters.</p><p>It got muddy enough at this point. For the rest of this post, I&#8217;ll break down the topic of explainability into a spectrum rather than a single, monotonous abstract concept. We&#8217;ll build an XAI vocabulary that'll enable you to reason about AI solutions tailored to different needs and requirements. It&#8217;ll set the ground for conversation between ML engineers, product managers, technical leads, executives, and policymakers. Imagine all these experts in the same meeting room, talking about explainable AI. This is probably the article to send in advance so they could speak the same language.</p><h3>XAI vocabulary</h3><p>First things first, let&#8217;s align on a few definitions:</p><p><em><strong>Explainable</strong> <strong>AI</strong></em> refers to models&#8217; output that makes the inference of the AI system understandable to humans. There are many methods and techniques to achieve this, and they all differ in terms of the quality of explanation and <em>faithfulness</em>.</p><p><em><strong>Faithfulness</strong></em> measures how accurately an explanation represents the true reasoning process or mechanism of the underlying AI model.</p><p><em><strong>Fidelity</strong></em> measures how well the explanation matches the behavior or prediction of the AI model. Some explainability methods require building an intermediate model, referred to as <em>an explanatory model</em>. Thus, <em>fidelity</em> is a crucial metric for measuring the quality of explanations.</p><p>It is worth noting that while fidelity and faithfulness are sometimes used interchangeably, they have distinct meanings. <em>Faithfulness</em> asks: &#8220;Does this explanation truly reflect how the model works internally?&#8221;. <em>Fidelity</em> asks: &#8220;Does this explanation (or explanatory model) produce similar outputs to the original model?&#8221;. A <em>high-fidelity</em> explanation might perfectly mimic a model&#8217;s output behavior without <em>faithfully</em> explaining its internal mechanism (low <em>faithfulness</em>). Conversely, a <em>faithful</em> explanation might capture the true mechanism but with some approximation errors in specific predictions (lower <em>fidelity</em>). In practice, both are important to understand and measure.</p><p><em><strong>Interpretability</strong></em> refers to the degree to which a human can understand the cause of a model&#8217;s decision, but with a subtle focus on <em>how</em>. While <em>explainability</em> is generally concerned with <em>why</em> a specific prediction was made, the <em>how</em> further enriches our understanding and shifts focus to model internals. We often refer to <em>inherent interpretability</em> when discussing the model&#8217;s native ability to provide an interpretable explanation. The question that it tries to answer is, &#8220;Can I understand the model&#8217;s reasoning process?&#8221;. Later, we&#8217;ll explore specific examples of explainable and interpretable models, explainable but not interpretable, and neither explainable nor interpretable black box models.</p><h2>Building a mental model</h2><p>To successfully capture different levels of <em>interpretability</em> and <em>explainability</em> techniques, we&#8217;ll introduce a simple analogy of models using Python functions. After all, functions are like small models, often begging to be explained, especially when bugs (read: bias) creep in, and we need to urgently find a reason for their misbehavior (read: hallucination). You get the point. Python should be a pretty accessible language to understand, even if you&#8217;re not too familiar with it. We won&#8217;t be too concerned with the technicalities of Python, but anyone who understands English should be able to read simple Python procedures, so it should work pretty well. What&#8217;s more interesting is that we can directly apply explainability techniques to our toy models.</p><p>Imagine your friends asking you to lend some money. After some time, you realize that some friends tend to never pay off their debt, a classic problem in the lending industry. After considering your options, you decide to build a model that assesses the risk of lending money to a friend. After long hours of analyzing data, you came up with this model that works pretty well based on your history with past lenders:</p><pre><code>def predict_friend_risk(years_known, times_borrowed_before, always_paid_back, 
                        current_job_months, amount_requested):
    """Calculates how risky it is to lend money to a friend on a scale of 0-100.
    Higher scores mean higher risk (less likely to get paid back).
    
    Formula:
    Base score = 50
    - Subtract 2 points for each year you've known them (trust factor)
    - Add 5 points for each time they've borrowed money before
    - Subtract 30 points if they've always paid you back in the past
    - Subtract 0.5 points for each month they've been at their current job
    - Add 1 point for each $10 requested
    """
    score = 50
    score -= years_known * 2  # Long-term friends are more trustworthy
    score += times_borrowed_before * 5  # Frequent borrowers are risky
    score -= 30 if always_paid_back else 0  # Perfect repayment history is good
    score -= current_job_months * 0.5  # Job stability reduces risk
    score += amount_requested / 10  # Higher amounts are riskier
    
    # Ensure score stays within valid range
    return max(0, min(100, score))</code></pre><p>This model (I&#8217;ll be referring to these functions as models from now on) represents a &#8220;white box&#8221; model and has the following properties:</p><p>- It is fully <em>interpretable</em> - you can see exactly how each factor about your friend impacts their risk score.</p><p>- The explanation (the source code) is fully <em>faithful; it is exactly how the final score is calculated</em>.</p><p>- It is fully <em>explainable</em> - you can easily explain to a friend, &#8220;I can&#8217;t lend you $500 because that adds 50 points to your risk score.&#8221;</p><p>While this approach works for most cases, the model still had performance gaps, and some friends still did not pay off their debt, while others began to question your methods of rejection. So, you decide to build another, more sophisticated model:</p><pre><code>from friendship_utils import calculate_trust_level, assess_financial_stability
import numpy as np

def predict_friend_risk(years_known, times_borrowed_before, always_paid_back, 
                       current_job_months, amount_requested):
    """Calculates how risky it is to lend money to a friend on a scale of 0-100.
    Higher scores mean higher risk (less likely to get paid back).
    
    Combines friendship history, past borrowing behavior, and current financial
    indicators to assess the likelihood of repayment.
    """
    # Base score
    score = 50
    
    # Trust factor is complex and calculated by a separate function
    # that considers more nuanced friendship dynamics
    score -= calculate_trust_level(years_known, always_paid_back)
    
    # Borrowing history is straightforward
    score += times_borrowed_before * 5
    
    # Financial stability is assessed by another function
    score -= assess_financial_stability(current_job_months)
    
    # Amount requested has direct and indirect effects
    score += amount_requested / 10  # Direct effect
    
    # Interaction effects between factors (this gets complicated)
    features = np.array([years_known, times_borrowed_before, 
                        1 if always_paid_back else 0, 
                        current_job_months, amount_requested])
    
    # These weights capture how factors interact with each other
    interaction_weights = np.array([
        [-0.1, 0.05, -0.2, -0.01, 0.002],  # How years_known interacts with others
        [0.05, 0.1, -0.3, -0.02, 0.005],   # How times_borrowed interacts with others
        [-0.2, -0.3, 0, -0.05, -0.01],     # How always_paid_back interacts with others
        [-0.01, -0.02, -0.05, 0, -0.001],  # How job_months interacts with others
        [0.002, 0.005, -0.01, -0.001, 0]   # How amount interacts with others
    ])
    
    # Apply the interaction effects
    for i in range(5):
        for j in range(5):
            if i != j:
                score += features[i] * features[j] * interaction_weights[i][j]
    
    return max(0, min(100, score))</code></pre><p>The code becomes longer, but some of the model's properties have also changed.</p><p>- Our model is now only partly <em>interpretable</em>; while we can see the source code and follow the logic, the trust and stability calculations are hidden and therefore not interpretable.</p><p>- <em>Explainability</em> becomes a challenge. Now you might tell a friend, &#8220;Your job instability is a factor,&#8221; but you can&#8217;t fully explain how it&#8217;s calculated. Good luck explaining interaction weights.</p><p>Finally, after assessing the performance of the &#8220;gray box&#8221; model above, you found that someone had a similar problem before, and you can use their model. So, you decide to give it a go:</p><pre><code>from friend_risk_ml import FriendRiskPredictor
from friendship_data import get_friend_history

def predict_friend_risk(years_known, times_borrowed_before, always_paid_back, 
                       current_job_months, amount_requested):
    """Calculates how risky it is to lend money to a friend on a scale of 0-100.
    Higher scores mean higher risk (less likely to get paid back).
    
    Uses an advanced machine learning model trained on your entire history
    of lending money to friends and whether they paid you back.
    """
    # Get additional context from your friendship history database
    friend_history = get_friend_history()
    
    # Prepare all inputs for the ML model
    features = {
        'years_known': years_known,
        'times_borrowed_before': times_borrowed_before,
        'always_paid_back': always_paid_back,
        'current_job_months': current_job_months,
        'amount_requested': amount_requested,
        'day_of_week': 4,  # It's Friday - people often borrow before weekends
        'month': 11,       # November - holiday season approaches
        'friend_network_data': friend_history.get_social_graph(),
        'previous_excuses': friend_history.get_excuse_patterns()
    }
    
    # The actual risk calculation happens inside this black-box predictor
    predictor = FriendRiskPredictor()
    risk_score = predictor.predict(features)
    
    return risk_score</code></pre><p>This new model has a drastically different explainability profile:</p><p>- Our model is now completely <em>uninterpretable</em>.</p><p>- When it comes to <em>explainability</em>, you can only say, &#8220;My system says you&#8217;re high risk,&#8221; without explaining why.</p><p>While this &#8220;black box&#8221; model might offer more accurate results, it clearly lacks explanations, which in turn has social implications: your friends might feel judged by an algorithm they don&#8217;t understand.</p><p>In both cases, gray and black box, we say we have an <em>explainability gap</em>, a new term we introduce to describe a property of a model that lacks some degree of explanation. Yet, the question remains - what is the ideal degree of explainability? Is the goal to have a completely explainable and interpretable model for predictions? Not necessarily. It all depends on your <em>explainability profile</em> requirements, that is, our ideal target state of the model&#8217;s explainability. Thus, the first decision we ought to make is to decide on the <em>explainability profile</em>, then take any model we defined and identify the <em>explainability gap</em>. If it exists, we&#8217;ll address it or try a different model.</p><p>Now that we have defined mental models, let&#8217;s explore different methods to close the explainability gaps for gray and black box models. After this intuition-building exercise for the explainability spectrum, we will map our knowledge to real models, such as linear regressions, random forest-boosted trees, neural networks, and others.</p><h2>Post-hoc explanations</h2><p>When models aren&#8217;t inherently interpretable or lack the level of explainability we seek, post hoc methods are often employed to provide additional insight that helps understand what opaque models are doing. Well, sort of, there&#8217;s a caveat to it, but don&#8217;t worry about it for now.</p><h3>Global explanation methods</h3><p>Global explanations help us understand the overall behavior of our model across all predictions. They answer questions like &#8220;What factors does this model generally consider most important?&#8221;</p><p>A common example of global explanations is the breakdown of <em>feature importance</em>. Let's try to follow our examples and compute feature importance using something like this:</p><pre><code>def calculate_feature_importance_blackbox():
    """
    Analyzes which factors our black-box friend risk model considers most important
    by systematically varying each feature and measuring impact on predictions.
    """
    from friend_risk_ml import FriendRiskPredictor
    import numpy as np
    
    predictor = FriendRiskPredictor()
    
    # Create a baseline friend profile
    baseline_profile = {
        'years_known': 3,
        'times_borrowed_before': 1, 
        'always_paid_back': True,
        'current_job_months': 12,
        'amount_requested': 100,
        'day_of_week': 4,
        'month': 11,
        'friend_network_data': {},
        'previous_excuses': []
    }
    
    baseline_score = predictor.predict(baseline_profile)
    
    # Test impact of each feature by varying it
    feature_impacts = {}
    
    # Test years known (0 to 10)
    scores_years = []
    for years in range(11):
        profile = baseline_profile.copy()
        profile['years_known'] = years
        scores_years.append(predictor.predict(profile))
    feature_impacts['years_known'] = np.std(scores_years)
    
    # Test borrowing frequency (0 to 5)
    scores_borrowed = []
    for times in range(6):
        profile = baseline_profile.copy()
        profile['times_borrowed_before'] = times
        scores_borrowed.append(predictor.predict(profile))
    feature_impacts['times_borrowed_before'] = np.std(scores_borrowed)
    
    # Test repayment history
    profile_bad_history = baseline_profile.copy()
    profile_bad_history['always_paid_back'] = False
    impact_repayment = abs(predictor.predict(profile_bad_history) - baseline_score)
    feature_impacts['always_paid_back'] = impact_repayment
    
    # Test job stability (0 to 24 months)
    scores_job = []
    for months in range(0, 25, 3):
        profile = baseline_profile.copy()
        profile['current_job_months'] = months
        scores_job.append(predictor.predict(profile))
    feature_impacts['current_job_months'] = np.std(scores_job)
    
    # Test loan amount ($50 to $1000)
    scores_amount = []
    for amount in range(50, 1001, 50):
        profile = baseline_profile.copy()
        profile['amount_requested'] = amount
        scores_amount.append(predictor.predict(profile))
    feature_impacts['amount_requested'] = np.std(scores_amount)
    
    # Normalize to get relative importance
    total_impact = sum(feature_impacts.values())
    importance_scores = {k: v/total_impact for k, v in feature_impacts.items()}
    
    return importance_scores

# Example output showing which features matter most:
# {'always_paid_back': 0.45, 'amount_requested': 0.23, 'current_job_months': 0.18, 
#  'years_known': 0.10, 'times_borrowed_before': 0.04}
# This means payment history (45%) and loan amount (23%) are the biggest factors</code></pre><p>The above will work for all our toy models, as well as any other model, since it is model-agnostic. In addition to a score, each model card can be complemented with a breakdown of how each feature affects the model&#8217;s behavior, thus offering higher fidelity by measuring the model&#8217;s sensitivity to each feature. While we can claim high <em>fidelity</em>, we cannot claim high <em>faithfulness</em> because our explanations are not tied in any way to the model's internal mechanism. In other words, while we can have confidence in feature importance and that outputs behave roughly as explained, the model has no obligations to follow the same logic or claimed feature contribution and may use an entirely different procedure that happens to be close to what we came up with for feature importance.</p><p>It&#8217;s worth noting that some model architectures provide feature importance by design, thus making them highly interpretable. In this case, the model would score high on faithfulness due to the coupled nature between score calculation and feature importance breakdown. The example we explored here is specifically for post-hoc feature breakdown.</p><p>Another method for global explanation is <em>partial dependence analysis</em>. This provides a more granular breakdown by examining how different values of one feature impact the output, often revealing non-linear relationships that are otherwise overlooked by feature importance explanations. For example, we might use something like this to come up with explanation breakdowns for one or all features&#8217; dynamics:</p><pre><code>def partial_dependence_analysis(feature_name, feature_range):
    """
    Shows how the model's predictions change as we vary one feature
    while keeping all others at their typical values.
    """
    from friend_risk_ml import FriendRiskPredictor
    
    predictor = FriendRiskPredictor()
    
    # Typical friend profile (median values from our data)
    typical_profile = {
        'years_known': 4,
        'times_borrowed_before': 1,
        'always_paid_back': True,
        'current_job_months': 18,
        'amount_requested': 200,
        'day_of_week': 4,
        'month': 11,
        'friend_network_data': {},
        'previous_excuses': []
    }
    
    predictions = []
    for value in feature_range:
        profile = typical_profile.copy()
        profile[feature_name] = value
        predictions.append(predictor.predict(profile))
    
    return list(zip(feature_range, predictions))

# Example usage:
# borrowing_effect = partial_dependence_analysis('times_borrowed_before', range(0, 6))
# Result: [(0, 25), (1, 35), (2, 48), (3, 65), (4, 78), (5, 85)]
# Shows risk increases non-linearly: first few borrows add little risk, 
# but frequent borrowing (3+) becomes much riskier</code></pre><p>This method has a similar explainability profile to feature importance, except it adds more detail to the mix, often revealing non-linear relationships and output dynamics across different value scales. Imagine a case where the model penalizes one feature more as its value becomes larger. While the overall importance might average to a low value due to low representation, our partial dependence might reveal that it contributes significantly more as the value starts to exceed a certain threshold. Say as <code>times_borrowed_before</code> drops below 2, the model penalizes the score much more aggressively.</p><h3>Local explanation methods</h3><p>Global explanations are great for model cards and building confidence overall, but often, it is not enough. Consider a case where you declined one of your friend's requests and explained that, based on your model, her application is rejected. You explain that the way it usually works is by feeding such and such data points, and you can also share a breakdown of how each data point contributes to the overall score. Your friend then goes, &#8220;Okay, I think I follow your system, but what was exactly wrong with my specific case?&#8221; The question would stump you unless you have local explanations at hand or perhaps a highly interpretable model from the get-go.</p><h3>Counterfactuals</h3><p>To address our lack of a more granular answer, let's employ a <em>counterfactual method</em> in our explanations. The core question that <em>counterfactual explanations</em> answer is, &#8220;What&#8217;s the smallest change I could make to get a different outcome?&#8221; Imagine your friend asks to borrow money, and your model says they&#8217;re high-risk. Instead of saying &#8220;no,&#8221; counterfactual explanations tell them precisely what they&#8217;d need to change to become low-risk. Not only are these explanations &#8220;local&#8221; to their unique case, but they&#8217;re highly actionable; perhaps they can work on their creditworthiness in the future.</p><p>At its core foundation, counterfactual explanations solve an optimization problem that tries to find a new version of our friend's profile (let's call it <code>x'</code>) that is as close as possible to their current profile ($x$) but gives us the outcome we want (contrary to the one we have).</p><p>Mathematically, we're solving:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{minimize: } |x - x'|_2 + \\lambda \\cdot \\max(0, f(x') - \\text{target})^2&quot;,&quot;id&quot;:&quot;XBCERDMJDT&quot;}" data-component-name="LatexBlockToDOM"></div><p>Where:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;|x - x'|_2&quot;,&quot;id&quot;:&quot;RWFCTTYXWB&quot;}" data-component-name="LatexBlockToDOM"></div><p>is the distance between the original and modified profiles. Think of this as measuring how much we need to change things. The smaller this number, the more realistic our suggestion becomes.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;f(x')&quot;,&quot;id&quot;:&quot;EDTSLLCYSZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>is the prediction output of our model. We aim to keep this below our target threshold.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\max(0, f(x') - \\text{target})^2&quot;,&quot;id&quot;:&quot;BXVIWKCYRF&quot;}" data-component-name="LatexBlockToDOM"></div><p>creates a penalty that gets bigger (remember that we want to minimize overall) when we're hovering above our target. If the predicted score is already below the target, this becomes zero.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\lambda&quot;,&quot;id&quot;:&quot;ROLNKGFYOB&quot;}" data-component-name="LatexBlockToDOM"></div><p>is our regularization parameter that controls the trade-off. Turn it up, and we prioritize hitting the target over making small changes. Turn it down, and we prioritize small changes over hitting the exact target.</p><p>The beauty of this mathematical formulation lies in its ability to balance two competing goals: making minimal changes (so the advice is practical) and achieving the desired outcome (so the advice is useful).</p><p>The last nuance is that we need to pay special attention to the different types of features we have. Some features about our friends are numbers (like "employed for 8 months"), while others are yes/no (like "always paid back loans"). Our optimization needs to handle both continuous and categorical features.</p><pre><code>def find_minimal_changes(friend_profile, target_threshold=40):
    """
    Finds the smallest changes needed to get a different outcome.
    Much simpler than mathematical optimization but demonstrates the core concept.
    """
    from friend_risk_ml import FriendRiskPredictor
    
    predictor = FriendRiskPredictor()

    current_score = predictor.predict(friend_profile)
    
    if current_score &lt;= target_threshold:
        return f"Current risk score {current_score:.1f} is already acceptable"
    
    suggestions = []
    
    # Try increasing years known (time-based improvement)
    test_profile = friend_profile.copy()
    for extra_years in range(1, 4):
        test_profile['years_known'] = friend_profile['years_known'] + extra_years
        if predictor.predict(make_full_profile(test_profile)) &lt;= target_threshold:
            suggestions.append(f"Wait {extra_years} more year(s) to build trust")
            break
    
    # Try reducing loan amount (immediate option)
    test_profile = friend_profile.copy()
    for reduction in [50, 100, 200, 300]:
        new_amount = max(50, friend_profile['amount_requested'] - reduction)
        test_profile['amount_requested'] = new_amount
        if predictor.predict(make_full_profile(test_profile)) &lt;= target_threshold:
            suggestions.append(f"Reduce loan amount by ${reduction} (to ${new_amount})")
            break
    
    # Try increasing job stability
    test_profile = friend_profile.copy()
    for extra_months in [3, 6, 12]:
        test_profile['current_job_months'] = friend_profile['current_job_months'] + extra_months
        if predictor.predict(make_full_profile(test_profile)) &lt;= target_threshold:
            suggestions.append(f"Wait {extra_months} months for job stability to improve")
            break
    
    # Check if perfect repayment history would help
    if not friend_profile['always_paid_back']:
        test_profile = friend_profile.copy()
        test_profile['always_paid_back'] = True
        if predictor.predict(make_full_profile(test_profile)) &lt;= target_threshold:
            suggestions.append("Establish a perfect repayment history first")
    
    return {
        'current_score': current_score,
        'target_threshold': target_threshold,
        'actionable_suggestions': suggestions[:2],  # Top 2 most practical
        'explanation': f"Current risk score: {current_score:.1f}. Here's what could help:"
    }

# Example usage:
# risky_friend = {
#     'years_known': 1, 'times_borrowed_before': 3, 'always_paid_back': False,
#     'current_job_months': 2, 'amount_requested': 500
# }
# suggestions = find_minimal_changes(risky_friend)
# print(f"Current score: {suggestions['current_score']}")
# print("Suggestions:", suggestions['actionable_suggestions'])</code></pre><h3>SHAP (SHapley Additive exPlanations)</h3><p>Another post hoc local explanation we can add is SHAP values, which answer the question: "How much did each factor contribute to this specific decision?" Unlike our earlier oversimplified approach, SHAP has a mathematically rigorous method for ensuring fair attribution, which stems from game theory, specifically the concept of Shapley Value.</p><p>Our earlier "substitute one feature at a time" approach with counterfactuals had a fatal flaw: it ignored interactions between features. Perhaps "employed for 2 years" seems unfavorable in isolation, but combined with "perfect payment history," it might be acceptable. SHAP values consider all possible interactions, giving us the most accurate attribution possible.</p><p>We wrote a detailed article on using SHAP on A/B Testing scenarios. Check it out:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;ffd5c100-74e8-4283-8710-47d43889c61d&quot;,&quot;caption&quot;:&quot;Start writing today. Use the button below to create a Substack of your own&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;A/B Testing with SHAP: From Black Box to Glass Box&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:9307169,&quot;name&quot;:&quot;Saeed Garmsiri&quot;,&quot;bio&quot;:&quot;AI Alchemist | Data Cartographer | ML Engineer&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fee94b0e-21b0-4c07-912c-9e68308ac092_2886x2886.png&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null},{&quot;id&quot;:280689766,&quot;name&quot;:&quot;Taras Yanchynskyy&quot;,&quot;bio&quot;:&quot;Machine Learning Unicorn | Indie AI Engineer | Phronimos | Teaching Machines, Learning Humans&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/98d97974-6ebd-489e-947b-3bf44768ebbf_1080x1080.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2025-02-21T01:31:13.859Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://interference.substack.com/p/ab-testing-with-shap-from-black-box&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:155659027,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:1,&quot;comment_count&quot;:0,&quot;publication_id&quot;:null,&quot;publication_name&quot;:&quot;Interpretable Inference&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8cf09e4-3263-4306-bfec-06ee39f6eb7a_500x500.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><h2>The explainability-performance trade-off</h2><p>The relationship between model performance and explainability typically follows predictable patterns:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{array}{|l|c|c|c|l|}\n\\hline\n\\textbf{Model Type} &amp; \\textbf{Interpretability} &amp; \\textbf{Explainability} &amp; \\textbf{Performance} &amp; \\textbf{Use Cases} \\\\\n\\hline\n\\text{White Box (Linear Regression)} &amp; \\text{High} &amp; \\text{High} &amp; \\text{Good} &amp; \\text{Regulated industries, research} \\\\\n\\hline\n\\text{Gray Box (Random Forest)} &amp; \\text{Medium} &amp; \\text{Medium-High} &amp; \\text{Better} &amp; \\text{Business analytics, features} \\\\\n\\hline\n\\text{Black Box (Deep Networks)} &amp; \\text{Low} &amp; \\text{Low (w/o post-hoc)} &amp; \\text{Best} &amp; \\text{Images, language processing} \\\\\n\\hline\n\\end{array}&quot;,&quot;id&quot;:&quot;ESSDEGMKTR&quot;}" data-component-name="LatexBlockToDOM"></div><h2>How much explainability is enough?</h2><p>The goal isn't to maximize explainability for all use cases, but to use appropriate explainability for the specific context and requirements. This raises a natural question: how do we determine what is appropriate?</p><p>The answer to this question will largely depend on the industry and jurisdiction in which you&#8217;re operating. For example, healthcare, financial lending, and AV safety would likely require a high degree of explainability, whereas marketing and entertainment might suffice with good enough or even none at all. Jurisdiction largely matters when it comes to the regulatory requirement profile. For example, the GDPR right to explanation, or the Fair Credit Reporting Act, essentially ban completely black box models, but aren't very strict on explainability methods. At the same time, healthcare in many jurisdictions implies explanations with high fidelity and faithfulness. In 2021, Health Canada, the FDA, and the UK&#8217;s MHRA identified 10 guiding principles for good machine learning practice (GMLP). Notably, two principles pertain to explainability. First, "**Users Are Provided Clear, Essential Information**", which includes exposing interpretations. Second, a more critical driver, "**Focus Is Placed on the Performance of the Human-AI Team**", emphasizes the Human-AI team's performance over just the model's performance isolation". This means that the model&#8217;s performance is less important than the operator's ability to interpret the results and take appropriate action. In turn, this means that the combined performance of the model (e.g., precision) and the quality of explanations (e.g., SHAP values) are more important than performance alone.</p><h2>Mapping to real-world models</h2><p>Let's connect our friend risk predictor concepts to real machine learning algorithms.</p><h3>White box models with high interpretability</h3><p><strong>Linear Regression</strong>:</p><ul><li><p><strong>Interpretability</strong>: Perfect - each coefficient shows exact impact</p></li><li><p><strong>Explainability</strong>: Great - "Each additional year of friendship reduces risk by 2 points"</p></li><li><p><strong>When to use</strong>: Regulated environments, need for audit trails</p></li></ul><p><strong>Decision Trees</strong>:</p><ul><li><p><strong>Interpretability</strong>: High - can follow decision path</p></li><li><p><strong>Explainability</strong>: High - can explain exact reasoning chain</p></li><li><p><strong>When to use</strong>: Need human-readable business rules</p></li></ul><h3>Gray box models with medium interpretability</h3><p><strong>Random Forest</strong>:</p><ul><li><p><strong>Interpretability</strong>: Medium - can see feature importance but not individual predictions</p></li><li><p><strong>Explainability</strong>: Medium - can explain overall patterns, harder for specific cases</p></li><li><p><strong>Post-hoc methods</strong>: Feature importance, partial dependence plots work well</p></li></ul><p><strong>Gradient Boosting</strong>:</p><ul><li><p><strong>Interpretability</strong>: Medium-Low - complex interactions between weak learners</p></li><li><p><strong>Explainability</strong>: Requires post-hoc methods like SHAP</p></li><li><p><strong>When to use</strong>: High performance needed with some explainability</p></li></ul><h3>Black box models with low interpretability</h3><p><strong>Deep Neural Networks</strong>:</p><ul><li><p><strong>Interpretability</strong>: Very Low - millions of parameters, complex interactions</p></li><li><p><strong>Explainability</strong>: Requires post-hoc methods</p></li><li><p><strong>Post-hoc methods</strong>: SHAP, LIME, attention mechanisms, saliency maps</p></li></ul><p><strong>Ensemble Methods:</strong></p><ul><li><p><strong>Interpretability</strong>: Very Low - combining multiple different algorithms</p></li><li><p><strong>Explainability</strong>: Model-agnostic methods like SHAP work best</p></li></ul><h2>Model-specific explainability techniques</h2><h3>For tree-based models</h3><ul><li><p><strong>Built-in feature importance</strong>: Measures how much each feature reduces impurity</p></li><li><p><strong>Tree visualization</strong>: Can literally draw the decision process</p></li><li><p><strong>Rule extraction</strong>: Convert tree paths into if-then rules</p></li></ul><h3>For neural networks</h3><ul><li><p><strong>Attention mechanisms</strong>: Show which parts of the input the model focuses on</p></li><li><p><strong>Layer-wise relevance propagation</strong>: Traces predictions back through network layers</p></li><li><p><strong>Gradient-based methods</strong>: Show which input changes would most affect the output</p></li></ul><h3>For ensemble methods</h3><ul><li><p><strong>Model-agnostic approaches</strong>: SHAP, LIME work regardless of the underlying algorithm</p></li><li><p><strong>Consensus explanations</strong>: Aggregate explanations from individual models</p></li><li><p><strong>Disagreement analysis</strong>: Identify when different models give conflicting explanations</p></li></ul><h2>Quick reference for choosing an XAI approach</h2><p><strong>Do we need to explain to regulators or auditors?</strong></p><ul><li><p><strong>Use:</strong> White box model + documentation.  </p></li><li><p><strong>Why:</strong> High faithfulness, audit trail.</p></li></ul><p><strong>Does the business want general feature insights?</strong></p><ul><li><p><strong>Use:</strong> Gray box + feature importance.  </p></li><li><p><strong>Why:</strong> Balance of performance and interpretability.</p></li></ul><p><strong>Users ask, &#8220;Why was I rejected?"</strong></p><ul><li><p><strong>Use:</strong> SHAP values + counterfactuals.</p></li><li><p><strong>Why:</strong> Individual explanations + actionable advice.</p></li></ul><p><strong>Need simple business rules?</strong></p><ul><li><p><strong>Use:</strong> Decision trees or linear models.  </p></li><li><p><strong>Why:</strong> Inherently interpretable.</p></li></ul><p><strong>High-stakes decisions (medical, legal)?</strong></p><ul><li><p><strong>Use:</strong> White box or extensive post-hoc explanations.</p></li><li><p><strong>Why:</strong> Transparency for critical outcomes.</p></li></ul><p><strong>Performance is paramount?</strong></p><ul><li><p><strong>Use:</strong> Black box + post-hoc explanations.</p></li><li><p><strong>Why:</strong> Achieves best accuracy with an explanation layer.</p></li></ul><h2>Bringing it all together: the XAI spectrum</h2><p>The explainable AI landscape can be visualized as a spectrum with multiple dimensions:</p><ul><li><p><strong>Interpretability Spectrum</strong>: Inherent &#8594; Requires Tools &#8594; Opaque</p></li><li><p><strong>Explainability Spectrum</strong>: Self-Evident &#8594; Post-hoc Possible &#8594; Unexplainable</p></li><li><p><strong>Fidelity Spectrum</strong>: Perfect Match &#8594; Good Approximation &#8594; Poor Approximation </p></li><li><p><strong>Faithfulness Spectrum</strong>: True Mechanism &#8594; Simplified Process &#8594; Misleading</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!QvgG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!QvgG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!QvgG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!QvgG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!QvgG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!QvgG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:170413,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://interference.substack.com/i/165863944?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!QvgG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!QvgG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!QvgG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!QvgG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd2062ab-1966-4ed2-9c19-6f03f5743aa9_1920x1080.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Remember: the goal of explainable AI isn't to make every model a white box, but to provide the right level of transparency for each specific context. Sometimes, a simple feature importance chart is enough; sometimes, you need detailed counterfactual scenarios. The key is matching the explanation to the need.</p><div><hr></div><h2>References</h2><ul><li><p>https://www.sciencedirect.com/science/article/pii/S0306457324002590</p></li><li><p>https://arxiv.org/abs/1711.00399</p></li><li><p>https://www.canada.ca/en/health-canada/services/drugs-health-products/medical-devices/good-machine-learning-practice-medical-device-development.html</p></li><li><p>https://www.canada.ca/en/health-canada/services/drugs-health-products/medical-devices/transparency-machine-learning-guiding-principles.html</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://interference.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Interpretable Inference! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[A/B Testing with SHAP: From Black Box to Glass Box]]></title><description><![CDATA[Uncover the True Impact of Your Web Changes Using Explainable AI]]></description><link>https://interference.substack.com/p/ab-testing-with-shap-from-black-box</link><guid isPermaLink="false">https://interference.substack.com/p/ab-testing-with-shap-from-black-box</guid><dc:creator><![CDATA[Saeed Garmsiri]]></dc:creator><pubDate>Fri, 21 Feb 2025 01:31:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div class="captioned-button-wrap" data-attrs="{&quot;url&quot;:&quot;https://substack.com/refer/saeed.21?utm_source=substack&amp;utm_context=post&amp;utm_content=155659027&amp;utm_campaign=writer_referral_button&quot;,&quot;text&quot;:&quot;Start a Substack&quot;}" data-component-name="CaptionedButtonToDOM"><div class="preamble"><p class="cta-caption">Start writing today. Use the button below to create a Substack of your own</p></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://substack.com/refer/saeed.21?utm_source=substack&amp;utm_context=post&amp;utm_content=155659027&amp;utm_campaign=writer_referral_button&quot;,&quot;text&quot;:&quot;Start a Substack&quot;,&quot;hasDynamicSubstitutions&quot;:false}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://substack.com/refer/saeed.21?utm_source=substack&amp;utm_context=post&amp;utm_content=155659027&amp;utm_campaign=writer_referral_button"><span>Start a Substack</span></a></p></div><p>In the competitive landscape of e-commerce, understanding the "why" behind user behavior is crucial for success. While traditional A/B testing shows us what works, it often leaves us wondering why. A product page redesign might show a 15% increase in conversions, but which specific changes drove this improvement? Did some changes actually hurt conversion rates? How did different elements work together to influence purchasing decisions?</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://interference.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Interpretable Inference! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>This is where SHAP enters the scene. Instead of just telling us that our changes worked, SHAP (SHapley Additive exPlanations) acts like a detective, investigating each change's contribution to success. It breaks down that 15% improvement into precise measurements: the new button location added 42%, while having too many images actually reduced success by 15%. Now that's the kind of insight we can actually use.</p><h2>What You'll Learn</h2><ul><li><p>How to go beyond simple "it worked/didn't work" test results</p></li><li><p>Understanding which changes actually drove conversion rate improvements</p></li><li><p>Detecting when changes hurt rather than helped</p></li><li><p>Using SHAP to measure the impact of each change</p></li><li><p>How to identify and leverage feature interactions</p></li><li><p>Implementing changes based on data-driven insights</p></li></ul><p></p><h2>The Challenge: Beyond Simple Conversion Metrics</h2><h3>The Testing Dilemma</h3><p>In our recent test of a web page, we faced a common dilemma. Like many teams, we wanted to improve fast, so we tested multiple changes at once:</p><ul><li><p><strong>Button Placement:</strong> We moved the main action button from the bottom to the top of the page, making it immediately visible</p></li><li><p><strong>Price Display Style:</strong> We experimented with a larger, more prominent price display, including clearer discount information</p></li><li><p><strong>Mobile-Friendly Improvements:</strong> We redesigned the layout to work better on phones, with easier navigation and better touch targets</p></li><li><p><strong>Image Layout:</strong> We adjusted how product images were displayed, testing different sizes and arrangements</p></li><li><p><strong>Checkout Process:</strong> We streamlined the steps needed to complete an action, removing unnecessary fields</p></li></ul><p>Before we dive in, let's clarify some key concepts:</p><p><strong>A/B Testing</strong> is like giving users two different versions of your website and seeing which one works better. Imagine having two ice cream shops with different layouts - one with the menu at the entrance, another with it above the counter. Which gets more sales? That's A/B testing.</p><p><strong>SHAP (SHapley Additive exPlanations)</strong> is a tool that helps us understand why something worked. Think of it as a detective that can tell you not just that your new shop layout increased sales, but exactly how much each change (menu position, lighting, seating) contributed to that success.</p><p><a href="https://youtu.be/ekr2nIex040?t=97">Hey so now you know the game, are you ready?</a> Let&#8217;s dive into the web page scenario</p><h2>How SHAP Helps Understand Results</h2><p>SHAP (SHapley Additive exPlanations) helps untangle these results. It measures how each change contributes to success, both alone and in combination with other changes. It breaks down complex changes into understandable pieces. Think of it as taking apart a complex machine to see exactly how each gear and lever contributes to the whole. Let&#8217;s get our hands dirty by developing a code to create a sample dataset for the case study.</p><pre><code>import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import shap

# Generate test data
def generate_test_data(n_samples=50000):
    """Simulate web page test data with known effects"""
    data = pd.DataFrame({
        'time_on_page': np.random.normal(60, 20, n_samples),
        'button_location': np.random.choice(['top', 'middle', 'bottom'], n_samples),
        'price_style': np.random.choice(['large', 'medium', 'small'], n_samples),
        'image_count': np.random.randint(1, 10, n_samples),
        'mobile_score': np.random.uniform(0.5, 1.0, n_samples)
    })

    # Define known effects
    success_prob = (
        0.2 +  # baseline
        0.42 * (data['button_location'] == 'top') +
        0.35 * (data['price_style'] == 'large') +
        -0.15 * (data['image_count'] &gt; 5) +
        0.18 * ((data['button_location'] == 'top') &amp;
                (data['price_style'] == 'large')) +
        0.28 * (data['mobile_score'] &gt; 0.8)
    )

    data['success'] = np.random.binomial(1, np.clip(success_prob, 0, 1))
    return data</code></pre><div><hr></div><h4>Understanding Our Test Data Generation</h4><p>To demonstrate how SHAP analyzes test results, we first need test data. Just like a well-designed scientific experiment, we need to create data that represents real user behavior while controlling for specific variables we want to study. Let's break down how we create this data and the assumptions we make.</p><h5>Creating Test Variables</h5><p>First, we simulate different aspects of our web page. Think of this as setting up a controlled experiment where we can measure exactly how each element affects user behavior. Each variable simulates a real metric:</p><ul><li><p><code>time_on_page</code>: Average time spent is 60 seconds, varying by 20 seconds (normal distribution)</p><ul><li><p>Similar to how real users might spend anywhere from 40 to 80 seconds on a page</p></li><li><p>We use a normal distribution because that's how real user behavior typically varies</p></li></ul></li><li><p><code>button_location</code>: Button can be at top, middle, or bottom with equal chance</p><ul><li><p>Like testing different positions for the "Buy Now" button</p></li><li><p>Each position has an equal probability to simulate unbiased testing</p></li></ul></li><li><p><code>price_style</code>: Price display can be large, medium, or small with equal chance</p><ul><li><p>Represents different ways of showing prices to users</p></li><li><p>Could be font size, color contrast, or prominence of display</p></li></ul></li><li><p><code>image_count</code>: Pages show between 1 to 9 images</p><ul><li><p>Simulates different amounts of visual content</p></li><li><p>Range chosen based on typical product page layouts</p></li></ul></li><li><p><code>mobile_score</code>: Mobile optimization score ranges from 50% to 100%</p><ul><li><p>Represents how well the page works on mobile devices</p></li><li><p>Higher scores mean better mobile experience</p></li></ul></li></ul><h5>Calculating Success Probability</h5><p>Now comes the interesting part. We calculate how likely each user is to succeed (like making a purchase) based on these factors. Let's look at a practical example:</p><ul><li><p>Base case: 20% success chance</p></li><li><p>With top button: +42% (total: 62%)</p></li><li><p>With large price: +35% (total: 97%)</p></li><li><p>With 6 images: -15% (total: 82%)</p></li><li><p>With both top button and large price: +18% (total: 100%)</p></li><li><p>With good mobile score: +28% (total: 100%, capped)</p></li></ul><p>After calculating these probabilities, we need to convert them into realistic yes/no outcomes that mirror real-world user behavior.</p><h5>Converting to Actual Success</h5><p>Finally, we turn these probabilities into actual yes/no outcomes.</p><ul><li><p><code>np.clip</code>: Ensures probabilities stay between 0 and 1</p></li><li><p><code>np.random.binomial</code>: Converts probability into 0 (fail) or 1 (success)</p></li></ul><p>For example, if <code>success_prob</code> is 0.75:</p><ul><li><p>75% chance to get 1 (success)</p></li><li><p>25% chance to get 0 (fail)</p></li></ul><p>This creates realistic test data where we know exactly which factors influence success and by how much, letting us validate SHAP's findings against true effects.</p><h2>Our Test Setup</h2><p>To ensure thorough analysis, we tracked multiple metrics beyond just overall success:</p><ul><li><p>Time spent on page</p><ul><li><p>Indicates user engagement level</p></li><li><p>Helps understand browsing patterns</p></li></ul></li><li><p>Mobile optimization score</p><ul><li><p>Measures how well the page performs on mobile</p></li><li><p>Ranges from 50% to 100% optimization</p></li></ul></li><li><p>User interactions</p><ul><li><p>Button placement effects</p></li><li><p>Price display visibility impact</p></li></ul></li><li><p>Image presentation</p><ul><li><p>Number of images shown</p></li><li><p>Impact on user engagement</p></li></ul></li></ul><p>Using a dataset of 50,000 simulated sessions (with 1,000 used for detailed SHAP analysis), we can understand how each element contributes to overall success.</p><pre><code># Prepare data for analysis
data = generate_test_data()
X = pd.get_dummies(data.drop('success', axis=1))
y = data['success']

# Store column names OUTSIDE the function
feature_names = X.columns

# Create explainer
def predict(X):
    if isinstance(X, np.ndarray):
        X = pd.DataFrame(X, columns=feature_names)  # Use stored feature_names
    return (0.42 * (X['button_location_top'].astype(float)) +
            0.35 * (X['price_style_large'].astype(float)) +
            -0.15 * (X['image_count'].astype(float) &gt; 5) +
            0.28 * (X['mobile_score'].astype(float) &gt; 0.8))

background = shap.sample(X, 100)
explainer = shap.KernelExplainer(predict, background)
shap_values = explainer.shap_values(X[:1000])</code></pre><pre><code># 1. SHAP Summary Plot
plt.figure(figsize=(12, 6))
shap.summary_plot(shap_values, X[:1000], show=False)
plt.title('Impact of Each Change')
plt.tight_layout()
plt.show()</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2EcM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2EcM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png 424w, https://substackcdn.com/image/fetch/$s_!2EcM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png 848w, https://substackcdn.com/image/fetch/$s_!2EcM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png 1272w, https://substackcdn.com/image/fetch/$s_!2EcM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2EcM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png" width="774" height="499" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:499,&quot;width&quot;:774,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2EcM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png 424w, https://substackcdn.com/image/fetch/$s_!2EcM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png 848w, https://substackcdn.com/image/fetch/$s_!2EcM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png 1272w, https://substackcdn.com/image/fetch/$s_!2EcM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0bc74bc4-5321-4370-8a3b-4827bcff8da8_774x499.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure1: SHAP summary plot</figcaption></figure></div><p>The presented plot is a SHAP summary plot which is a powerful visualization that shows how each feature impacts our test results.</p><p>The plot tells us three key things such as Feature Importance. Features are ordered by impact (most important at the top) and the spread of SHAP values shows the magnitude of impact, in this plot wider spreads indicate stronger effects.</p><p>It also demonstrates the direction of Impact. When the direction points to the right it shows a positive impact on success and pointing to the left means a negative impact on success. The position shows how much impact (further from the center equals stronger impact)</p><p>And last but not least it shows the Value Relationships. In this plot, red dots show high feature values and blue dots show low feature values.</p><p>Looking at our data, we see:</p><ul><li><p>The button location at the top shows red dots on the right (positive impact)</p></li><li><p>High image counts show blue dots on the left (negative impact)</p></li><li><p>Mobile scores show a gradient from blue to red (linear relationship)</p></li></ul><p>This helps understand:</p><ul><li><p>Which changes matter most</p></li><li><p>How feature values affect outcomes</p></li><li><p>Where to focus optimization efforts</p></li></ul><pre><code># 2. True vs SHAP Effects Comparison
# Step 1: Define true effects
true_effects = {
    'Button Location': 0.42,
    'Price Style': 0.35,
    'Mobile Score': 0.28,
    'Image Count': -0.15
}

# Step 2: Calculate SHAP effects
shap_effects = {
    'Button Location': float(np.abs(shap_values).mean(0)[X.columns.str.contains('button_location')].sum()),
    'Price Style': float(np.abs(shap_values).mean(0)[X.columns.str.contains('price_style')].sum()),
    'Mobile Score': float(np.abs(shap_values).mean(0)[X.columns == 'mobile_score']),
    'Image Count': float(np.abs(shap_values).mean(0)[X.columns == 'image_count'])
}

# Step 3: Create comparison plot
plt.figure(figsize=(12, 6))
x = np.arange(len(true_effects))
width = 0.35  # Width of bars

fig, ax = plt.subplots()
rects1 = ax.bar(x - width/2, list(true_effects.values()), width, label='True Effect', color='skyblue')
rects2 = ax.bar(x + width/2, list(shap_effects.values()), width, label='SHAP Effect', color='lightgreen')

# Add labels and title
ax.set_ylabel('Effect Size')
ax.set_xlabel('Changes')
ax.set_title('True vs SHAP-discovered Effects')
ax.set_xticks(x)
ax.set_xticklabels(list(true_effects.keys()), rotation=45)
ax.legend()

# Add value labels on bars
def autolabel(rects):
    for rect in rects:
        height = rect.get_height()
        ax.annotate(f'{height:.2f}',
                    xy=(rect.get_x() + rect.get_width()/2, height),
                    xytext=(0, 3),  # 3 points vertical offset
                    textcoords="offset points",
                    ha='center', va='bottom')

autolabel(rects1)
autolabel(rects2)

plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pRNM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pRNM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png 424w, https://substackcdn.com/image/fetch/$s_!pRNM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png 848w, https://substackcdn.com/image/fetch/$s_!pRNM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png 1272w, https://substackcdn.com/image/fetch/$s_!pRNM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pRNM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png" width="630" height="470" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:630,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pRNM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png 424w, https://substackcdn.com/image/fetch/$s_!pRNM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png 848w, https://substackcdn.com/image/fetch/$s_!pRNM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png 1272w, https://substackcdn.com/image/fetch/$s_!pRNM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa178cdcf-83e9-45ae-b17d-4b24cfd29173_630x470.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure2: True vs SHAP effects comparison</figcaption></figure></div><p>This code creates a bar chart comparing what we know to be true (our designed effects) against what SHAP discovered. Let's break it down:</p><h4>Step 1: Define True Effects</h4><p>At this step, we see the effects we built into our test data.</p><h4>Step 2: Calculate SHAP Effects</h4><p>Here we:</p><ul><li><p>Take the mean of absolute SHAP values</p></li><li><p>Sum effects for categorical variables (like button_location)</p></li><li><p>Get single values for numeric variables</p></li></ul><h4>Step 3: Create Comparison Plot</h4><p>This creates a DataFrame with:</p><ul><li><p>Rows for each change</p></li><li><p>Columns for true and SHAP-discovered effects</p></li></ul><h4>Step 4: Visualization</h4><p>The resulting plot shows two bars for each change, the blue ones show true effects (what we designed) and the green bars show SHAP effects (what was discovered). also, the exact values labeled on each bar</p><p>This visualization helps validate SHAP's effectiveness by comparing its discoveries against known truth, building confidence in its use for real-world analysis where true effects are unknown.</p><pre><code># Interaction Analysis
plt.clf()
plt.close('all')

# Create correlation matrix of SHAP values
shap_corr = pd.DataFrame(shap_values, columns=X.columns).corr()

# Fill NA values with 0
shap_corr = shap_corr.fillna(0)

# Create a single figure
fig, ax = plt.subplots(figsize=(12, 8))

# Create heatmap with all values shown
sns.heatmap(shap_corr,
            xticklabels=X.columns,
            yticklabels=X.columns,
            cmap='viridis',
            annot=True,
            fmt='.2f',
            center=0,
            square=True,
            mask=None,  # Don't mask any values
            cbar_kws={'label': 'SHAP Value Correlation'})

plt.title('Change Interaction Analysis')
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
plt.show()</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Qfyj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Qfyj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png 424w, https://substackcdn.com/image/fetch/$s_!Qfyj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png 848w, https://substackcdn.com/image/fetch/$s_!Qfyj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png 1272w, https://substackcdn.com/image/fetch/$s_!Qfyj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Qfyj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png" width="944" height="790" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:790,&quot;width&quot;:944,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Qfyj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png 424w, https://substackcdn.com/image/fetch/$s_!Qfyj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png 848w, https://substackcdn.com/image/fetch/$s_!Qfyj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png 1272w, https://substackcdn.com/image/fetch/$s_!Qfyj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3cfaf43-f4b9-43cb-9e36-ea6bb7496416_944x790.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure3: Interaction Analysis</figcaption></figure></div><p>As you can see in the heatmap, most changes work independently and there is only a few strong interactions between different features. Design changes (button, price) have minimal interference and the mobile optimization and image count have minimal interaction.</p><p>This suggests our changes can be implemented relatively independently without worrying about negative interactions.</p><pre><code># 4. Distribution of SHAP Values
# First, identify most important features by mean absolute SHAP value
mean_shap = np.abs(shap_values).mean(0)
top_features_idx = np.argsort(mean_shap)[-4:]  # Get indices of top 4 features
top_features = X.columns[top_features_idx]

plt.figure(figsize=(12, 6))
for i, (idx, col) in enumerate(zip(top_features_idx, top_features)):
    plt.subplot(2, 2, i+1)
    sns.kdeplot(shap_values[:, idx], fill=True)
    plt.title(f'SHAP Distribution\n{col}')
    plt.xlabel('SHAP Value')
    plt.ylabel('Density')
plt.tight_layout()
plt.show()</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hnQJ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hnQJ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png 424w, https://substackcdn.com/image/fetch/$s_!hnQJ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png 848w, https://substackcdn.com/image/fetch/$s_!hnQJ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png 1272w, https://substackcdn.com/image/fetch/$s_!hnQJ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hnQJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png" width="1202" height="590" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:590,&quot;width&quot;:1202,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!hnQJ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png 424w, https://substackcdn.com/image/fetch/$s_!hnQJ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png 848w, https://substackcdn.com/image/fetch/$s_!hnQJ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png 1272w, https://substackcdn.com/image/fetch/$s_!hnQJ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff04c8f05-2c39-4226-935d-2d802fe6e250_1202x590.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure4: Distribution of SHAP values</figcaption></figure></div><p>his plot shows the distribution of SHAP values for the four most impactful features in our analysis. Let's break down each subplot:</p><ol><li><p>Image Count Distribution</p></li></ol><p>Shows two distinct peaks centered around -0.08 and 0.08 Bimodal distribution suggests image count has two common effects:</p><p>Negative peak (-0.08): Likely when there are too many images (&gt;5) Positive peak (0.08): When image count is optimal</p><p>The symmetrical peaks indicate balanced positive/negative impacts</p><ol start="2"><li><p>Mobile Score Distribution</p></li></ol><p>Also bimodal, with peaks around -0.1 and 0.2 Larger positive peak (0.2): Shows strong positive impact of good mobile optimization Smaller negative peak (-0.1): Indicates potential downsides of poor mobile scores Wider spread suggests more variable impact than image count</p><ol start="3"><li><p>Price Style (Large) Distribution</p></li></ol><p>Similar bimodal pattern with peaks at -0.1 and 0.25 Strong positive effect (0.25) when price is prominently displayed Negative effect (-0.1) when not using large price style Distribution suggests price style has the most consistent positive impact</p><ol start="4"><li><p>Button Location (Top) Distribution</p></li></ol><p>Widest range of SHAP values (-0.2 to 0.35) Strongest positive peak around 0.3 Notable negative impact around -0.15 Most polarized effect among all features, suggesting it's the most influential change</p><p>Key Insights</p><ol><li><p>All important features show bimodal distributions</p></li><li><p>Button location and price style have the largest potential positive impacts</p></li><li><p>Mobile score shows more balanced positive/negative effects</p></li><li><p>Image count has the most symmetrical impact distribution</p></li></ol><p>This visualization helps understand not just the average impact of each feature, but how those impacts vary across different scenarios.</p><pre><code># 5. Cumulative Effects Plot
# Sort features by importance for cumulative plot
sorted_idx = np.argsort(mean_shap)
cumulative_effects = np.cumsum(mean_shap[sorted_idx])

plt.figure(figsize=(15, 10))

plt.plot(range(1, len(cumulative_effects) + 1), cumulative_effects,
         marker='o', linewidth=2, markersize=8, color='#1f77b4')

for i, effect in enumerate(cumulative_effects):
    if effect &lt; 0.1:
        y_offset = -40 if i % 2 == 0 else -20
        x_offset = -20 if i % 2 == 0 else 20
    else:
        y_offset = 20
        x_offset = 50

    plt.annotate(f'{X.columns[sorted_idx[i]]}\n{effect:.2f}',
                (i+1, effect),
                xytext=(x_offset, y_offset),
                textcoords='offset points',
                ha='left' if x_offset &gt; 0 else 'right',
                va='center',
                bbox=dict(boxstyle='round,pad=0.5', 
                         fc='white', 
                         ec='gray', 
                         alpha=0.8))

plt.title('Cumulative Impact of Changes', pad=20, size=14)
plt.xlabel('Number of Changes', size=12)
plt.ylabel('Cumulative Effect', size=12)
plt.grid(True, alpha=0.3)
plt.margins(x=0.1)

plt.ylim(-0.1, 0.6)

plt.tight_layout()
plt.show()</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ijnk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ijnk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png 424w, https://substackcdn.com/image/fetch/$s_!Ijnk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png 848w, https://substackcdn.com/image/fetch/$s_!Ijnk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png 1272w, https://substackcdn.com/image/fetch/$s_!Ijnk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ijnk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png" width="1456" height="974" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:974,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ijnk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png 424w, https://substackcdn.com/image/fetch/$s_!Ijnk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png 848w, https://substackcdn.com/image/fetch/$s_!Ijnk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png 1272w, https://substackcdn.com/image/fetch/$s_!Ijnk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47c367b4-5542-4d2b-8d48-478f00880f9c_1479x989.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Figure5: Cumulative impact of changes</figcaption></figure></div><p>This plot visualizes how the total impact builds up as we add features in order of their importance. Each point on the line represents the cumulative effect after adding another feature.</p><p>The line moves upward in steps, with each step showing:</p><ol><li><p>The total impact so far</p></li><li><p>Which feature was just added</p></li><li><p>How much that feature contributed</p></li></ol><p>The plot begins with a flat line at zero effect through the first five changes: time_on_page, button_location_bottom, button_location_middle, price_style_medium, and price_style_small. This indicates these features had negligible impact on our results when implemented sequentially. Image_count introduces the first noticeable increase, showing a cumulative effect of 0.07. While modest, this marks where measurable impacts begin to appear in our implementation sequence. The final three changes demonstrate significant effects:</p><ul><li><p>Mobile_score: Increases cumulative impact to 0.21</p></li><li><p>Price_style_large: Further rises to 0.36</p></li><li><p>Button_location_top: Reaches final impact of 0.55</p></li></ul><p>These steep increases in the line's trajectory indicate these three changes were responsible for most of the overall improvement. The button_location_top change shows the largest individual contribution, evidenced by the steepest slope in the plot.</p><h2>Analysis and Findings: Understanding the Impact of Each Change</h2><p>Our SHAP analysis revealed clear insights about how each change affected our test results. Let's break down what we found:</p><p><strong>Button Location Impact</strong> (42% Effect) The position of the button emerged as our strongest influencer. Moving it to the top of the page showed a consistent 42% improvement. SHAP's distribution plot for this feature shows two distinct clusters - a strong positive effect when placed at the top and a negative effect when placed lower, validating our initial design hypothesis.</p><p><strong>Price Display Effectiveness</strong> (35% Effect) Making prices more prominent was our second most impactful change. SHAP analysis shows this had a 35% positive effect, with the distribution plot revealing a clear pattern: large price displays consistently improved results while smaller displays sometimes hindered performance.</p><p><strong>Mobile Optimization Results</strong> (28% Effect) Mobile optimization proved significant with a 28% improvement when scoring above 0.8 on our mobile metrics. The SHAP distribution for mobile scores shows an interesting bimodal pattern - strong positive effects for well-optimized pages and moderate negative effects for poor mobile experiences.</p><p><strong>Image Count Findings</strong> (-15% Effect) Perhaps our most surprising finding came from image count analysis. Pages with more than 5 images showed a 15% decrease in effectiveness. The SHAP distribution here shows two clear peaks, suggesting a clear threshold where additional images begin to hurt rather than help. Interaction Effects</p><p>The interaction heatmap revealed minimal interference between our changes. The strongest interaction appeared between button placement and price display, though even this was relatively modest. This suggests our changes largely worked independently, allowing for flexible implementation approaches. These findings are particularly reliable because SHAP's discovered effects closely match our known true effects, validating the analysis methodology and providing confidence in our conclusions.</p><h2>From Analysis to Action: Implementing Test Insights</h2><p>Our SHAP analysis provided clear direction for practical improvements that can be broken down into specific, measurable actions:</p><h3>Primary Changes</h3><p>Based on the 42% improvement from button placement and 35% from price visibility:</p><ul><li><p><strong>Standardize button placement</strong></p><ul><li><p>Move all primary action buttons above the fold Maintain consistent positioning across all pages Remove any competing calls to action near the main button</p></li></ul></li><li><p><strong>Enhance price visibility</strong></p><ul><li><p>Increase price font size and contrast Position pricing near the action button Display any discounts or savings prominently</p><p></p></li></ul></li></ul><h3>Performance Optimizations</h3><p>Following the negative 15% impact of excess images:</p><ul><li><p><strong>Image strategy</strong></p><ul><li><p>Limit pages to a maximum of 5 key images Implement lazy loading for additional images Optimize image compression and formats</p></li></ul></li><li><p><strong>Mobile experience</strong> (28% improvement potential)</p><ul><li><p>Prioritize mobile page speed Ensure touch targets meet size guidelines Simplify navigation for mobile users</p></li></ul></li></ul><h3>Implementation Strategy</h3><p>To maximize impact while minimizing risk:</p><ul><li><p>Start with the highest-impact changes (button and price)</p></li><li><p>Follow with mobile optimizations</p></li><li><p>Implement image limits on new pages first</p></li><li><p>Roll out changes gradually to measure real-world impact</p></li></ul><p></p><h2>Conclusion</h2><p>Each change should be monitored with clear metrics to validate the improvements match our analysis predictions.</p><p>Our journey through SHAP analysis reveals more than just technical metrics. It's a testament to the power of understanding, not just what works, but why it works. In the digital landscape, where user experience is king, these insights are more than data points; they're windows into user behavior.</p><p>The real magic isn't in blindly implementing changes, but in understanding the nuanced interactions that drive user decisions. Each pixel, each button placement, each design choice tells a story. SHAP helps us read that story with unprecedented clarity.</p><p>As we continue to explore the intersection of AI, web design, and user experience, remember: </p><p><em>data isn't just about numbers. It's about people. It's about understanding the human behind the click, the motivation behind the interaction.</em></p><p>Stay curious. Keep exploring. And never stop asking why.</p><h2>References</h2><ol><li><p>Lundberg, S. M., &amp; Lee, S. I. (2017). "A unified approach to interpreting model predictions." Advances in Neural Information Processing Systems, 30.</p></li><li><p>Molnar, C. (2020). "Interpretable Machine Learning: A Guide for Making Black Box Models Explainable." <a href="https://christophm.github.io/interpretable-ml-book/">https://christophm.github.io/interpretable-ml-book/</a></p></li><li><p>Lipovetsky, S., &amp; Conklin, M. (2001). "Analysis of regression in game theory approach." Applied Stochastic Models in Business and Industry, 17(4), 319-330.</p></li><li><p>SHAP (SHapley Additive exPlanations) GitHub Repository: <a href="https://github.com/slundberg/shap">https://github.com/slundberg/shap</a></p></li><li><p>Interpretable Machine Learning with SHAP: <a href="https://towardsdatascience.com/interpretable-machine-learning-with-shap-61e7c1f53f9d">https://towardsdatascience.com/interpretable-machine-learning-with-shap-61e7c1f53f9d</a></p></li></ol><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://interference.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Interpretable Inference! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>