diff --git a/lib/growth/calc/centile.ex b/lib/growth/calc/centile.ex index 332cf44..d06342f 100644 --- a/lib/growth/calc/centile.ex +++ b/lib/growth/calc/centile.ex @@ -1,6 +1,6 @@ defmodule Growth.Calc.Centile do @moduledoc """ - Calculate percentile for a given measurement and the fitted values of Box-Cox by age/height: + Calculate the value of measurement at a given z-score and fitted values of Box-Cox by age/height: * **power** `t:Growth.Calc.ZScore.l/0` * **median** `t:Growth.Calc.ZScore.m/0` @@ -12,9 +12,9 @@ defmodule Growth.Calc.Centile do iex> measures = ...> [ - ...> [30, -1.7862, 16.9392, 0.1107], - ...> [14, -1.3592, 20.4951, 0.12579], - ...> [19, -1.6318, 16.049, 0.10038] + ...> [-3.0, -1.7862, 16.9392, 0.1107], + ...> [2, -1.3592, 20.4951, 0.12579], + ...> [-1.2, -1.6318, 16.049, 0.10038] ...> ] iex> Enum.map(measures, &apply(Growth.Calc.Centile, :compute, &1)) [:na, :na, 19.0] @@ -32,17 +32,15 @@ defmodule Growth.Calc.Centile do ## Examples: - iex> Growth.Calc.Centile.compute(19, -1.6318, 16.049, 0.10038) + iex> Growth.Calc.Centile.compute(0.0, -1.6318, 16.049, 0.10038) 19.0 """ - def compute(y, l, m, s) do - zscore = ZScore.raw(y, l, m, s) + def compute(z_score, l, m, s) when -3 <= z_score and z_score <= 3 do + m * :math.pow(1 + l * s * z_score, 1 / l) + end - if -3 <= zscore and zscore <= 3 do - m * :math.pow(1 + l * s * zscore, 1 / l) - else - :na - end + def compute(_z_score, _l, _m, _s) do + :na end end diff --git a/lib/growth/calc/percentile.ex b/lib/growth/calc/percentile.ex new file mode 100644 index 0000000..8e1a1a6 --- /dev/null +++ b/lib/growth/calc/percentile.ex @@ -0,0 +1,30 @@ +defmodule Growth.Calc.Percentile do + @moduledoc """ + Convert the z-score of a given measurement into a cumulative percentile. + + This calculation is described in the [cumulative distribution function][https://en.wikipedia.org/wiki/Error_function#Cumulative_distribution_function]. + + ## Examples + + iex> z_scores = [-1.0, 0.0, 1.0] + iex> Enum.map(z_scores, &apply(Growth.Calc.Percentile, :compute, :&1)) + [0.15865525393145707, 0.5, 0.8413447460685429] + + """ + + @doc """ + Convert the z-score of a given measurement into a percentile representation, ranging from 0 to 1. + + ## Examples + + iex> Growth.Calc.Percentile.compute(2.0) + 0.9772498680518208 + iex> Growth.Calc.Percentile.compute(-2.0) + 0.02275013194817921 + + """ + @spec compute(number()) :: number() + def compute(z_score) do + 0.5 * (:math.erf(z_score / :math.sqrt(2)) + 1) + end +end diff --git a/lib/growth/score/scorer.ex b/lib/growth/score/scorer.ex index 6c74334..5767f38 100644 --- a/lib/growth/score/scorer.ex +++ b/lib/growth/score/scorer.ex @@ -1,9 +1,9 @@ defmodule Growth.Score.Scorer do @moduledoc """ - Behaviour defining common interface to calculate z-score and centile for a given measurement. + Behaviour defining common interface to calculate z-score and percentile for a given measurement. """ - alias Growth.Calc.Centile + alias Growth.Calc.Percentile alias Growth.Calc.ZScore @callback measure_name() :: atom() @@ -18,7 +18,7 @@ defmodule Growth.Score.Scorer do @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`. + Calculate z-score and percentile values for the given indicator and add them to the growth measurement `results`. """ def result(indicator, growth) do result = @@ -58,24 +58,22 @@ defmodule Growth.Score.Scorer do |> 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. + Calculate the z-score and percentile of an indicator measurement. """ + @spec scores(module(), Growth.t(), ZScore.l(), ZScore.m(), ZScore.s()) :: + {Growth.measure(), Growth.measure()} 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) - } + growth + |> Map.get(indicator.measure_name()) + |> z_score(l, m, s) + |> then(fn score -> {score, percentile(score)} end) end - @spec z_score(Growth.measure(), ZScore.l(), ZScore.m(), ZScore.s()) :: Growth.measure() @doc """ Check `Growth.Calc.ZScore.compute/4`. """ + @spec z_score(Growth.measure(), ZScore.l(), ZScore.m(), ZScore.s()) :: Growth.measure() def z_score(nil, _, _, _) do nil end @@ -84,15 +82,15 @@ defmodule Growth.Score.Scorer 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`. + Check `Growth.Calc.Percentile.compute/1`. """ - def centile(nil, _, _, _) do + @spec percentile(Growth.measure()) :: Growth.measure() + def percentile(nil) do nil end - def centile(value, l, m, s) do - Centile.compute(value, l, m, s) + def percentile(score) do + Percentile.compute(score) end end