diff --git a/lib/growth/score/scorer.ex b/lib/growth/score/scorer.ex new file mode 100644 index 0000000..26ab1cf --- /dev/null +++ b/lib/growth/score/scorer.ex @@ -0,0 +1,80 @@ +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, {z_score(indicator, growth, l, m, s), centile(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 z_score(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) :: number() + @doc """ + Check `Growth.Calc.ZScore.compute/4`. + """ + def z_score(indicator, growth, l, m, s) do + growth + |> Map.get(indicator.measure_name()) + |> ZScore.compute(l, m, s) + end + + @spec centile(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) :: number() | :na + @doc """ + Check `Growth.Calc.Centile.compute/4`. + """ + def centile(indicator, growth, l, m, s) do + growth + |> Map.get(indicator.measure_name()) + |> Centile.compute(l, m, s) + end +end