Explainability Extract Analysis

This notebook demonstrates the DecisionAnalyzer class from pdstools using the Explainability Extract v1 dataset.

The Explainability Extract captures actions at the Arbitration stage only. It can be extracted from Infinity 24.1 and earlier versions. For the full decision funnel (v2 format), see the Decision Analyzer notebook.

The analyses below cover action distribution, sensitivity, win/loss patterns, and personalization.

[2]:
from pdstools.decision_analyzer.data_read_utils import read_data
from pdstools.decision_analyzer.DecisionAnalyzer import DecisionAnalyzer
from pdstools import read_ds_export
import polars as pl

Read Data

Load the sample Explainability Extract data and create a DecisionAnalyzer instance. The sample data can be downloaded directly from GitHub.

[3]:
df = read_ds_export(
    filename="sample_explainability_extract.parquet",
    path="https://raw.githubusercontent.com/pegasystems/pega-datascientist-tools/master/data",
)
decision_data = DecisionAnalyzer(df)
[4]:
decision_data.decision_data.collect()
[4]:
shape: (7_297, 21)
Subject IDInteraction IDDecision TimeIssueGroupActionChannelDirectionValueContext WeightLeverspyPropensityPropensityPriorityModel Control Groupis_mandatorydayRankStage GroupStage OrderRecord Type
strstrdatetime[μs]strstrstrstrstrf64f64f64f64f64f32stri32dateu32cati32str
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""CreditCard_1""pyName_232""Mobile""Inbound"0.370.5900171.5125270.0167840.0165510.005465"Test"02024-06-1725"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Mortgage_1""pyName_251""Mobile""Inbound"0.31.3602371.1698660.50.4976750.237584"Test"02024-06-173"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Proactive""pyName_439""Mobile""Inbound"0.30.7184341.4274340.0079710.0080.002461"Test"02024-06-1741"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Collections_1""pyName_82""Mobile""Inbound"0.551.3444011.0235690.0012160.0011990.000908"Test"02024-06-1754"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Acquisition""Proactive_1""pyName_363""Mobile""Inbound"0.30.6066071.4531020.0094860.0094510.002499"Test"02024-06-1740"Arbitration"10000"FILTERED_OUT"
"pySubjectID_1748""-8570391784720840506"2024-06-15 18:21:40.202300"Engagement""Proactive_1""pyName_242""Mobile""Inbound"0.30.0953810.8680150.0296650.0299050.000743"Test"02024-06-1562"Arbitration"10000"FILTERED_OUT"
"pySubjectID_1748""-8570391784720840506"2024-06-15 18:21:40.202300"Retention""Collections_1""pyName_426""Mobile""Inbound"0.581.7230521.2579730.01280.0123720.015554"Test"02024-06-1512"Arbitration"10000"FILTERED_OUT"
"pySubjectID_1748""-8570391784720840506"2024-06-15 18:21:40.202300"Retention""Mortgage""pyName_61""Mobile""Inbound"0.580.0676821.2088780.50.5036170.023899"Test"02024-06-158"Arbitration"10000"FILTERED_OUT"
"pySubjectID_1748""-8570391784720840506"2024-06-15 18:21:40.202300"Retention""Mortgage""pyName_178""Mobile""Inbound"0.741.1877291.5601560.0308440.0306430.042019"Test"02024-06-155"Arbitration"10000"FILTERED_OUT"
"pySubjectID_1748""-8570391784720840506"2024-06-15 18:21:40.202300"Engagement""RetailBank_1""pyName_463""Mobile""Inbound"0.370.5079691.2122890.012760.0131940.003006"Test"02024-06-1540"Arbitration"10000"FILTERED_OUT"

Overview

General statistics of the dataset.

[5]:
decision_data.get_overview_stats
[5]:
{'Actions': 152,
 'Channels': 1,
 'Duration': datetime.timedelta(days=7),
 'StartDate': datetime.date(2024, 6, 12),
 'Customers': 100,
 'Decisions': 100,
 'avgOffersAtArbitration': 73,
 'avgAvailable': 0}

A single decision. The number of rows shows how many actions are available at the Arbitration stage. Rank shows the action ranking.

[6]:
selected_interaction_id = (
    decision_data.unfiltered_raw_decision_data.select("Interaction ID")
    .first()
    .collect()
    .row(0)[0]
)
print(f"{selected_interaction_id=}")
decision_data.unfiltered_raw_decision_data.filter(
    pl.col("Interaction ID") == selected_interaction_id
).sort("Rank").collect()
selected_interaction_id='-8570391784720831070'
[6]:
shape: (72, 21)
Subject IDInteraction IDDecision TimeIssueGroupActionChannelDirectionValueContext WeightLeverspyPropensityPropensityPriorityModel Control Groupis_mandatorydayRankStage GroupStage OrderRecord Type
strstrdatetime[μs]strstrstrstrstrf64f64f64f64f64f32stri32dateu32cati32str
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Mortgage""pyName_476""Mobile""Inbound"0.581.2602941.0601280.50.4705960.364676"Test"02024-06-171"Output"3800"OUTPUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Mortgage""pyName_61""Mobile""Inbound"0.581.2475560.7884460.50.5019490.286365"Test"02024-06-172"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Mortgage_1""pyName_251""Mobile""Inbound"0.31.3602371.1698660.50.4976750.237584"Test"02024-06-173"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""RetailBank_1""pyName_590""Mobile""Inbound"0.350.7060761.4668140.50.5035240.182522"Test"02024-06-174"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""RetailBank_1""pyName_211""Mobile""Inbound"0.380.5820791.1972110.50.5118750.13555"Test"02024-06-175"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Acquisition""Billing""pyName_206""Mobile""Inbound"0.30.3028880.5854440.0034730.0034760.000185"Test"02024-06-1768"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"ActivateUse""Proactive_1""pyName_362""Mobile""Inbound"0.460.3066660.5464290.0023010.0022430.000173"Test"02024-06-1769"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Billing_1""pyName_224""Mobile""Inbound"0.110.5713861.4630250.0016770.0016750.000154"Test"02024-06-1770"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Mortgage""pyName_431""Mobile""Inbound"0.261.4811450.6318460.0002450.0002420.000059"Test"02024-06-1771"Arbitration"10000"FILTERED_OUT"
"pySubjectID_466""-8570391784720831070"2024-06-17 18:21:40.202300"Retention""Proactive""pyName_483""Mobile""Inbound"0.31.026960.8975420.0001030.0001010.000028"Test"02024-06-1772"Arbitration"10000"FILTERED_OUT"

Action Distribution

Distribution of actions at the Arbitration stage. Helps identify action groups that rarely survive to Arbitration.

[7]:
stage = "Arbitration"
scope_options = ["Issue", "Group", "Action"]
distribution_data = decision_data.getDistributionData(stage, scope_options)
fig = decision_data.plot.distribution_as_treemap(
    df=distribution_data, stage=stage, scope_options=scope_options
)
fig.show()

Global Sensitivity

Shows the impact of each arbitration factor (Propensity, Value, Context Weight, Levers) on the final decision. Each bar represents how many decisions would change if that factor were removed. Ideally, Propensity should have the strongest influence.

[8]:
decision_data.plot.sensitivity(win_rank=1)

Wins and Losses in Arbitration

Distribution of wins and losses by Issue. The level parameter can be set to "Group" or "Action" for different granularity. Actions are classified as winning or losing based on win_rank.

[9]:
decision_data.plot.global_winloss_distribution(level="Issue", win_rank=1)

Optionality Analysis

Shows how many actions are available per customer at the Arbitration stage. Limited optionality reduces the ability to personalize. The bars show decision counts per number of available actions; the line shows average propensity of the top-ranked action. Average propensity should increase with more available actions.

[10]:
decision_data.plot.propensity_vs_optionality(stage="Arbitration")

Win/Loss Analysis

Win Analysis

Select an action and see how often it wins and which actions it defeats.

[11]:
win_rank = 1
selected_action = (
    decision_data.unfiltered_raw_decision_data.filter(pl.col("Rank") == 1)
    .group_by("Action")
    .len()
    .sort("len", descending=True)
    .collect()
    .get_column("Action")
    .to_list()[1]
)
filter_statement = pl.col("Action") == selected_action

interactions_where_comparison_group_wins = (
    decision_data.get_winning_or_losing_interactions(
        win_rank=win_rank,
        group_filter=filter_statement,
        win=True,
    )
)

print(
    f"selected action '{selected_action}' wins(Rank{win_rank}) in {interactions_where_comparison_group_wins.collect().height} interactions."
)
selected action 'pyName_61' wins(Rank1) in 30 interactions.

Actions that lose to the selected action in arbitration.

[12]:
# Losing actions in interactions where the selected action wins.
groupby_cols = ["Issue", "Group", "Action"]
winning_from = decision_data.winning_from(
    interactions=interactions_where_comparison_group_wins,
    win_rank=win_rank,
    groupby_cols=groupby_cols,
    top_k=20,
)

decision_data.plot.distribution_as_treemap(
    df=winning_from, stage="Arbitration", scope_options=groupby_cols
)

Loss Analysis

Actions that beat the selected action in arbitration.

[13]:
interactions_where_comparison_group_loses = (
    decision_data.get_winning_or_losing_interactions(
        win_rank=win_rank,
        group_filter=filter_statement,
        win=False,
    )
)

print(
    f"selected action '{selected_action}' loses in {interactions_where_comparison_group_loses.collect().height} interactions."
)
# Winning actions in interactions where the selected action loses.
losing_to = decision_data.losing_to(
    interactions=interactions_where_comparison_group_loses,
    win_rank=win_rank,
    groupby_cols=groupby_cols,
    top_k=20,
)

decision_data.plot.distribution_as_treemap(
    df=losing_to, stage="Arbitration", scope_options=groupby_cols
)
selected action 'pyName_61' loses in 58 interactions.

Sensitivity for Selected Action

Change in win count when each prioritization factor is individually removed. Unlike the global sensitivity above, negative values are possible: a negative value means removing that factor would increase wins for the selected action (i.e., that factor is hurting it).

[14]:
decision_data.plot.sensitivity(
    reference_group=pl.col("Action") == selected_action
)

Prioritization Factor Distributions

Box plots comparing the arbitration factor distributions of the selected action vs competitors in the same interactions.

[15]:
fig, warning_message = decision_data.plot.prio_factor_boxplots(
    reference=pl.col("Action") == selected_action,
)
if warning_message:
    print(warning_message)
else:
    fig.show()

Rank Distribution

Distribution of the prioritization rank for the selected action. Low ranks indicate the action is not often winning.

[16]:
decision_data.plot.rank_boxplot(
    reference=pl.col("Action") == selected_action,
)