-
-
Save jacksonpradolima/f9b19d65b7f16603c837024d5f8c8a65 to your computer and use it in GitHub Desktop.
import itertools as it | |
from bisect import bisect_left | |
from typing import List | |
import numpy as np | |
import pandas as pd | |
import scipy.stats as ss | |
from pandas import Categorical | |
def VD_A(treatment: List[float], control: List[float]): | |
""" | |
Computes Vargha and Delaney A index | |
A. Vargha and H. D. Delaney. | |
A critique and improvement of the CL common language | |
effect size statistics of McGraw and Wong. | |
Journal of Educational and Behavioral Statistics, 25(2):101-132, 2000 | |
The formula to compute A has been transformed to minimize accuracy errors | |
See: http://mtorchiano.wordpress.com/2014/05/19/effect-size-of-r-precision/ | |
:param treatment: a numeric list | |
:param control: another numeric list | |
:returns the value estimate and the magnitude | |
""" | |
m = len(treatment) | |
n = len(control) | |
if m != n: | |
raise ValueError("Data d and f must have the same length") | |
r = ss.rankdata(treatment + control) | |
r1 = sum(r[0:m]) | |
# Compute the measure | |
# A = (r1/m - (m+1)/2)/n # formula (14) in Vargha and Delaney, 2000 | |
A = (2 * r1 - m * (m + 1)) / (2 * n * m) # equivalent formula to avoid accuracy errors | |
levels = [0.147, 0.33, 0.474] # effect sizes from Hess and Kromrey, 2004 | |
magnitude = ["negligible", "small", "medium", "large"] | |
scaled_A = (A - 0.5) * 2 | |
magnitude = magnitude[bisect_left(levels, abs(scaled_A))] | |
estimate = A | |
return estimate, magnitude | |
def VD_A_DF(data, val_col: str = None, group_col: str = None, sort=True): | |
""" | |
:param data: pandas DataFrame object | |
An array, any object exposing the array interface or a pandas DataFrame. | |
Array must be two-dimensional. Second dimension may vary, | |
i.e. groups may have different lengths. | |
:param val_col: str, optional | |
Must be specified if `a` is a pandas DataFrame object. | |
Name of the column that contains values. | |
:param group_col: str, optional | |
Must be specified if `a` is a pandas DataFrame object. | |
Name of the column that contains group names. | |
:param sort : bool, optional | |
Specifies whether to sort DataFrame by group_col or not. Recommended | |
unless you sort your data manually. | |
:return: stats : pandas DataFrame of effect sizes | |
Stats summary :: | |
'A' : Name of first measurement | |
'B' : Name of second measurement | |
'estimate' : effect sizes | |
'magnitude' : magnitude | |
""" | |
x = data.copy() | |
if sort: | |
x[group_col] = Categorical(x[group_col], categories=x[group_col].unique(), ordered=True) | |
x.sort_values(by=[group_col, val_col], ascending=True, inplace=True) | |
groups = x[group_col].unique() | |
# Pairwise combinations | |
g1, g2 = np.array(list(it.combinations(np.arange(groups.size), 2))).T | |
# Compute effect size for each combination | |
ef = np.array([VD_A(list(x[val_col][x[group_col] == groups[i]].values), | |
list(x[val_col][x[group_col] == groups[j]].values)) for i, j in zip(g1, g2)]) | |
return pd.DataFrame({ | |
'A': np.unique(data[group_col])[g1], | |
'B': np.unique(data[group_col])[g2], | |
'estimate': ef[:, 0], | |
'magnitude': ef[:, 1] | |
}) | |
if __name__ == '__main__': | |
# Examples | |
# negligible | |
F = [0.8236111111111111, 0.7966666666666666, 0.923611111111111, 0.8197222222222222, 0.7108333333333333] | |
G = [0.8052777777777779, 0.8172222222222221, 0.8322222222222223, 0.783611111111111, 0.8141666666666666] | |
print(VD_A(G, F)) | |
# small | |
A = [0.478515625, 0.4638671875, 0.4638671875, 0.4697265625, 0.4638671875, 0.474609375, 0.4814453125, 0.4814453125, | |
0.4697265625, 0.4814453125, 0.474609375, 0.4833984375, 0.484375, 0.44921875, 0.474609375, 0.484375, | |
0.4814453125, 0.4638671875, 0.484375, 0.478515625, 0.478515625, 0.45703125, 0.484375, 0.419921875, | |
0.4833984375, 0.478515625, 0.4697265625, 0.484375, 0.478515625, 0.4638671875] | |
B = [0.4814453125, 0.478515625, 0.44921875, 0.4814453125, 0.4638671875, 0.478515625, 0.474609375, 0.4638671875, | |
0.474609375, 0.44921875, 0.474609375, 0.478515625, 0.478515625, 0.474609375, 0.4697265625, 0.474609375, | |
0.45703125, 0.4697265625, 0.478515625, 0.4697265625, 0.4697265625, 0.484375, 0.45703125, 0.474609375, | |
0.474609375, 0.4638671875, 0.45703125, 0.474609375, 0.4638671875, 0.4306640625] | |
print(VD_A(A, B)) | |
# medium | |
C = [0.9108333333333334, 0.8755555555555556, 0.900277777777778, 0.9274999999999999, 0.8777777777777779] | |
E = [0.8663888888888888, 0.8802777777777777, 0.7816666666666667, 0.8377777777777776, 0.9305555555555556] | |
print(VD_A(C, E)) | |
# Large | |
D = [0.7202777777777778, 0.77, 0.8544444444444445, 0.7947222222222222, 0.7577777777777778] | |
print(VD_A(C, D)) |
I'm running the following code snippet, comparing two lists of numbers sampled from some random distribution. I would expect that the results of these comparisons should show something non-trivial ,but for almost every parameter of the normal or uniform distribution i am using, the A value is always 0.0? Wondering if you could help me understand this. Much thanks.
if __name__ == '__main__':
rng = np.random.default_rng(2)
a = rng.uniform(0.8514, 1, 10)
b = rng.uniform(0.945, 1, 10)
print(f'uniform a & b: {VD_A(a,b)}')
a = rng.normal(62.8125, 134, 10)
b = rng.normal(10.3199, 1.124, 10)
print(f'normal a & b: {VD_A(a,b)}')`
Hey @wingos80! Checked out your issue, and here’s the lowdown:
What’s Happening?
The VD_A
function calculates the Vargha-Delaney A index based on ranks of two lists. If one list completely dominates the other in ranking (like all b
values are higher than a
), the A value will be 0.0
. That’s just how the formula works—it’s not a bug but a feature! 😄
In your case:
- Uniform Distributions: Your ranges overlap slightly, but the
b
values are consistently higher, soVD_A
gives0.0
. - Normal Distributions: Same story—your parameters for
b
make its values dominatea
.
Why Does It Return 0.0
?
A value of 0.0
means all the b
values rank higher than a
. The A index is about ranks, not raw values, so this can happen even if the lists look “close” numerically.
How to Fix This?
If you want non-trivial results, tweak your data ranges to have more overlap. For example:
- For uniform, try
rng.uniform(0.5, 0.75)
fora
andrng.uniform(0.7, 0.9)
forb
. - For normal, get closer means or tweak the standard deviations.
Here’s a quick tweak to your code to make it more interesting:
if __name__ == '__main__':
rng = np.random.default_rng(2)
# Adjust ranges for more overlap
a = rng.uniform(0.5, 0.75, 10)
b = rng.uniform(0.7, 0.9, 10)
print(f'Uniform a & b: {VD_A(a, b)}')
a = rng.normal(50, 10, 10)
b = rng.normal(55, 10, 10)
print(f'Normal a & b: {VD_A(a, b)}')
This should give you A values that aren’t just 0.0
.
TL;DR
0.0
is valid—it just meansb
ranks abovea
in all cases.- To get more varied results, adjust your input ranges so the groups overlap more.
- The function is working as intended—nothing’s broken here!
Let me know if you still hit a wall.
@wingos80 here has a python test with multiple tests and covering your case: https://github.com/jacksonpradolima/coleman4hcs/blob/main/coleman4hcs/tests/statistics/test_varga_delaney.py
The same result from Python code can be found using R code: https://github.com/mtorchiano/effsize/blob/master/R/VD_A.R