Skip to content

Commit d918a5c

Browse files
committed
reduce allocations in hs93
1 parent 7128ef7 commit d918a5c

2 files changed

Lines changed: 153 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# v0.11.8 (Upcoming Release)
22

33
- Reduce allocations in OLS and LTS. Implements new olsf() and olsf!() functions.
4+
- Recude allocations in hs93.
5+
46

57
# v0.11.7
68

src/hs93.jl

Lines changed: 151 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,53 @@ import ..Diagnostics: dffits
1111

1212
import Distributions: TDist, quantile
1313

14-
import LinearAlgebra: det
14+
import LinearAlgebra: ColumnNorm, PosDefException, Symmetric, cholesky, ldiv!, qr
15+
16+
@inline function _rowdot(
17+
X::AbstractMatrix{Float64},
18+
rowindex::Int,
19+
betas::AbstractVector{Float64},
20+
)::Float64
21+
s = 0.0
22+
@inbounds @simd for j in eachindex(betas)
23+
s += X[rowindex, j] * betas[j]
24+
end
25+
return s
26+
end
27+
28+
function _set_membership!(
29+
membership::AbstractVector{Bool},
30+
indices::AbstractVector{Int},
31+
)::Nothing
32+
fill!(membership, false)
33+
@inbounds for idx in indices
34+
membership[idx] = true
35+
end
36+
return nothing
37+
end
38+
39+
function _gramian!(
40+
target::AbstractMatrix{Float64},
41+
X::AbstractMatrix{Float64},
42+
indices::AbstractVector{Int},
43+
)::AbstractMatrix{Float64}
44+
fill!(target, 0.0)
45+
p = size(X, 2)
46+
@inbounds for idx in indices
47+
for col = 1:p
48+
xcol = X[idx, col]
49+
for row = 1:col
50+
target[row, col] += X[idx, row] * xcol
51+
end
52+
end
53+
end
54+
@inbounds for col = 1:p
55+
for row = (col + 1):p
56+
target[row, col] = target[col, row]
57+
end
58+
end
59+
return target
60+
end
1561

1662
"""
1763
@@ -112,31 +158,55 @@ function hs93basicsubset(
112158
initialindices::Array{Int,1},
113159
)::Array{Int,1}
114160
n, p = size(X)
115-
h = floor((n + p - 1) / 2)
161+
h = fld(n + p - 1, 2)
116162
s = length(initialindices)
117-
indices = initialindices
163+
indices = copy(initialindices)
164+
if h > s
165+
resize!(indices, h)
166+
end
118167
betas = zeros(Float64, p)
119168
d = zeros(Float64, n)
120-
orderingd = zeros(Int, n)
169+
orderingd = Array{Int}(undef, n)
170+
xtx = Matrix{Float64}(undef, p, p)
171+
insubset = falses(n)
172+
solvework = Vector{Float64}(undef, p)
173+
ymax = maximum(y)
174+
175+
for i in (s + 1):h
176+
activeindices = view(indices, 1:(i - 1))
177+
betas .= qr(view(X, activeindices, :), ColumnNorm()) \ view(y, activeindices)
178+
_set_membership!(insubset, activeindices)
179+
_gramian!(xtx, X, activeindices)
180+
181+
chol = try
182+
cholesky(Symmetric(xtx))
183+
catch err
184+
if err isa PosDefException
185+
nothing
186+
else
187+
rethrow(err)
188+
end
189+
end
121190

122-
for i in range(s + 1, stop = h)
123-
betas = X[indices, :] \ y[indices]
124-
XM = X[indices, :]
125191
for j = 1:n
126-
if det(XM'XM) > 0
127-
xxxx = X[j, :]' * inv(XM'XM) * X[j, :]
128-
if j in indices
129-
d[j] = abs.(y[j] - sum(X[j, :] .* betas)) / sqrt(abs(1 - xxxx))
192+
if !isnothing(chol)
193+
@inbounds for k = 1:p
194+
solvework[k] = X[j, k]
195+
end
196+
ldiv!(chol, solvework)
197+
xxxx = _rowdot(X, j, solvework)
198+
resid = abs(y[j] - _rowdot(X, j, betas))
199+
if insubset[j]
200+
d[j] = resid / sqrt(abs(1 - xxxx))
130201
else
131-
d[j] = abs.(y[j] - sum(X[j, :] .* betas)) / sqrt(abs(1 + xxxx))
202+
d[j] = resid / sqrt(abs(1 + xxxx))
132203
end
133204
else
134-
# When XM'XM is singular, the corresponding d[j] is set to the maximum of y.
135-
d[j] = maximum(y)
205+
d[j] = ymax
136206
end
137207
end
138-
orderingd .= sortperm(abs.(d))
139-
indices = orderingd[1:Int(i)]
208+
sortperm!(orderingd, d, by = abs)
209+
copyto!(indices, 1, orderingd, 1, i)
140210
end
141211
return indices
142212
end
@@ -205,16 +275,41 @@ function hs93(
205275
s = length(indices)
206276
betas = zeros(Float64, p)
207277
d = zeros(Float64, n)
208-
orderingd = zeros(Int, n)
278+
orderingd = Array{Int}(undef, n)
279+
if s < n
280+
workingsubset = copy(indices)
281+
resize!(workingsubset, n)
282+
else
283+
workingsubset = copy(indices)
284+
end
285+
indices = workingsubset
286+
xtx = Matrix{Float64}(undef, p, p)
287+
insubset = falses(n)
288+
solvework = Vector{Float64}(undef, p)
209289

210290
while s < n
211-
betas .= X[indices, :] \ y[indices]
212-
resids = y[indices] - X[indices,:] * betas
213-
sigma = sqrt(sum(resids .^ 2.0) / (length(resids) - p))
214-
215-
XM = X[indices, :]
291+
activeindices = view(indices, 1:s)
292+
betas .= qr(view(X, activeindices, :), ColumnNorm()) \ view(y, activeindices)
293+
294+
rss = 0.0
295+
@inbounds for idx in activeindices
296+
resid = y[idx] - _rowdot(X, idx, betas)
297+
rss += resid * resid
298+
end
299+
sigma = sqrt(rss / (s - p))
300+
301+
_gramian!(xtx, X, activeindices)
302+
chol = try
303+
cholesky(Symmetric(xtx))
304+
catch err
305+
if err isa PosDefException
306+
nothing
307+
else
308+
rethrow(err)
309+
end
310+
end
216311

217-
if det(XM'XM) <= 0
312+
if isnothing(chol)
218313
return Dict(
219314
"d" => [],
220315
"t" => [],
@@ -223,25 +318,45 @@ function hs93(
223318
"converged" => false,
224319
)
225320
end
226-
227-
iXmXm = inv(XM'XM)
321+
_set_membership!(insubset, activeindices)
228322
for j = 1:n
229-
xMMx = X[j, :]' * iXmXm * X[j, :]
230-
if j in indices
231-
d[j] = (y[j] - sum(X[j, :] .* betas)) / (sigma * sqrt(abs(1.0 - xMMx)))
323+
@inbounds for k = 1:p
324+
solvework[k] = X[j, k]
325+
end
326+
ldiv!(chol, solvework)
327+
xMMx = _rowdot(X, j, solvework)
328+
resid = y[j] - _rowdot(X, j, betas)
329+
if insubset[j]
330+
d[j] = resid / (sigma * sqrt(abs(1.0 - xMMx)))
232331
else
233-
d[j] = (y[j] - sum(X[j, :] .* betas)) / (sigma * sqrt(abs(1.0 + xMMx)))
332+
d[j] = resid / (sigma * sqrt(abs(1.0 + xMMx)))
234333
end
235334
end
236-
orderingd .= sortperm(abs.(d))
335+
sortperm!(orderingd, d, by = abs)
237336
tdist = TDist(s - p)
238337
tcalc = quantile(tdist, alpha / (2 * (s + 1)))
239338

240-
241-
outlierset = filter(x -> abs(d[x]) > abs(tcalc), 1:n)
242-
inlierset = setdiff(1:n, outlierset)
243-
cleanbetas = X[inlierset, :] \ y[inlierset]
244-
if abs(d[orderingd][s+1]) > abs(tcalc)
339+
if abs(d[orderingd[s + 1]]) > abs(tcalc)
340+
outliercount = 0
341+
@inbounds for j = 1:n
342+
if abs(d[j]) > abs(tcalc)
343+
outliercount += 1
344+
end
345+
end
346+
outlierset = Vector{Int}(undef, outliercount)
347+
inlierset = Vector{Int}(undef, n - outliercount)
348+
outlierpos = 1
349+
inlierpos = 1
350+
@inbounds for j = 1:n
351+
if abs(d[j]) > abs(tcalc)
352+
outlierset[outlierpos] = j
353+
outlierpos += 1
354+
else
355+
inlierset[inlierpos] = j
356+
inlierpos += 1
357+
end
358+
end
359+
cleanbetas = qr(view(X, inlierset, :), ColumnNorm()) \ view(y, inlierset)
245360
result = Dict(
246361
"d" => d,
247362
"t" => tcalc,
@@ -252,7 +367,7 @@ function hs93(
252367
return result
253368
end
254369
s += 1
255-
indices = orderingd[1:s]
370+
copyto!(indices, 1, orderingd, 1, s)
256371
end
257372

258373
return Dict(

0 commit comments

Comments
 (0)