5.3.5. Iranian Churn Dataset#

5.3.5.1. Summary#

A customer churn dataset from an Iranian telecom company, collected over 12 months [1] and published in the UCI Machine Learning Repository [2]. Each row is a customer, described by usage and account features, with a label indicating whether they churned by the end of the period.

What makes this dataset well suited to value-driven modelling is its Customer Value column, which Empulse exposes as a per-customer lifetime value (clv). Because the value of retaining a customer varies, the profit-optimal customer to target is not simply the one most likely to churn — which is exactly the situation the maximum profit framework was designed for.

Classes

2

Churners

495

Non-churners

2755

Samples

3150

Features

12

5.3.5.2. Using the Dataset#

The dataset is fetched through fetch_iranian_churn. It is downloaded from the UCI repository on first use and cached under ~/empulse_data (override with $EMPULSE_DATA_HOME or the data_home argument), so later calls work offline.

It returns a Dataset object with the following attributes:

  • data: the feature matrix

  • target: the target vector

  • cost_matrix: a CostMatrix with default values pre-filled

  • instance_costs: a dict of per-instance cost drivers ('clv')

  • feature_names: the feature names

  • target_names: the target names

  • DESCR: the full description of the dataset

import pandas as pd
from empulse.datasets import fetch_iranian_churn

dataset = fetch_iranian_churn(backend=pd)

The backend argument selects the dataframe library used for data and target. Pass the module itself — backend=pd for pandas or backend=pl for polars.

All features are numeric, so a scaler is enough preprocessing for a linear model:

import pandas as pd
from empulse.datasets import fetch_iranian_churn
from empulse.metrics import Metric, Cost
from empulse.models import CSLogitClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

dataset = fetch_iranian_churn(backend=pd)
X, y = dataset.data, dataset.target

pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', CSLogitClassifier(loss=Metric(dataset.cost_matrix, Cost()))),
])
pipeline.fit(X, y, model__clv=dataset.instance_costs['clv'])

Because the cost matrix is the standard churn retention matrix, the dataset also works directly with the prebuilt churn metrics:

from empulse.metrics import empc_score

y_score = pipeline.predict_proba(X)[:, 1]
expected_profit = empc_score(y, y_score, clv=dataset.instance_costs['clv'])
target_fraction = empc_score.optimal_rate(y, y_score, clv=dataset.instance_costs['clv'])

5.3.5.3. Cost Matrix#

Contacting a customer costs a fraction \(f\) of their value, whether or not they accept. A contacted customer accepts the retention offer with probability \(\gamma\), in which case their value is retained minus the incentive, a fraction \(d\) of that value. Losing a customer you did not contact costs their full value.

Actual churner \(y_i = 1\)

Actual non-churner \(y_i = 0\)

Predicted churner \(\hat{y}_i = 1\)

tp_benefit \(= \gamma (CLV_i - d \cdot CLV_i - f \cdot CLV_i) - (1-\gamma) f \cdot CLV_i\)

fp_cost \(= d \cdot CLV_i + f \cdot CLV_i\)

Predicted non-churner \(\hat{y}_i = 0\)

fn_cost \(= CLV_i\)

tn_benefit \(= 0\)

The symbolic parameters carry these defaults, and can be overridden by passing their alias:

Alias

Default

Meaning

accept_rate (\(\gamma\))

0.3

Probability a contacted customer accepts the offer

incentive_fraction (\(d\))

0.05

Retention incentive, as a fraction of CLV

contact_fraction (\(f\))

0.01

Cost of contacting a customer, as a fraction of CLV

from empulse.metrics import Metric, Cost

cost = Metric(dataset.cost_matrix, Cost())
generous_offer = cost(
    y,
    y_score,
    accept_rate=0.5,
    incentive_fraction=0.10,
    clv=dataset.instance_costs['clv'],
)

Warning

132 of the 3150 customers have a Customer Value of exactly 0. For those rows every term of the cost matrix evaluates to 0, which makes the profit-optimal decision undefined for that customer. As a result optimal_threshold and optimal_rate raise a ValueError for the Cost and Savings strategies on this dataset.

The MaxProfit strategy is unaffected, because it derives the operating point from the ROC convex hull across the whole population rather than per customer. Use empc_score.optimal_rate(...) as shown above, or drop the zero-value customers if you need a cost-based threshold.

5.3.5.4. Data Description#

Feature

Description

call_failure

Number of failed calls

complains

Whether the customer filed a complaint (0 = no, 1 = yes)

subscription_length

Total months of subscription

charge_amount

Ordinal attribute from 0 (lowest) to 9 (highest)

seconds_of_use

Total seconds of calls

frequency_of_use

Total number of calls

frequency_of_sms

Total number of text messages

distinct_called_numbers

Number of distinct phone numbers called

age_group

Ordinal age band from 1 (youngest) to 5 (oldest)

tariff_plan

Tariff plan (1 = pay as you go, 2 = contractual)

status

Subscription status (1 = active, 2 = non-active)

age

Age of the customer in years

The Customer Value column is not part of data; it is returned separately as instance_costs['clv'] because it is a cost driver rather than a predictive feature. Using it as a feature would leak business value into the model instead of into the objective.

5.3.5.5. References#