diff --git a/.gitignore b/.gitignore index ede7de6..c917664 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vscode/ /**/.DS_Store node_modules +*_files/ diff --git a/README.md b/README.md index 2c9decb..d9e55aa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RheumaComposites.jl +# RheumaComposites.jl [![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/) diff --git a/docs/src/examples/categorisation.md b/docs/src/examples/categorisation.md index 1e26e5f..4b462d2 100644 --- a/docs/src/examples/categorisation.md +++ b/docs/src/examples/categorisation.md @@ -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. diff --git a/docs/src/index.md b/docs/src/index.md index 4bd8a69..b1ff11c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -2,7 +2,7 @@ EditURL = "https://github.com/simonsteiger/RheumaComposites.jl/blob/main/README.md" ``` -# RheumaComposites.jl +# RheumaComposites.jl [![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/) diff --git a/examples/categorisation.jl b/examples/categorisation.jl index 49a8b45..6ade4ad 100644 --- a/examples/categorisation.jl +++ b/examples/categorisation.jl @@ -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. diff --git a/src/functions/categorise.jl b/src/functions/categorise.jl index 5e6d617..e4db5d2 100644 --- a/src/functions/categorise.jl +++ b/src/functions/categorise.jl @@ -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)) diff --git a/src/functions/isremission.jl b/src/functions/isremission.jl index 142c668..e6f9b90 100644 --- a/src/functions/isremission.jl +++ b/src/functions/isremission.jl @@ -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 @@ -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) diff --git a/src/functions/weight.jl b/src/functions/weight.jl index 00e16a0..d50b219 100644 --- a/src/functions/weight.jl +++ b/src/functions/weight.jl @@ -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) diff --git a/src/types/boolean.jl b/src/types/boolean.jl index 7300b36..c90015a 100644 --- a/src/types/boolean.jl +++ b/src/types/boolean.jl @@ -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 diff --git a/src/types/cdai.jl b/src/types/cdai.jl index 479b23c..7276658 100644 --- a/src/types/cdai.jl +++ b/src/types/cdai.jl @@ -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 diff --git a/src/types/das28.jl b/src/types/das28.jl index dc99020..606e2c0 100644 --- a/src/types/das28.jl +++ b/src/types/das28.jl @@ -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 @@ -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 diff --git a/src/types/sdai.jl b/src/types/sdai.jl index 570ec27..f473b01 100644 --- a/src/types/sdai.jl +++ b/src/types/sdai.jl @@ -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 diff --git a/src/utils/auxfuns.jl b/src/utils/auxfuns.jl index c9640a0..8c6c63f 100644 --- a/src/utils/auxfuns.jl +++ b/src/utils/auxfuns.jl @@ -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 +=# diff --git a/src/utils/constants.jl b/src/utils/constants.jl index fbf5421..e9cb77b 100644 --- a/src/utils/constants.jl +++ b/src/utils/constants.jl @@ -1,4 +1,4 @@ -cont_cutoff = ( +cutoff = ( DAS28ESR=( remission=2.6, low=3.2, @@ -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 = ( diff --git a/test/types/boolean.jl b/test/types/boolean.jl index 132ebcb..3dba7ba 100644 --- a/test/types/boolean.jl +++ b/test/types/boolean.jl @@ -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 diff --git a/test/types/cdai.jl b/test/types/cdai.jl index 1d992fc..963f5da 100644 --- a/test/types/cdai.jl +++ b/test/types/cdai.jl @@ -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 diff --git a/test/types/das28.jl b/test/types/das28.jl index e20e948..643d51b 100644 --- a/test/types/das28.jl +++ b/test/types/das28.jl @@ -52,10 +52,10 @@ end end @testset "Categorise DAS28ESR" begin - @test categorise(das28e) == "Moderate" - @test categorise.(DAS28ESR, [2.59, 2.61]) == ["Remission", "Low"] - @test categorise.(DAS28ESR, [3.19, 3.21]) == ["Low", "Moderate"] - @test categorise.(DAS28ESR, [5.09, 5.11]) == ["Moderate", "High"] + @test categorise(das28e) == "moderate" + @test categorise.(DAS28ESR, [2.59, 2.61]) == ["remission", "low"] + @test categorise.(DAS28ESR, [3.19, 3.21]) == ["low", "moderate"] + @test categorise.(DAS28ESR, [5.09, 5.11]) == ["moderate", "high"] end @testset "Construct DAS28CRP" begin @@ -88,12 +88,16 @@ end @test faceted(das28c, facets) isa Faceted{<:ContinuousComposite} @test score(faceted(das28c, facets)) == score(das28c) @test sum(decompose(faceted(das28c, facets), digits=5)) ≈ 1.0 atol = 1e-5 - @test try faceted(das28c, (abc=[:tjc, :pga], cde=[:tjc, :apr])) catch e; e isa ErrorException end + @test try + faceted(das28c, (abc=[:tjc, :pga], cde=[:tjc, :apr])) + catch e + e isa ErrorException + end end @testset "Categorise DAS28CRP" begin - @test categorise(das28c) == "Moderate" - @test categorise.(DAS28CRP, [2.39, 2.51]) == ["Remission", "Low"] - @test categorise.(DAS28CRP, [2.89, 2.91]) == ["Low", "Moderate"] - @test categorise.(DAS28CRP, [4.59, 4.61]) == ["Moderate", "High"] + @test categorise(das28c) == "moderate" + @test categorise.(DAS28CRP, [2.39, 2.51]) == ["remission", "low"] + @test categorise.(DAS28CRP, [2.89, 2.91]) == ["low", "moderate"] + @test categorise.(DAS28CRP, [4.59, 4.61]) == ["moderate", "high"] end diff --git a/test/types/sdai.jl b/test/types/sdai.jl index faffd8d..cbdeb9b 100644 --- a/test/types/sdai.jl +++ b/test/types/sdai.jl @@ -35,8 +35,8 @@ end end @testset "Categorise SDAI" begin - @test categorise(sdai) == "Remission" - @test categorise.(SDAI, [3.29, 3.31]) == ["Remission", "Low"] - @test categorise.(SDAI, [10.99, 11.01]) == ["Low", "Moderate"] - @test categorise.(SDAI, [25.99, 26.01]) == ["Moderate", "High"] + @test categorise(sdai) == "remission" + @test categorise.(SDAI, [3.29, 3.31]) == ["remission", "low"] + @test categorise.(SDAI, [10.99, 11.01]) == ["low", "moderate"] + @test categorise.(SDAI, [25.99, 26.01]) == ["moderate", "high"] end