The ROC AUC is the most used statistic to assess the predictive power of a classification model. However, few working data scientists know theoretical results about its statistical fluctuations. Here, we show in detail a derivation of a commonly found result on the variance of the ROC AUC.
We have not found this demonstration done in length in any references, and so write it here for pedagogical reasons.
We further study the case of the random classifier under extreme imbalance, which demonstrates interesting statistical results regarding asymptotic independence.
Originally presented at the DataLab Journal Club on September 2022.
Recap: the probabilistic interpretation of the ROC AUC
Let (X,Y) be jointly distributed with Y taking values in {0,1}. This is the standard binary classification problem.
X, y = make_classification(2000, n_features=20, class_sep=0.6, weights=(0.8,), random_state=2)
model = LogisticRegression().fit(X, y)
# As an areay_probs = model.predict_proba(X)[:,1]auc_area = roc_auc_score(y, y_probs)
# As a probabilityn0, n1 = np.bincount(y)total_sum = 0for pi in y_probs[y==1]: for pj in y_probs[y==0]: if pi >= pj: total_sum += 1auc_estimator = total_sum/(n0*n1)
print("ROC AUC calculated as an area:", round(auc_area,5))print("ROC AUC calculated a statistic:", round(auc_estimator,5))
ROC AUC calculated as an area: 0.93089 ROC AUC calculated a statistic: 0.93089
Notice how both results are identical.
Statistics of the ROC AUC
It is easily seen that E[A^]=A, and thus one might ask what is the variance of this estimator. We will prove the following result (eq. (2) of [1] and eq. (6) of [2]):
Theorem. Under the hypothesis that all points in a given class are independent (ie. P’s are iid among themselves, as are the Q’) then the variance of the Mann-Whitney-Wilcoxon statistic is
Now, the summand in the last term is (explicitly) E[1Pi≥Qj,Pk≥Ql].
But since i=k and j=l, the events Pi≥Qj is completely independent from Pk≥Ql, and we can split the expectation into
E[1Pi≥Qj]E[1Pk≥Ql]=A2,
which cancels the other A2 term.
Furthermore, it is easy to see that
E[1ij,il]=PXYY;
it is exactly the term PXYY term defined above: it is the probability that a single score in class 1 (the P) is greater than two random scores from class 0 (the Q’s). An analogous reasoning shows that
Proof: we will need the expression for conditional variance: if X,Y are random variables,
VarX=E[Var(X∣Y)]+Var(E[X∣Y])
For any P and Q, it then follows that
Var1P≥Q=E[Var(1P≥Q∣Q)]+Var(E[1P≥Q∣Q])
Now, it is not too hard to see how the variable 1P≥Q∣Q is distributed. Since it takes values on {0,1} is is in the Bernoulli familly; recall that X∼Bernoulli(p) means that P(X=1)=1−P(X=0)=p. In our case,
P(1P≥Q=1∣Q)=P(P≥Q∣Q)=1−P(P≤Q∣Q)≡1−FP(Q)
where FP is the CDF of P. Hence,
1P≥Q∣Q∼Bernoulli(1−FP(Q))
Now, a Bernoulli(p) variable has expectation p and variance p(1−p). It follows that
E[1P≥Q∣Q]=1−FP(Q),Var(1P≥Q∣Q)=FP(Q)(1−FP(Q))
and plugging these into the original expression for the variance we get
Var1P≥Q=E[Var(1P≥Q∣Q)]+Var(E[1P≥Q∣Q])=E[FP(Q)(1−FP(Q))]+Var(1−FP(Q))=E[FP(Q)]−E[FP2(Q)]+Var(FP(Q)); but VarX=E[X2]−E[X]2=E[FP(Q)]−E[FP2(Q)]+E[FP2(Q)]−(E[FP(Q)])2=E[FP(Q)](1−E[FP(Q)])
How much is E[FP(Q)]? This is indeed EQ[FP(Q)]; let fP,fQ denote the respective PDFs of P and Q. Then
None of the terms depend on the indices i,j,k,l anymore. The first term will output a factor of n0n1; the second one, n1n0(n0−1), and the third one n0n1(n1−1), hence
theoretical_var = variance(n0, n1, A, pxxy, pxyy)print("Theoretical variance:", round(theoretical_var,7))
Theoretical variance: 7.21e-05
Use bootstrapping:
def bootstrap_X_y(X, y, seed=None): n = len(X) if seed is not None: np.random.seed(seed) indices = np.random.randint(0, high=len(X), size=len(X)) return X[indices], y[indices]
In Experian, it is common for us to develop models and then backtest them with client data, as a free test before clients purchase the models. Assume we run backtests of a specific credit model with clients.
We need to ask them for a dataset to prove our product’s performance. The dataset must:
not be too small (so we avoid small data issues)
not be too big (complex, might have legal issues to share)
What is an optimal range for the sample size?
Let us assume the population imbalance ratio is α<1
Assume we want the ROC AUC to be precise to ϵ, an absolute number such as 0.01 (ie. a variance of 10−4).
Thus, it is reasonable to ask clients for a dataset with around 10,000 points for a backtest if we want to report AUC to 1 percentage point error.
The (beautiful & useless) extreme-imbalance, random classifier
Lett us get the simplest possible estimator: a random one, where all scores are uniformly sampled from [0,1]. In this context, how likely are we to get a spuriously large AUC?
In particular, we want to consider an extreme imbalance scenario where
n is small (say, less than 100)
The ratio n/N is small: n/N≪1 (say, 1% or less)
Our intuition tells us that this case should be more interesting than the big-data, balanced one: since one has only a few (n) points in class 1, if we are “lucky” to get high scores for all of them, the AUC will end up being high.
We will use the AUC estimator as defined above:
A^=n(N−n)1i=1∑nj=1∑N−n1Pi≥Qj
First, we have a trivial result: for all i∈I, j∈J, if Pi,Qj iid distributed as Uniform([0,1]) then
1Pi≥Qj∣Pi∼Bernoulli(Pi)
Since a binomial variable is built from a sum of independent Bernoulli ones, we have a corollary: for all i∈I,
j∈J∑1Pi≥QjPi∼Binomial(N−n,Pi)
Now, for large N−n, we may use the normal approximation to the binomial, namely a Binomial(n,p) variable converges to a N(μ=np,σ2=np(1−p)) variable as n grows. Hence
This defines a set of n variables Zi. To obtain their marginal distribution, notice that for any Zi its PDF is given by
pZi(z)=∫p(z∣Pi=p)p(Pi=p)dp;
but Pi∼Uniform([0,1]), and hence its PDF is just the identity on [0,1]. Letting
f(x∣μ,σ2)≡2πσ21exp[−2σ2(x−μ)2]
be the Gaussian PDF, we obtain
pZi(z)=∫01f(z∣p,N−np(1−p))dp
For N−n large (as is our hypothesis), the integrand (as a function of p) is basically a very sharp peak centered at p=z. In fact, we may approximate it as a Dirac delta function
f(z∣p,N−np(1−p))≈δ(z−p)
to obtain
pZi(z)=1z∈[0,1]⇒Zi∼Uniform([0,1])
We have also tested this numerically - even for n/N not that small this holds surprisingly well.
If we assume all Zi‘s are independent among themselves, the it means that nAUC is the sum of n independent uniform variables: it follows the so-called Irwin-Hall distribution, and we have our most important result below.
Notice: we have not proven this independence assumption - it is not obvious, and we prove it below.
Theorem. Let AUCrandom denote the ROC AUC estimator for a uniformly random scoring function. Let there be n instances of class 1 and N−n instances of class 0, where N≥n. Then
nAUCrandom∼IrwinHall(n);
notice that this result does not depend on N explicitly; we’ve only used that N is large and also much larger than n.
As Wikipedia can tell us, if A∼IrwinHall(n) then
E[A]=2n;Var(A)=12n;
for the AUC, this gives
E[AUCrandom]=21;Var[AUCrandom]=12n1
The first result is not surprising: we know that for a random scoring function the AUC should be 1/2. The second one is more surprising, and shows that as we increase n, we get an increasingly more precise AUC at 0.5 with a standard deviation that goes as 1/n.
We can now use this result to calculate how likely a statistical fluke is to happen: recall Chebyshev’s inequality for any (square-integrable) random variable X:
P(∣X−μ∣≥t)≤t2VarX
for our random AUC, this gives
P(AUCrandom−21≥t)≤12nt21
Examples:
n = 20print(f"{n=}")for t in [0.1, 0.2, 0.3, 0.4]: print(f"Probability of AUC > {0.5+t} is less than {round(100/(12*n*t**2), 2)}%")
n=20 Probability of AUC > 0.6 is less than 41.67% Probability of AUC > 0.7 is less than 10.42% Probability of AUC > 0.8 is less than 4.63% Probability of AUC > 0.9 is less than 2.6%
Proof that ROC AUC becomes the sum of scores for class 1
Recall that, above, we defined Zi as
A^=n(N−n)1i=1∑nj=1∑N−n1Pi≥Qj=n1i=1∑nZi
where
Zi≡Zi(N)=N−n1j∈J∑1Pi≥Qj.
We have shown that as N grows this variable becomes uniform. We can actually prove something stronger: that Zi essentially becomes Pi itself!
The intuition here is that, as N grows large, the Qj‘s basically cover the whole [0,1] interval, and since Zi cares only about their aggregated values, it essencially becomes independent of them.
More precisely: let QJ≡{Qj:j∈J} be a shorthand for all the scores in class 0. We will show three results:
Proposition 1:Zi∣QJ converges on the mean to Pi, ie.
N→∞limE[Zi(N)−PiQJ]=0
Corollary: Zi∣QJ converges in probability to Pi, ie.
N→∞limP(∣Zi(N)−Pi∣≥aQJ)=0,∀a>0
Proposition 2:Zi∣QJ converges in the mean-squared sense to Pi, ie.
N→∞limE[(Zi(N)−Pi)2QJ]=0
Proposition 2 alone implies Proposition 1 and the Corollary. We leave them here for completeness, and because the machinery is basically the same.
Since Pi is independent of QJ, this shows that the (conditional on QJ) variable Zi converges to the (unconditional on QJ) variable Pi. The problem is so unbalanced that the probabilities of the majority class are essentially “integrated out”, and only the scores of the minority class remain.
In practice, this means that
AUC≈n1i:yi=1∑Pi(very high imbalance, random scoring)
In what follows, we write N−n≡M to unclutter notation.
Proof of Proposition 1: by direct evaluation, using that the variable 1Pi≥Qj∣Qj is Bernoulli with parameter 1−Qj, we get
With no loss of generality, order the Qj‘s in an ascending order, so that Qj≥Qk if j>k. We can then simplify the second sum by considering that Pi must be larger than max(Qj,Qk)=Qj. Hence
Hence, putting all results (⋆) to (⋆⋆⋆) together, we get
N→∞limE[(Zi(N)−Pi)2QJ]=31−2⋅31+31=0
This proves Proposition 2.
Simulating this result
Below, we test
AUC≈n1i:yi=1∑Pi(very high imbalance, random scoring)
basically by creating a highly imbalanced dataset with only 15 points of class 1, and comparing the ROC AUC calculation with the average of the scores predicted for class 1.
from sklearn.metrics import roc_auc_score, roc_curvedef fixed_imbalanced_dataset(total_samples=20000, class_1_samples=15): import pandas as pd from sklearn.datasets import make_classification X, y = make_classification(n_samples=total_samples, weights=(0.8,), random_state=4) df = pd.DataFrame(X) df['y'] = y df0 = df[df['y']==0] df1 = df[df['y']==1] df1_red = df1.sample(n=class_1_samples) df_new = pd.concat([df1_red, df0]).sample(frac=1) X, y = df_new.drop('y', axis=1).values, df_new['y'].values return X, y
X, y = fixed_imbalanced_dataset()np.bincount(y)
array([15947, 15])
# create random predictionfor i, seed in enumerate([12,51,25,62]): np.random.seed(seed) y_pred = np.random.rand(*y.shape) roc_auc = roc_auc_score(y, y_pred) mean_prediction = y_pred[y==1].mean() print(f"Trial {i+1}:") print(f" - ROC AUC : {round(roc_auc,3)}") print(f" - Mean score class 1: {round(mean_prediction,3)}")
Trial 1: - ROC AUC : 0.594 - Mean score class 1: 0.593 Trial 2: - ROC AUC : 0.557 - Mean score class 1: 0.558 Trial 3: - ROC AUC : 0.549 - Mean score class 1: 0.551 Trial 4: - ROC AUC : 0.457 - Mean score class 1: 0.456
We see everything works: in this extremely imbalanced case, indeed the ROC AUC matches the mean predicted probabilities for the minority class!