Skip to content

Commit 7fcea24

Browse files
authored
Added Minimal Support for Reliability Scores (aka Cronbach's alpha) (#701)
1 parent f557775 commit 7fcea24

File tree

5 files changed

+167
-1
lines changed

5 files changed

+167
-1
lines changed

docs/src/scalarstats.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,9 @@ modes
6262
summarystats
6363
describe
6464
```
65+
66+
## Reliability Measures
67+
68+
```@docs
69+
cronbachalpha
70+
```

src/StatsBase.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,11 @@ export
212212
standardize,
213213
AbstractDataTransform, # the type to represent a abstract data transformation
214214
ZScoreTransform, # the type to represent a z-score data transformation
215-
UnitRangeTransform # the type to represent a 0-1 data transformation
215+
UnitRangeTransform, # the type to represent a 0-1 data transformation
216+
217+
# reliability
218+
CronbachAlpha, # the type to represent Cronbach's alpha scores
219+
cronbachalpha # function to compute Cronbach's alpha scores
216220

217221
# source files
218222

@@ -232,6 +236,7 @@ include("partialcor.jl")
232236
include("empirical.jl")
233237
include("hist.jl")
234238
include("pairwise.jl")
239+
include("reliability.jl")
235240
include("misc.jl")
236241

237242
include("sampling.jl")

src/reliability.jl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
struct CronbachAlpha{T <: Real}
2+
alpha::T
3+
dropped::Vector{T}
4+
end
5+
6+
function Base.show(io::IO, x::CronbachAlpha)
7+
@printf(io, "Cronbach's alpha for all items: %.4f\n", x.alpha)
8+
isempty(x.dropped) && return
9+
println(io, "\nCronbach's alpha if an item is dropped:")
10+
for (idx, val) in enumerate(x.dropped)
11+
@printf(io, "item %i: %.4f\n", idx, val)
12+
end
13+
end
14+
15+
"""
16+
cronbachalpha(covmatrix::AbstractMatrix{<:Real})
17+
18+
Calculate Cronbach's alpha (1951) from a covariance matrix `covmatrix` according to
19+
the [formula](https://en.wikipedia.org/wiki/Cronbach%27s_alpha):
20+
21+
```math
22+
\\rho = \\frac{k}{k-1} (1 - \\frac{\\sum^k_{i=1} \\sigma^2_i}{\\sum_{i=1}^k \\sum_{j=1}^k \\sigma_{ij}})
23+
```
24+
25+
where ``k`` is the number of items, i.e. columns, ``\\sigma_i^2`` the item variance,
26+
and ``\\sigma_{ij}`` the inter-item covariance.
27+
28+
Returns a `CronbachAlpha` object that holds:
29+
30+
* `alpha`: the Cronbach's alpha score for all items, i.e. columns, in `covmatrix`; and
31+
* `dropped`: a vector giving Cronbach's alpha scores if a specific item,
32+
i.e. column, is dropped from `covmatrix`.
33+
34+
# Example
35+
```jldoctest
36+
julia> using StatsBase
37+
38+
julia> cov_X = [10 6 6 6;
39+
6 11 6 6;
40+
6 6 12 6;
41+
6 6 6 13];
42+
43+
julia> cronbachalpha(cov_X)
44+
Cronbach's alpha for all items: 0.8136
45+
46+
Cronbach's alpha if an item is dropped:
47+
item 1: 0.7500
48+
item 2: 0.7606
49+
item 3: 0.7714
50+
item 4: 0.7826
51+
```
52+
"""
53+
function cronbachalpha(covmatrix::AbstractMatrix{<:Real})
54+
isposdef(covmatrix) || throw(ArgumentError("Covariance matrix must be positive definite."))
55+
k = size(covmatrix, 2)
56+
k > 1 || throw(ArgumentError("Covariance matrix must have more than one column."))
57+
v = vec(sum(covmatrix, dims=1))
58+
σ = sum(v)
59+
for i in axes(v, 1)
60+
v[i] -= covmatrix[i, i]
61+
end
62+
σ_diag = sum(i -> covmatrix[i, i], 1:k)
63+
64+
alpha = k * (1 - σ_diag / σ) / (k - 1)
65+
if k > 2
66+
dropped = typeof(alpha)[(k - 1) * (1 - (σ_diag - covmatrix[i, i]) /- 2*v[i] - covmatrix[i, i])) / (k - 2)
67+
for i in 1:k]
68+
else
69+
# if k = 2 do not produce dropped; this has to be also
70+
# correctly handled in show
71+
dropped = Vector{typeof(alpha)}()
72+
end
73+
return CronbachAlpha(alpha, dropped)
74+
end

test/reliability.jl

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using StatsBase
2+
using LinearAlgebra, Random, Test
3+
4+
@testset "Cronbach's Alpha" begin
5+
# basic vanilla test
6+
cov_X = [10 6 6 6;
7+
6 11 6 6;
8+
6 6 12 6;
9+
6 6 6 13]
10+
cronbach_X = cronbachalpha(cov_X)
11+
@test cronbach_X isa CronbachAlpha{Float64}
12+
@test cronbach_X.alpha 0.8135593220338981
13+
@test cronbach_X.dropped
14+
[0.75, 0.7605633802816901, 0.7714285714285715, 0.782608695652174]
15+
16+
# testing Rational
17+
cov_rational = cov_X .// 1
18+
cronbach_rational = cronbachalpha(cov_rational)
19+
@test cronbach_rational isa CronbachAlpha{Rational{Int}}
20+
@test cronbach_rational.alpha == 48 // 59
21+
@test cronbach_rational.dropped ==
22+
[3 // 4, 54 // 71, 27 // 35, 18 // 23]
23+
24+
# testing BigFloat
25+
cov_bigfloat = BigFloat.(cov_X)
26+
cronbach_bigfloat = cronbachalpha(cov_bigfloat)
27+
@test cronbach_bigfloat isa CronbachAlpha{BigFloat}
28+
@test cronbach_bigfloat.alpha 0.8135593220338981
29+
@test cronbach_bigfloat.dropped
30+
[0.75, 0.7605633802816901, 0.7714285714285715, 0.782608695652174]
31+
32+
# testing corner cases
33+
@test_throws MethodError cronbachalpha([1.0, 2.0])
34+
cov_k2 = [10 6;
35+
6 11]
36+
cronbach_k2 = cronbachalpha(cov_k2)
37+
@test cronbach_k2.alpha 0.7272727272727273
38+
@test isempty(cronbach_k2.dropped)
39+
40+
# testing when Matrix is not positive-definite
41+
cov_not_pos = [-1 1;
42+
-1 1]
43+
@test_throws ArgumentError cronbachalpha(cov_not_pos)
44+
45+
# testing with a zero
46+
cov_zero = [1 2;
47+
0 1]
48+
@test_throws ArgumentError cronbachalpha(cov_not_pos)
49+
50+
# testing with one column
51+
cov_k1 = reshape([1, 2], 2, 1)
52+
@test_throws ArgumentError cronbachalpha(cov_k1)
53+
54+
# testing with Missing
55+
cov_missing = [1 2;
56+
missing 1]
57+
@test_throws MethodError cronbachalpha(cov_missing)
58+
59+
60+
# testing Base.show
61+
cronbach_X = cronbachalpha(cov_X)
62+
io = IOBuffer()
63+
show(io, cronbach_X)
64+
str = String(take!(io))
65+
@test str == """
66+
Cronbach's alpha for all items: 0.8136
67+
68+
Cronbach's alpha if an item is dropped:
69+
item 1: 0.7500
70+
item 2: 0.7606
71+
item 3: 0.7714
72+
item 4: 0.7826
73+
"""
74+
# for two columns
75+
io = IOBuffer()
76+
show(io, cronbach_k2)
77+
str = String(take!(io))
78+
@test str == "Cronbach's alpha for all items: 0.7273\n"
79+
80+
end # @testset "Cronbach's Alpha"

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ tests = ["ambiguous",
1818
"signalcorr",
1919
"misc",
2020
"pairwise",
21+
"reliability",
2122
"robust",
2223
"sampling",
2324
"wsampling",

0 commit comments

Comments
 (0)