Joao P Dubas 0d7a4cc9c3
All checks were successful
continuous-integration/drone/pr Build is passing
chore(growth): move nil logic to scorer behaviour
Handle `nil` measurement in `Growth.Score.Scorer`.
2024-06-11 14:53:19 +00:00

99 lines
2.5 KiB
Elixir

defmodule Growth.Score.Scorer do
@moduledoc """
Behaviour defining common interface to calculate z-score and centile for a given measurement.
"""
alias Growth.Calc.Centile
alias Growth.Calc.ZScore
@callback measure_name() :: atom()
@spec results(Growth.t(), [module()]) :: Growth.t()
@doc """
Add z-score and centile values in growth measurements `results` for each indicator.
"""
def results(growth, indicators) do
Enum.reduce(indicators, growth, &result/2)
end
@spec result(module(), Growth.t()) :: Growth.t()
@doc """
Calculate z-score and centile values for the given indicator and add them to the growth measurement `results`.
"""
def result(indicator, growth) do
result =
growth
|> lms(indicator)
|> Enum.map(fn {precision, {l, m, s}} ->
{precision, scores(indicator, growth, l, m, s)}
end)
%{growth | results: [Map.new([{indicator.measure_name(), result}]) | growth.results]}
end
@spec lms(Growth.t(), module()) :: [{String.t(), {number(), number(), number()}}]
@doc """
Get the indicaator fitted values of Box-Cox transformation:
* power (`l`)
* median (`m`)
* coefficient of variation (`s`)
"""
def lms(growth, indicator) do
[
{growth.gender, "day", growth.age_in_days},
{growth.gender, "week", growth.age_in_weeks},
{growth.gender, "month", growth.age_in_months}
]
|> Enum.map(fn {_, precision, _} = key ->
case :ets.lookup(indicator, key) do
[{^key, %{l: l, m: m, s: s}} | _] ->
{precision, {l, m, s}}
_ ->
nil
end
end)
|> Enum.reject(&is_nil/1)
end
@spec scores(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) ::
{Growth.measure(), number() | :na, nil}
@doc """
Calculate the z-score and centile of an indicator measurement.
"""
def scores(indicator, growth, l, m, s) do
measure = Map.get(growth, indicator.measure_name())
{
z_score(measure, l, m, s),
centile(measure, l, m, s)
}
end
@spec z_score(Growth.measure(), ZScore.l(), ZScore.m(), ZScore.s()) :: Growth.measure()
@doc """
Check `Growth.Calc.ZScore.compute/4`.
"""
def z_score(nil, _, _, _) do
nil
end
def z_score(value, l, m, s) do
ZScore.compute(value, l, m, s)
end
@spec centile(Growth.measure(), ZScore.l(), ZScore.m(), ZScore.s()) :: number() | :na | nil
@doc """
Check `Growth.Calc.Centile.compute/4`.
"""
def centile(nil, _, _, _) do
nil
end
def centile(value, l, m, s) do
Centile.compute(value, l, m, s)
end
end