3. Psuedo r-squared for logistic regression
In ordinary least square (OLS) regression, the \(R^2\) statistics measures the amount of variance explained by the regression model. The value of \(R^2\) ranges in \([0, 1]\), with a larger value indicating more variance is explained by the model (higher value is better). For OLS regression, \(R^2\) is defined as following.
\(R^2 = 1 - \frac{ \sum (y_i - \hat{y}_i)^2 }{ \sum (y_i - \bar{y})^2 }\)
where
\(y_i\) is the i-th label (0 or 1)
\(\hat{y}_i\) is the i-th predicted value
\(\bar{y}\) is the mean of \(y\)
The three main ways to interpret \(R^2\) is as follows.
explained variable: how much variability is explained by the model
goodness-of-fit: how well does the model fit the data
correlation: the correlations between the predictions and true values
For logistic regression, there have been many proposed pseudo-\(R^2\). A non-exhaustive list is shown below.
Efron’s \(R^2\)
McFadden’s \(R^2\)
McFadden’s Adjusted \(R^2\)
Cox & Snell \(R^2\)
Nagelkerke/Cragg & Uhler’s \(R^2\)
McKelvey & Zavoina \(R^2\)
Count \(R^2\)
Adjusted Count \(R^2\)
In this notebook, we show how to compute some of these pseudo-\(R^2\). We will not compute pseudo-\(R^2\) that are based on raw likelihood since these may lead to underflow (Cox & Snell and Nagelkerke/Cragg & Uhler). Note the following.
these pseudo-\(R^2\) values lie in \([0, 1]\) with values closer to 1 indicating better fit
DL McFadden stated that a pseudo-\(R^2\) higher than 0.2 represents an
excellent
fitAdditionally, McFadden’s \(R^2\) can be negative
these pseudo-\(R^2\) values may be wildly different from one another
these pseudo-\(R^2\) values cannot be interpreted like OLS \(R^2\)
3.1. Simulate data
Here, we simulate data for logistic regression analysis. The functional form looks like the following.
\(\log \frac{p}{1-p} = 1.0 + 2.0 * x_1 + 3.0 * x_2\)
[1]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from numpy.random import binomial, normal
from scipy.stats import bernoulli, binom
np.random.seed(37)
sns.set(color_codes=True)
n = 10000
X = np.hstack([
np.array([1 for _ in range(n)]).reshape(n, 1),
normal(0.0, 1.0, n).reshape(n, 1),
normal(0.0, 1.0, n).reshape(n, 1)
])
z = np.dot(X, np.array([1.0, 2.0, 3.0])) + normal(0.0, 1.0, n)
p = 1.0 / (1.0 + np.exp(-z))
y = binom.rvs(1, p)
3.2. Learn the model parameters
Now, we use Sckit-Learn’s logistic regression solver to learn the model parameters (weights/coefficients). Note that we introduced some error during the simulation and so the coefficients of the learned model are not recovered to be exactly like the generating model.
[2]:
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(fit_intercept=False, solver='lbfgs')
lr.fit(X, y)
w = np.array(lr.coef_).transpose()
y_pred = lr.predict_proba(X)[:, 1]
print(lr.coef_)
[[0.89312519 1.71445566 2.59091944]]
3.3. Performance
3.3.1. Efron’s \(R^2\)
\(R^2 = 1 - \frac{\sum (y_i - \pi_i)^2}{\sum (y_i - \bar{y})^2}\)
\(y_i\) is the i-th outcome label (e.g. 1 or 0)
\(\pi_i\) is the i-th predicted outcome probability
\(\bar{y}\) is the expected value of the observed outcomes \(y = [y_1, \ldots, y_n]\) e.g. \(\bar{y} = \frac{\sum y_i}{n}\)
[3]:
def efron_rsquare(y, y_pred):
n = float(len(y))
t1 = np.sum(np.power(y - y_pred, 2.0))
t2 = np.sum(np.power((y - (np.sum(y) / n)), 2.0))
return 1.0 - (t1 / t2)
[4]:
efron_rsquare(y, y_pred)
[4]:
0.5513983981594343
3.3.2. McFadden’s \(R^2\)
\(R^2 = 1 - \frac{\ln \hat{L}_{full}}{\ln \hat{L}_{null}}\)
\(\hat{L}_{full}\) is the estimated likelihood of the full model
\(\hat{L}_{null}\) is the estimated likelihood of the null model (model with only intercept)
[5]:
def full_log_likelihood(w, X, y):
score = np.dot(X, w).reshape(1, X.shape[0])
return np.sum(-np.log(1 + np.exp(score))) + np.sum(y * score)
def null_log_likelihood(w, X, y):
z = np.array([w if i == 0 else 0.0 for i, w in enumerate(w.reshape(1, X.shape[1])[0])]).reshape(X.shape[1], 1)
score = np.dot(X, z).reshape(1, X.shape[0])
return np.sum(-np.log(1 + np.exp(score))) + np.sum(y * score)
def mcfadden_rsquare(w, X, y):
return 1.0 - (full_log_likelihood(w, X, y) / null_log_likelihood(w, X, y))
def mcfadden_adjusted_rsquare(w, X, y):
k = float(X.shape[1])
return 1.0 - ((full_log_likelihood(w, X, y) - k) / null_log_likelihood(w, X, y))
[6]:
mcfadden_rsquare(w, X, y)
[6]:
0.5173841067369636
3.3.3. McFadden’s Adjusted \(R^2\)
\(R^2 = 1 - \frac{\ln \hat{L}_{full} - K}{\ln \hat{L}_{null}}\)
\(\hat{L}_{full}\) is the estimated likelihood of the full model
\(\hat{L}_{null}\) is the estimated likelihood of the null model (model with only intercept)
\(K\) is the number of parameters (e.g. number of covariates associated with non-zero coefficients)
[7]:
mcfadden_adjusted_rsquare(w, X, y)
[7]:
0.5169598888329648
3.3.4. McKelvey & Zavoina \(R^2\)
\(R^2 = \frac{ \sigma^2(\hat{y}) }{ \sigma^2(\hat{y}) + \frac{\pi^2}{3} }\)
\(\sigma^2(\hat{y})\) is the variance of the predicted probabilities
[8]:
def mz_rsquare(y_pred):
return np.var(y_pred) / (np.var(y_pred) + (np.power(np.pi, 2.0) / 3.0) )
[9]:
mz_rsquare(y_pred)
[9]:
0.03882393736712112
3.3.5. Count \(R^2\)
\(R^2=\frac{C}{T}\)
\(C\) is the total number of correctly classified observations with treating a probability below 0.5 as a 0 and above as a 1
\(T\) is the total number of observations
[10]:
def get_num_correct(y, y_pred, t=0.5):
y_correct = np.array([0.0 if p < t else 1.0 for p in y_pred])
return sum([1.0 for p, p_pred in zip(y, y_correct) if p == p_pred])
def count_rsquare(y, y_pred, t=0.5):
n = float(len(y))
num_correct = get_num_correct(y, y_pred, t)
return num_correct / n
[11]:
count_rsquare(y, y_pred)
[11]:
0.8469
3.3.6. Adjust count \(R^2\)
\(R^2=\frac{C - n}{T - n}\)
\(C\) is the total number of correctly classified observations with treating a probability below 0.5 as a 0 and above as a 1
\(T\) is the total number of observations
\(n\) is the count of the most frequent outcome
[12]:
def get_count_most_freq_outcome(y):
num_0 = 0
num_1 = 0
for p in y:
if p == 1.0:
num_1 += 1
else:
num_0 += 1
return float(max(num_0, num_1))
def count_adjusted_rsquare(y, y_pred, t=0.5):
correct = get_num_correct(y, y_pred, t)
total = float(len(y))
n = get_count_most_freq_outcome(y)
return (correct - n) / (total - n)
[13]:
count_adjusted_rsquare(y, y_pred)
[13]:
0.6243866535819431
3.3.7. Comparison with other model performance metrics
Here, we show other performance metrics to see how these might compare to pseudo-\(R^2\).
[14]:
from sklearn.metrics import average_precision_score, brier_score_loss, \
log_loss, roc_auc_score
from sklearn.metrics import explained_variance_score, mean_absolute_error, \
mean_squared_error, median_absolute_error, r2_score
m_names = [
'average_precision_score', 'brier_score_loss',
'log_loss', 'roc_auc_score',
'explained_variance_score', 'mean_absolute_error',
'mean_squared_error', 'median_absolute_error', 'r2_score'
]
metrics = [average_precision_score, brier_score_loss, log_loss, roc_auc_score,
explained_variance_score, mean_absolute_error, mean_squared_error, median_absolute_error, r2_score]
for n, m in zip(m_names, metrics):
print('{:.5f} : {}'.format(m(y, y_pred), n))
0.94774 : average_precision_score
0.10832 : brier_score_loss
0.34130 : log_loss
0.92479 : roc_auc_score
0.55140 : explained_variance_score
0.21691 : mean_absolute_error
0.10832 : mean_squared_error
0.11089 : median_absolute_error
0.55140 : r2_score