Skip to content

Commit

Permalink
rely on tuple indexing to simplify categorise (#15)
Browse files Browse the repository at this point in the history
- `categorise` is now an impure function since it indexes a `NamedTuple` with cutoffs
- benchmarking suggests that 1 microsecond is lost due to the lookup that this incurs (50% slowdown)
- the remaining time is mostly spent in `weight`, and likely even in building the type-unstable `Tuple` with `getproperty.(Ref(x), components(x)`. Not sure how to solve this yet, and eventually might revisit this code to speed it up.
  • Loading branch information
simonsteiger authored Aug 15, 2024
1 parent 6d77962 commit 2dc8b17
Show file tree
Hide file tree
Showing 18 changed files with 171 additions and 120 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.vscode/
/**/.DS_Store
node_modules
*_files/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# RheumaComposites.jl
# RheumaComposites.jl <img src='docs/src/assets/logo.svg' align='right' height='135'/>

[![Build Status](https://github.com/simonsteiger/RheumaComposites.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/simonsteiger/RheumaComposites.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://simonsteiger.github.io/RheumaComposites.jl/dev/)
Expand Down
6 changes: 3 additions & 3 deletions docs/src/examples/categorisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ The cutoffs used per composite and category are:
| Moderate | ``\leq`` 5.1 | ``\leq`` 4.6 | ``\leq`` 26.0 | ``\leq`` 22.0 |
| High | ``>`` 5.1 | ``>`` 4.6 | ``>`` 26.0 | ``>`` 22.0 |

Internally, these are saved in a `NamedTuple` which you can import with `import RheumaComposites: cont_cutoff`.
Internally, these are saved in a `NamedTuple` which you can import with `import RheumaComposites: cutoff`.
To retrieve the cutoff for a Moderate CDAI, you would:

````@example categorisation
import RheumaComposites: cont_cutoff
cont_cutoff.CDAI.moderate
import RheumaComposites: cutoff
cutoff.CDAI.moderate
````

Note that this only returns the boundary value of the respective category, and that the inclusion of this value varies across **both** composites and categories.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
EditURL = "https://github.com/simonsteiger/RheumaComposites.jl/blob/main/README.md"
```

# RheumaComposites.jl
# RheumaComposites.jl <img src='docs/src/assets/logo.svg' align='right' height='135'/>

[![Build Status](https://github.com/simonsteiger/RheumaComposites.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/simonsteiger/RheumaComposites.jl/actions/workflows/CI.yml?query=branch%3Amain)
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://simonsteiger.github.io/RheumaComposites.jl/dev/)
Expand Down
6 changes: 3 additions & 3 deletions examples/categorisation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ The cutoffs used per composite and category are:
| Moderate | ``\leq`` 5.1 | ``\leq`` 4.6 | ``\leq`` 26.0 | ``\leq`` 22.0 |
| High | ``>`` 5.1 | ``>`` 4.6 | ``>`` 26.0 | ``>`` 22.0 |
Internally, these are saved in a `NamedTuple` which you can import with `import RheumaComposites: cont_cutoff`.
Internally, these are saved in a `NamedTuple` which you can import with `import RheumaComposites: cutoff`.
To retrieve the cutoff for a Moderate CDAI, you would:
=#

import RheumaComposites: cont_cutoff
cont_cutoff.CDAI.moderate
import RheumaComposites: cutoff
cutoff.CDAI.moderate

#=
Note that this only returns the boundary value of the respective category, and that the inclusion of this value varies across **both** composites and categories.
Expand Down
66 changes: 22 additions & 44 deletions src/functions/categorise.jl
Original file line number Diff line number Diff line change
@@ -1,63 +1,41 @@
"""
categorise(x::ContinuousComposite)
categorise(::Type{T}, s::Real) where {T<:ContinuousComposite}
Convert `x` to a discrete value.
Convert score `s` to a discrete value using `SDAI` thresholds.
The same functionality exists for other `ContinuousComposites`.
# Examples
```jldoctest
julia> DAS28ESR(tjc=4, sjc=5, pga=12u"mm", apr=44u"mm/hr") |> categorise
"Moderate"
julia> categorise(SDAI, 3.6)
"low"
```
"""
function categorise(::Type{DAS28ESR}, v)
out = v < cont_cutoff.DAS28ESR.remission ? "Remission" :
v <= cont_cutoff.DAS28ESR.low ? "Low" :
v <= cont_cutoff.DAS28ESR.moderate ? "Moderate" :
"High"
return out
function categorise(::Type{T}, s::Real) where {T<:ContinuousComposite}
return seq_check(s, getproperty(cont_cutoff_funs, Symbol(T)))
end

function categorise(::Type{DAS28CRP}, v)
out = v < cont_cutoff.DAS28CRP.remission ? "Remission" :
v <= cont_cutoff.DAS28CRP.low ? "Low" :
v <= cont_cutoff.DAS28CRP.moderate ? "Moderate" :
"High"
return out
end

function categorise(::Type{SDAI}, v)
out = v < cont_cutoff.SDAI.remission ? "Remission" :
v <= cont_cutoff.SDAI.low ? "Low" :
v <= cont_cutoff.SDAI.moderate ? "Moderate" :
"High"
return out
end

function categorise(::Type{CDAI}, v)
out = v < cont_cutoff.CDAI.remission ? "Remission" :
v <= cont_cutoff.CDAI.low ? "Low" :
v <= cont_cutoff.CDAI.moderate ? "Moderate" :
"High"
return out
end

categorise(x::ContinuousComposite) = categorise(typeof(x), score(x))
# This implementation is roughly half as 2.5 times slower than hard coding
# cutoffs into each categorise function
# If performance is ever critical, I should change to the more verbose but faster version

"""
categorise(::Type{SDAI}, v)
Convert `v` to a discrete value using `SDAI` thresholds.
The same functionality exists for other `ContinuousComposites`.
categorise(x::ContinuousComposite)
See also [`DAS28ESR`](@ref), [`DAS28CRP`](@ref).
Convert `x` to a discrete value.
# Examples
```jldoctest
julia> categorise(SDAI, 3.6)
"Low"
julia> DAS28ESR(tjc=4, sjc=5, pga=12u"mm", apr=44u"mm/hr") |> categorise
"moderate"
```
"""
categorise(x::ContinuousComposite) = categorise(typeof(x), score(x))

"""
categorise(x::Faceted{<:ContinuousComposite})
Convert the `root` composite of `x` to a discrete value.
"""
categorise(x::Faceted{<:ContinuousComposite}) = categorise(typeof(x.root), score(x.root))
59 changes: 38 additions & 21 deletions src/functions/isremission.jl
Original file line number Diff line number Diff line change
@@ -1,24 +1,7 @@
"""
isremission(x::AbstractComposite)
Check whether a composite fulfils remission criteria.
# Examples
```jldoctest
julia> DAS28ESR(tjc=4, sjc=5, pga=44u"mm", apr=23u"mm/hr") |> isremission
false
julia> BooleanRemission(tjc=1, sjc=0, pga=14u"mm", crp=0.4u"mg/dl") |>
revised |>
isremission
true
```
"""
isremission(::Type{DAS28ESR}, x) = score(x) < cont_cutoff.DAS28ESR.remission
isremission(::Type{DAS28CRP}, x) = score(x) < cont_cutoff.DAS28CRP.remission

isremission(::Type{SDAI}, x) = score(x) <= cont_cutoff.SDAI.remission
isremission(::Type{CDAI}, x) = score(x) <= cont_cutoff.CDAI.remission
function isremission(::Type{T}, x::AbstractComposite) where {T<:ContinuousComposite}
cut = getproperty(cont_cutoff_funs, Symbol(T))
return cut.remission(score(x))
end

isremission(::Type{PGA}, x) = x.value <= 10.0u"mm"
isremission(::Type{SJC}, x) = x.value == 0
Expand Down Expand Up @@ -48,5 +31,39 @@ function isremission(::Type{<:Revised{<:BooleanComposite}}, x)
return out
end

"""
isremission(x::AbstractComposite)
Check whether a composite fulfils remission criteria.
# Examples
```jldoctest
julia> DAS28ESR(tjc=4, sjc=5, pga=44u"mm", apr=23u"mm/hr") |> isremission
false
julia> BooleanRemission(tjc=1, sjc=0, pga=14u"mm", crp=0.4u"mg/dl") |>
revised |>
isremission
true
```
"""
isremission(x::AbstractComposite) = isremission(typeof(x), x)

"""
isremission(::Type{T}, s::Real) where {T<:ContinuousComposite}
Check whether a composite fulfils remission criteria.
# Examples
```jldoctest
julia> isremission(DAS28ESR, 3.9)
false
```
"""
function isremission(::Type{T}, s::Real) where {T<:ContinuousComposite}
cut = getproperty(cont_cutoff_funs, Symbol(T))
return cut.remission(s)
end

isremission(x::AbstractComponent) = isremission(typeof(x), x)
11 changes: 4 additions & 7 deletions src/functions/weight.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@ weight(x::Subset{N,T}) where {N,T} = weight(WeightingScheme(T), x)

weight(::IsUnweightable, x::T) where {T} = throw(ErrorException("$(typeof(x)) type is unweightable."))

weight(::IsUnweighted, x::T) where {T} = ustrip.(getproperty.(Ref(x), fieldnames(T)))
weight(::IsUnweighted, x::T) where {T} = ustrip.(getproperty.(Ref(x), components(x)))

function map_weights(weights, x)
weighted_values = map(components(x)) do component
component_value = ustrip(getproperty(root(x), component))
component_weight = getproperty(weights, component)
component_weight(component_value)
end
return weighted_values
component_values = ustrip.(getproperty.(Ref(x), components(x)))
component_weights = getproperty.(Ref(weights), components(x))
return map((w, v) -> w(v), component_weights, component_values)
end

function weight(::IsWeighted, x::DAS28)
Expand Down
2 changes: 1 addition & 1 deletion src/types/boolean.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ struct BooleanRemission <: BooleanComposite
valid_joints.([tjc, sjc])
valid_vas(pga)
valid_apr(crp)
return new(tjc, sjc, pga, uconvert(units.brem_crp, crp))
return new(tjc, sjc, uconvert(units.brem_vas, pga), uconvert(units.brem_crp, crp))
end
end

Expand Down
8 changes: 4 additions & 4 deletions src/types/cdai.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ Store component measures of the Clinical Disease Activity Index, or CDAI.
# Categories
- ``<`` $(cont_cutoff.CDAI.low) = Remission
- ``\\leq`` $(cont_cutoff.CDAI.low) = Low
- ``\\leq`` $(cont_cutoff.CDAI.moderate) = Moderate
- ``>`` $(cont_cutoff.CDAI.moderate) = High
- ``<`` $(cutoff.CDAI.low) = Remission
- ``\\leq`` $(cutoff.CDAI.low) = Low
- ``\\leq`` $(cutoff.CDAI.moderate) = Moderate
- ``>`` $(cutoff.CDAI.moderate) = High
# External links
Expand Down
16 changes: 8 additions & 8 deletions src/types/das28.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ Store the component measures of the DAS28CRP.
# Categories
- ``<`` $(cont_cutoff.DAS28CRP.low) = Remission
- ``\\leq`` $(cont_cutoff.DAS28CRP.low) = Low
- ``\\leq`` $(cont_cutoff.DAS28CRP.moderate) = Moderate
- ``>`` $(cont_cutoff.DAS28CRP.moderate) = High
- ``<`` $(cutoff.DAS28CRP.low) = Remission
- ``\\leq`` $(cutoff.DAS28CRP.low) = Low
- ``\\leq`` $(cutoff.DAS28CRP.moderate) = Moderate
- ``>`` $(cutoff.DAS28CRP.moderate) = High
# External links
Expand Down Expand Up @@ -83,10 +83,10 @@ Store the component measures of the DAS28ESR.
# Categories
- ``<`` $(cont_cutoff.DAS28ESR.low) = Remission
- ``\\leq`` $(cont_cutoff.DAS28ESR.low) = Low
- ``\\leq`` $(cont_cutoff.DAS28ESR.moderate) = Moderate
- ``>`` $(cont_cutoff.DAS28ESR.moderate) = High
- ``<`` $(cutoff.DAS28ESR.low) = Remission
- ``\\leq`` $(cutoff.DAS28ESR.low) = Low
- ``\\leq`` $(cutoff.DAS28ESR.moderate) = Moderate
- ``>`` $(cutoff.DAS28ESR.moderate) = High
# External links
Expand Down
8 changes: 4 additions & 4 deletions src/types/sdai.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ Store component measures of the Simplified Disease Activity Index, or SDAI.
# Categories
- ``\\leq`` $(cont_cutoff.SDAI.low) = Remission
- ``\\leq`` $(cont_cutoff.SDAI.low) = Low
- ``\\leq`` $(cont_cutoff.SDAI.moderate) = Moderate
- ``>`` $(cont_cutoff.SDAI.moderate) = High
- ``\\leq`` $(cutoff.SDAI.low) = Remission
- ``\\leq`` $(cutoff.SDAI.low) = Low
- ``\\leq`` $(cutoff.SDAI.moderate) = Moderate
- ``>`` $(cutoff.SDAI.moderate) = High
# External links
Expand Down
20 changes: 20 additions & 0 deletions src/utils/auxfuns.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,23 @@ function values_flatten(x::NamedTuple)
collect
return property_vec
end

function seq_check(x::Real, conds::NamedTuple)
funs = values(conds)
i = 1
for fun in funs
fun(x) && break
i += 1
end
return string(keys(conds)[i])
end

#=
function seq_check(x::BooleanComposite, conds::NamedTuple)
funs = values(conds)
for fun in funs
fun(x) || return false
end
return true
end
=#
44 changes: 39 additions & 5 deletions src/utils/constants.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
cont_cutoff = (
cutoff = (
DAS28ESR=(
remission=2.6,
low=3.2,
Expand All @@ -19,13 +19,47 @@ cont_cutoff = (
low=10.0,
moderate=22.0
),
BooleanRemission=(
tjc=1,
sjc=1,
pga=10u"mm",
crp=1.0u"mg/dL",
),
)

bool_cutoff_funs = (
tjc=(x; offset = 0) -> tjc(x) <= 1 + offset,
sjc=(x; offset = 0) -> sjc(x) <= 1 + offset,
pga=(x; offset = 0u"mm") -> pga(x) <= 10u"mm" + offset,
crp=(x; offset = 0u"mg/dL") -> crp(x) <= 1.0u"mg/dL" + offset,
tjc=(x; offset = 0) -> tjc(x) <= cutoff.BooleanRemission.tjc + offset,
sjc=(x; offset = 0) -> sjc(x) <= cutoff.BooleanRemission.sjc + offset,
pga=(x; offset = 0u"mm") -> pga(x) <= cutoff.BooleanRemission.pga + offset,
crp=(x; offset = 0u"mg/dL") -> crp(x) <= cutoff.BooleanRemission.crp + offset,
)

# TODO add a check that asserts that all continuous composites have an entry here
cont_cutoff_funs = (
DAS28ESR=(
remission=(x) -> x < cutoff.DAS28ESR.remission,
low=(x) -> x <= cutoff.DAS28ESR.low,
moderate=(x) -> x <= cutoff.DAS28ESR.moderate,
high=(x) -> x > cutoff.DAS28ESR.moderate,
),
DAS28CRP=(
remission=(x) -> x < cutoff.DAS28CRP.remission,
low=(x) -> x <= cutoff.DAS28CRP.low,
moderate=(x) -> x <= cutoff.DAS28CRP.moderate,
high=(x) -> x > cutoff.DAS28CRP.moderate,
),
SDAI=(
remission=(x) -> x <= cutoff.SDAI.remission,
low=(x) -> x <= cutoff.SDAI.low,
moderate=(x) -> x <= cutoff.SDAI.moderate,
high=(x) -> x > cutoff.SDAI.moderate,
),
CDAI=(
remission=(x) -> x <= cutoff.CDAI.remission,
low=(x) -> x <= cutoff.CDAI.low,
moderate=(x) -> x <= cutoff.CDAI.moderate,
high=(x) -> x > cutoff.CDAI.moderate,
),
)

weights_das28esr = (
Expand Down
2 changes: 1 addition & 1 deletion test/types/boolean.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
boolrem = BooleanRemission(tjc=1, sjc=0, pga=14u"mm", crp=0.4u"mg/dl")
boolrem = BooleanRemission(tjc=1, sjc=0, pga=14u"mm", crp=0.4u"mg/dL")

@testset "Original BoolRem" begin
@test boolrem isa AbstractComposite
Expand Down
8 changes: 4 additions & 4 deletions test/types/cdai.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ end
end

@testset "Categorise CDAI" begin
@test categorise(cdai) == "Remission"
@test categorise.(CDAI, [2.79, 2.81]) == ["Remission", "Low"]
@test categorise.(CDAI, [9.99, 10.01]) == ["Low", "Moderate"]
@test categorise.(CDAI, [21.99, 22.01]) == ["Moderate", "High"]
@test categorise(cdai) == "remission"
@test categorise.(CDAI, [2.79, 2.81]) == ["remission", "low"]
@test categorise.(CDAI, [9.99, 10.01]) == ["low", "moderate"]
@test categorise.(CDAI, [21.99, 22.01]) == ["moderate", "high"]
end
Loading

0 comments on commit 2dc8b17

Please sign in to comment.