Skip to content

Commit

Permalink
add Subset and generlise Revised (#14)
Browse files Browse the repository at this point in the history
* swap t28, s28 for tjc, sjc

Modify field names to use more common description. Online calculators mention swollen / tender joint counts without specifying the number of joints.

* add categories

- added categories to docstrings of composites
- added example about categorisation

* chore: straggler md after makedocs

* fix cutoffs in categorisation table

SDAI and CDAI have boundary-inclusive remission cutoffs, see SDAI calculator https://www.mdcalc.com/calc/2194/simple-disease-activity-index-sdai-rheumatoid-arthritis

* change `threeitem` to a `Subset`

implemented a more general Subset type that allows for keeping only a specified subset of components of a given composite. Made `threeitem(x::BooleanRemission)` a call to a prespecified `Subset`.

* generalise `revised`

`revised`can now change cutoffs for any component of a boolean remission, with the default implementation being that for the "classic" revised boolean composite

* Documentation and test

- make `ModifiedComposites` error on misspecification (same component assigned to two facets, subsetting by the same component twice)
- add tests that check if misspecification of `ModifiedComposites` errors as intended
- refactor main functions from utils to functions folder
  • Loading branch information
simonsteiger authored Aug 12, 2024
1 parent b5c07ad commit 6d77962
Show file tree
Hide file tree
Showing 29 changed files with 582 additions and 250 deletions.
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ end
pages = [
"Home" => "index.md",
"Tutorials" => [
"Basics" => joinpath("examples", "basics.md")
"Basics" => joinpath("examples", "basics.md"),
"Categorisation" => joinpath("examples", "categorisation.md"),
],
"API reference" => "api.md",
]
Expand Down
12 changes: 8 additions & 4 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,24 @@ CDAI
Faceted
BooleanRemission
Revised
ThreeItem
Subset
AbstractComponent
PGA
SJC28
SJC
```

## Interface

```@docs
t28
s28
tjc
sjc
pga
apr
ega
crp
offset
components
root
```

## Functions
Expand All @@ -47,6 +50,7 @@ decompose
categorise
faceted
revised
subset
threeitem
value
```
Expand Down
6 changes: 3 additions & 3 deletions docs/src/examples/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ This means that you do not have to remember that SDAI requires a 0-10 cm VAS sca
Let's try this by creating a DAS28CRP composite with patient's global assessment measured in centimeters:

````@example basics
das28_cm = DAS28CRP(t28=1, s28=0, pga=2.2u"cm", apr=4u"mg/L")
das28_cm = DAS28CRP(tjc=1, sjc=0, pga=2.2u"cm", apr=4u"mg/L")
````

As you can see, centimeters were automatically converted to millimeters.
Providing the same score in millimeters return the same result:

````@example basics
das28_mm = DAS28CRP(t28=1, s28=0, pga=22u"mm", apr=4u"mg/L")
das28_mm = DAS28CRP(tjc=1, sjc=0, pga=22u"mm", apr=4u"mg/L")
score(das28_cm) == score(das28_mm)
````

Expand All @@ -61,7 +61,7 @@ To see the docstring, first hit `?` in the REPL, then type the name of the compo
This is all we need to explore the most important aspects of many different composite scores!

````@example basics
sdai = SDAI(s28=3, t28=4, pga=34u"mm", ega=28u"mm", crp=21u"mg/L")
sdai = SDAI(sjc=3, tjc=4, pga=34u"mm", ega=28u"mm", crp=21u"mg/L")
````

````@example basics
Expand Down
57 changes: 57 additions & 0 deletions docs/src/examples/categorisation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
```@meta
EditURL = "../../../examples/categorisation.jl"
```

# Categorising composites

In this tutorial, we will explore how to convert continuous composites, like the DAS28ESR or SDAI, into discrete disease activity levels.

````@example categorisation
using RheumaComposites
using Unitful
````

As long as the composite you are defining is a continuous composite, all you need to do is to call [`categorise`](@ref).

````@example categorisation
sdai = SDAI(tjc=2, sjc=1, pga=6u"cm", ega=5.5u"cm", crp=15u"mg/L")
sdai isa ContinuousComposite
````

Well, that was expected!

Now let's see what the discrete disease activity level of this score would be.

````@example categorisation
categorise(sdai)
````

## Cutoffs

The cutoffs used per composite and category are:

| Disease activity | DAS28ESR | DAS28CRP | SDAI | CDAI |
|:-----------------|----------|----------|--------|--------|
| Remission | ``<`` 2.6 | ``<`` 2.4 | ``\leq`` 3.3 | ``\leq`` 2.8 |
| Low | ``\leq`` 3.2 | ``\leq`` 2.9 | ``\leq`` 11.0 | ``\leq`` 10.0 |
| 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`.
To retrieve the cutoff for a Moderate CDAI, you would:

````@example categorisation
import RheumaComposites: cont_cutoff
cont_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.
It is therefore safest to simply rely on the `categorise` function.

An alternative way to check the cutoffs is the respective composite's documentation.
You can see this by typing `?CDAI` in the REPL.

---

*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).*

6 changes: 3 additions & 3 deletions examples/basics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ This means that you do not have to remember that SDAI requires a 0-10 cm VAS sca
Let's try this by creating a DAS28CRP composite with patient's global assessment measured in centimeters:
=#

das28_cm = DAS28CRP(t28=1, s28=0, pga=2.2u"cm", apr=4u"mg/L")
das28_cm = DAS28CRP(tjc=1, sjc=0, pga=2.2u"cm", apr=4u"mg/L")

# As you can see, centimeters were automatically converted to millimeters.
# Providing the same score in millimeters return the same result:

das28_mm = DAS28CRP(t28=1, s28=0, pga=22u"mm", apr=4u"mg/L")
das28_mm = DAS28CRP(tjc=1, sjc=0, pga=22u"mm", apr=4u"mg/L")
score(das28_cm) == score(das28_mm)

#=
Expand All @@ -52,6 +52,6 @@ To see the docstring, first hit `?` in the REPL, then type the name of the compo
This is all we need to explore the most important aspects of many different composite scores!
=#

sdai = SDAI(s28=3, t28=4, pga=34u"mm", ega=28u"mm", crp=21u"mg/L")
sdai = SDAI(sjc=3, tjc=4, pga=34u"mm", ega=28u"mm", crp=21u"mg/L")
#-
score(sdai), isremission(sdai), categorise(sdai)
43 changes: 43 additions & 0 deletions examples/categorisation.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# # Categorising composites

# In this tutorial, we will explore how to convert continuous composites, like the DAS28ESR or SDAI, into discrete disease activity levels.

using RheumaComposites
using Unitful

# As long as the composite you are defining is a continuous composite, all you need to do is to call [`categorise`](@ref).

sdai = SDAI(tjc=2, sjc=1, pga=6u"cm", ega=5.5u"cm", crp=15u"mg/L")
sdai isa ContinuousComposite

# Well, that was expected!

# Now let's see what the discrete disease activity level of this score would be.

categorise(sdai)
#=
## Cutoffs
The cutoffs used per composite and category are:
| Disease activity | DAS28ESR | DAS28CRP | SDAI | CDAI |
|:-----------------|----------|----------|--------|--------|
| Remission | ``<`` 2.6 | ``<`` 2.4 | ``\leq`` 3.3 | ``\leq`` 2.8 |
| Low | ``\leq`` 3.2 | ``\leq`` 2.9 | ``\leq`` 11.0 | ``\leq`` 10.0 |
| 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`.
To retrieve the cutoff for a Moderate CDAI, you would:
=#

import RheumaComposites: cont_cutoff
cont_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.
It is therefore safest to simply rely on the `categorise` function.
An alternative way to check the cutoffs is the respective composite's documentation.
You can see this by typing `?CDAI` in the REPL.
=#
27 changes: 18 additions & 9 deletions src/RheumaComposites.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Term
using Unitful

export AbstractComponent
export PGA, SJC28
export PGA, SJC
export value
export AbstractComposite
export ContinuousComposite
Expand All @@ -22,8 +22,13 @@ export SDAI
export CDAI
export faceted, Faceted
export BooleanRemission
export revised, Revised, threeitem, ThreeItem
export t28, s28, pga, apr, ega, crp
export revised, Revised
export subset, Subset
export threeitem
export tjc, sjc, pga, apr, ega, crp
export components
export offset
export root
export intercept
export weight
export score
Expand All @@ -32,18 +37,22 @@ export decompose
export categorise

include("utils/units.jl")
include("utils/constants.jl")
include("utils/auxfuns.jl")
include("utils/valid.jl")

include("types/components.jl")
include("types/composites.jl")
include("types/modified.jl")
include("types/das28.jl")
include("types/sdai.jl")
include("types/cdai.jl")
include("types/boolean.jl")
include("types/modified.jl")
include("utils/weight.jl")
include("utils/score.jl")
include("utils/remission.jl")
include("utils/decompose.jl")
include("utils/categorise.jl")

include("functions/weight.jl")
include("functions/score.jl")
include("functions/isremission.jl")
include("functions/decompose.jl")
include("functions/categorise.jl")

end
28 changes: 14 additions & 14 deletions src/utils/categorise.jl → src/functions/categorise.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,38 @@ Convert `x` to a discrete value.
# Examples
```jldoctest
julia> DAS28ESR(t28=4, s28=5, pga=12u"mm", apr=44u"mm/hr") |> categorise
julia> DAS28ESR(tjc=4, sjc=5, pga=12u"mm", apr=44u"mm/hr") |> categorise
"Moderate"
```
"""
function categorise(::Type{DAS28ESR}, v)
out = v < 2.6 ? "Remission" :
v <= 3.2 ? "Low" :
v <= 5.1 ? "Moderate" :
out = v < cont_cutoff.DAS28ESR.remission ? "Remission" :
v <= cont_cutoff.DAS28ESR.low ? "Low" :
v <= cont_cutoff.DAS28ESR.moderate ? "Moderate" :
"High"
return out
end

function categorise(::Type{DAS28CRP}, v)
out = v < 2.4 ? "Remission" :
v <= 2.9 ? "Low" :
v <= 4.6 ? "Moderate" :
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 < 3.3 ? "Remission" :
v <= 11.0 ? "Low" :
v <= 26.0 ? "Moderate" :
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 < 2.8 ? "Remission" :
v <= 10.0 ? "Low" :
v <= 22.0 ? "Moderate" :
out = v < cont_cutoff.CDAI.remission ? "Remission" :
v <= cont_cutoff.CDAI.low ? "Low" :
v <= cont_cutoff.CDAI.moderate ? "Moderate" :
"High"
return out
end
Expand All @@ -60,4 +60,4 @@ julia> categorise(SDAI, 3.6)
"Low"
```
"""
categorise(x::Faceted{<:ContinuousComposite}) = categorise(typeof(x.c0), score(x.c0))
categorise(x::Faceted{<:ContinuousComposite}) = categorise(typeof(x.root), score(x.root))
12 changes: 6 additions & 6 deletions src/utils/decompose.jl → src/functions/decompose.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ See also [`score`](@ref).
# Examples
```jldoctest
julia> SDAI(t28=4, s28=5, pga=16u"mm", ega=12u"mm", crp=3u"mg/L") |> decompose
(t28 = 0.331, s28 = 0.413, pga = 0.132, ega = 0.099, crp = 0.025)
julia> SDAI(tjc=4, sjc=5, pga=16u"mm", ega=12u"mm", crp=3u"mg/L") |> decompose
(tjc = 0.331, sjc = 0.413, pga = 0.132, ega = 0.099, crp = 0.025)
```
"""
function decompose(x::ContinuousComposite; digits=3)
Expand All @@ -28,17 +28,17 @@ Return the proportion to which each facet contributes to the composite's score.
# Examples
```jldoctest
julia> c0 = DAS28ESR(t28=4, s28=5, pga=14u"mm", apr=12u"mm/hr");
julia> root = DAS28ESR(tjc=4, sjc=5, pga=14u"mm", apr=12u"mm/hr");
julia> faceted(c0, (objective=[:s28, :apr], subjective=[:t28, :pga])) |> decompose
julia> faceted(root, (objective=[:sjc, :apr], subjective=[:tjc, :pga])) |> decompose
(objective = 0.474, subjective = 0.525)
```
"""
function decompose(x::Faceted{<:ContinuousComposite}; digits=3)
c0 = x.c0
root = x.root
facets = propertynames(x.facets)
fields_per_facet = getproperty.(Ref(x.facets), facets)
decomp = decompose(c0; digits=digits)
decomp = decompose(root; digits=digits)
sum_per_facet = mapreduce(fields -> getproperty.(Ref(decomp), fields), +, fields_per_facet)
return NamedTuple{facets}(sum_per_facet)
end
52 changes: 52 additions & 0 deletions src/functions/isremission.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""
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

isremission(::Type{PGA}, x) = x.value <= 10.0u"mm"
isremission(::Type{SJC}, x) = x.value == 0

_check(component, x) = getproperty(bool_cutoff_funs, component)(x)

_check(component, x, offset) = getproperty(bool_cutoff_funs, component)(x; offset=offset)

function isremission(::Type{<:BooleanComposite}, x)
return mapreduce(component -> _check(component, x), &, components(x))
end

function isremission(::Type{<:Subset{N, <:BooleanComposite}}, x) where {N}
return mapreduce(component -> _check(component, root(x)), &, components(x))
end

function isremission(::Type{<:Revised{<:BooleanComposite}}, x)
offset_components = propertynames(offset(x))
out = mapreduce(&, components(x)) do compo
if compo in offset_components
compo_offset = getproperty(offset(x), compo)
_check(compo, root(x), compo_offset)
else
_check(compo, root(x))
end
end
return out
end

isremission(x::AbstractComposite) = isremission(typeof(x), x)
isremission(x::AbstractComponent) = isremission(typeof(x), x)
Loading

0 comments on commit 6d77962

Please sign in to comment.