[WIP] Implement growth assessment using WHO indicators #80

Draft
joao.dubas wants to merge 76 commits from jpd-feat-add-bmi-module-with-live-view into main
38 changed files with 32836 additions and 4 deletions

View File

@ -27,11 +27,15 @@ credo: ## run credo
.PHONY: dialyzer
dialyzer: ## run dialyzer
@mix dialyzer --no-check --quiet --ignore-exit-status --format short
@mix dialyzer --format short
.PHONY: static_code_analysis
static_code_analysis: check_format check_compile credo dialyzer ## run static code analysis
.PHONY: docs
docs: ## create documentation files
@mix docs
.PHONY: test
test: ## run tests
@mix test --cover --trace --slowest 10

View File

@ -1,5 +1,8 @@
import Config
config :wabanex, Growth.Indicators.Download,
who_req_options: [plug: {Req.Test, Growth.Indicators.Download.WHO}]
config :wabanex, Wabanex.Repo,
database: "wabanex_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox

48
lib/growth/calc/age.ex Normal file
View File

@ -0,0 +1,48 @@
defmodule Growth.Calc.Age do
@moduledoc """
Calculate the age in months.
"""
@type precision :: :day | :week | :month | :year
# NOTE: (jpd): based on [WHO instructions][0]
# [0]: https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/instructions-en.pdf
@day_in_month 30.4375
@day_in_week 7.0
@doc """
Calculate the age with the precision of `:day`, or `:week`, or `:month`, considering the measurement date as today.
For age in weeks or months, considers completed ones, removing decimal value.
"""
@spec calculate(precision(), Date.t(), Date.t()) :: pos_integer()
def calculate(precision, date_of_birth) do
calculate(precision, date_of_birth, Date.utc_today())
end
@doc """
Calculate the age with the precision of `:day`, or `:week`, or `:month`.
For age in weeks or months, considers completed ones, removing decimal value.
"""
def calculate(:month, date_of_birth, date_of_measurement) do
:day
|> calculate(date_of_birth, date_of_measurement)
|> Kernel./(@day_in_month)
|> floor()
end
def calculate(:week, date_of_birth, date_of_measurement) do
:day
|> calculate(date_of_birth, date_of_measurement)
|> Kernel./(@day_in_week)
|> floor()
end
def calculate(:day, date_of_birth, date_of_measurement) do
Date.diff(date_of_measurement, date_of_birth)
end
end

29
lib/growth/calc/bmi.ex Normal file
View File

@ -0,0 +1,29 @@
defmodule Growth.Calc.BMI do
@moduledoc """
Calculate body mass index
"""
@inch_to_centimeter 2.54
@pound_to_kilogram 0.453592
@doc """
Calculate the body mass index for a given weight and height.
Measurements taken in english unit (pounds and inches) are converted to their metric equivalents (kilograms and centimeters).
"""
@spec calculate(:english | :metric, number, number) :: number
def calculate(:english, weight, height) do
metric_weight = pound_to_kilogram(weight)
metric_height = inches_to_centimeters(height)
calculate(:metric, metric_weight, metric_height)
end
def calculate(:metric, weight, height) do
weight / :math.pow(height / 100.0, 2)
end
defp inches_to_centimeters(height), do: height * @inch_to_centimeter
defp pound_to_kilogram(weight), do: weight * @pound_to_kilogram
end

View File

@ -0,0 +1,45 @@
defmodule Growth.Calc.Centile do
@moduledoc """
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`
* **coefficientof variation** `t:Growth.Calc.ZScore.s/0`
This calculation is described in the [instructions provided by the World Health Organization](https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/computation.pdf).
## Examples:
iex> measures =
...> [
...> [-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))
[13.05127032828574, 27.884359024082663, 14.37765739914362]
"""
alias Growth.Calc.ZScore
@spec compute(number(), ZScore.l(), ZScore.m(), ZScore.s()) :: number() | :na
@doc """
Compute the measurement value for a given z-score and the Box-Cox values: power (`l`), median (`m`), and coefficient of variation (`s`).
Returns the measurement based on the z-score when -3 <= z-score <= 3; otherwise, `:na.`
## Examples:
iex> Growth.Calc.Centile.compute(0.0, -1.6318, 16.049, 0.10038)
16.049
"""
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
def compute(_z_score, _l, _m, _s) do
:na
end
end

View File

@ -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

116
lib/growth/calc/z_score.ex Normal file
View File

@ -0,0 +1,116 @@
defmodule Growth.Calc.ZScore do
@moduledoc """
Calculate z-score for a given measurement and the fitted values of Box-Cox by age/height:
* **power** `t:l/0`
* **median** `t:m/0`
* **coefficientof variation** `t:s/0`
This calculation is described in the [instructions provided by the World Health Organization](https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/computation.pdf).
## Examples:
iex> measures =
...> [
...> [30, -1.7862, 16.9392, 0.1107],
...> [14, -1.3592, 20.4951, 0.12579],
...> [19, -1.6318, 16.049, 0.10038]
...> ]
iex> Enum.map(measures, &apply(Growth.Calc.ZScore, :compute, &1))
[3.35390255606726, -3.7985108865993493, 1.4698319520484722]
"""
@typedoc """
The **power** value in Box-Cox transformation.
"""
@type l :: number()
@typedoc """
The **median** value in Box-Cox transformation.
"""
@type m :: number()
@typedoc """
The **coefficient of variation** in Box-Cox transformation.
"""
@type s :: number()
@spec compute(number(), l(), m(), s()) :: number()
@doc """
Compute the adjusted z-score for a given measurement (`y`) and the Box-Cox values: power (`l`), median (`m`), and
coefficient of variation (`s`).
## Examples:
iex> Growth.Calc.ZScore.compute(30, -1.7862, 16.9392, 0.1107)
3.35390255606726
iex> Growth.Calc.ZScore.compute(14, -1.3592, 20.4951, 0.12579)
-3.7985108865993493
iex> Growth.Calc.ZScore.compute(19, -1.6318, 16.049, 0.10038)
1.4698319520484722
"""
def compute(y, l, m, s) do
y
|> raw(l, m, s)
|> adjust(y, l, m, s)
end
@spec raw(number(), l(), m(), s()) :: number()
@doc """
Compute the raw z-score for a given measurement (`y`) and the Box-Cox values, power (`l`), median (`m`), and
coefficient of variation (`s`).
## Examples
iex> Growth.Calc.ZScore.raw(30, -1.7862, 16.9392, 0.1107)
3.2353902101095855
iex> Growth.Calc.ZScore.raw(14, -1.3592, 20.4951, 0.12579)
-3.969714730727475
iex> Growth.Calc.ZScore.compute(19, -1.6318, 16.049, 0.10038)
1.4698319520484722
"""
def raw(y, l, m, s) do
(:math.pow(y / m, l) - 1) / (s * l)
end
@spec adjust(number(), number(), l(), m(), s()) :: number()
@doc """
Adjust the raw z-score considering that extreme values, beyond -3 and 3 standard deviation, must be handled differently.
"""
def adjust(zscore, y, l, m, s) when zscore > 3 do
[sd2, sd3, _, _] = cutoffs(l, m, s)
sd_delta = sd3 - sd2
3 + (y - sd3) / sd_delta
end
def adjust(zscore, y, l, m, s) when zscore < -3 do
[_, _, sd2, sd3] = cutoffs(l, m, s)
sd_delta = sd2 - sd3
-3 + (y - sd3) / sd_delta
end
def adjust(zscore, _, _, _, _) do
zscore
end
@spec cutoffs(number(), number(), number()) :: [number()]
@doc """
Calculate the standard deviations cutoffs (2, 3, -2, and -3) for a given set of fitted Box-Cox values: power (`l`),
median (`m`), and coefficient of variation (`s`).
These cutoffs are used to calculate the adjusted z-score.
"""
def cutoffs(l, m, s) do
Enum.map([2, 3, -2, -3], &measure_at_standard_deviation(&1, l, m, s))
end
@spec measure_at_standard_deviation(integer(), number(), number(), number()) :: number()
@doc """
Calculate the measure value at a given standard deviation (`sd`), considering the fitted Box-Cox values: power (`l`),
median (`m`), and coefficient of variation (`s`).
"""
def measure_at_standard_deviation(sd, l, m, s) do
m * :math.pow(1 + l * s * sd, 1 / l)
end
end

225
lib/growth/growth.ex Normal file
View File

@ -0,0 +1,225 @@
defmodule Growth do
@moduledoc """
Follow up child growth from 0 to 19 years, calculating z-scores for the following measurements:
* weight between ages 0 and 10 years
* height between ages 0 and 19 years
* body mass index (bmi) between ages 0 and 19 years
* head circumference between ages 0 and 5 years
* arm circumference between ages 3 months and 5 years
* subscapular skinfold between ages 3 months and 5 years
* triceps skinfold between ages 3 months and 5 years
"""
alias __MODULE__.Calc.Age
alias __MODULE__.Calc.BMI
alias __MODULE__.Score
@type gender :: :male | :female
@type measure :: number() | nil
@type opts :: [
date_of_measurement: Date.t(),
weight: measure(),
height: measure(),
arm_circumference: measure(),
head_circumference: measure(),
subscapular_skinfold: measure(),
triceps_skinfold: measure()
]
@type t :: %__MODULE__{
name: String.t(),
gender: gender(),
date_of_birth: Date.t(),
date_of_measurement: Date.t(),
age_in_days: pos_integer(),
age_in_weeks: pos_integer(),
age_in_months: pos_integer(),
weight: measure(),
height: measure(),
arm_circumference: measure(),
head_circumference: measure(),
subscapular_skinfold: measure(),
triceps_skinfold: measure(),
bmi: measure(),
results: list()
}
@enforce_keys [:name, :gender, :date_of_birth]
defstruct [
:name,
:gender,
:date_of_birth,
:date_of_measurement,
:age_in_days,
:age_in_weeks,
:age_in_months,
:weight,
:height,
:arm_circumference,
:head_circumference,
:subscapular_skinfold,
:triceps_skinfold,
:bmi,
:results
]
@valid_measures [
:date_of_measurement,
:weight,
:height,
:arm_circumference,
:head_circumference,
:subscapular_skinfold,
:triceps_skinfold
]
@doc """
Create a new growth measurement for a children with name, gender, date of birth, and the following optional arguments:
* `:date_of_measurement`: date when the measures were collected, defaults to today.
* `:weight`: weight in kilograms, defaults to `nil`.
* `:height`: height in centimeters, defaults to `nil`.
* `:arm_circumference`: arm circumference in centimeters, defaults to `nil`.
* `:head_circumference`: head circumference in centimeters, defaults to `nil`.
* `:subscapular_skinfold`: subscapular skinfold in milimeters, defaults to `nil`.
* `:triceps_skinfold`: triceps skinfold in milimeters, defaults to `nil`.
## Examples
iex> Growth.new(
...> "child a",
...> :male,
...> ~D[2024-01-01],
...> date_of_measurement: ~D[2024-04-01],
...> weight: 8,
...> height: 65.4,
...> arm_circumference: 15.5,
...> head_circumference: 42.8,
...> subscapular_skinfold: 10.9,
...> triceps_skinfold: 13.5
...> )
%Growth{
name: "child a",
gender: :male,
date_of_birth: ~D[2024-01-01],
date_of_measurement: ~D[2024-04-01],
age_in_days: 91,
age_in_weeks: 13,
age_in_months: 2,
weight: 8,
height: 65.4,
arm_circumference: 15.5,
head_circumference: 42.8,
subscapular_skinfold: 10.9,
triceps_skinfold: 13.5,
bmi: 18.703999850368,
results: [
head_circumference: [
day: {1.945484886994137, 0.9741416765426315},
week: {1.945484886994137, 0.9741416765426315},
month: {3.130859582465616, 0.9991285226182205}
],
arm_circumference: [day: {1.9227031505630465, 0.9727413295221268}],
subscapular_skinfold: [day: {1.9437372448689536, 0.9740364275897885}],
triceps_skinfold: [day: {1.950277062993091, 0.974428447506235}],
weight: [
day: {1.982458622036091, 0.9762860329545557},
week: {1.982458622036091, 0.9762860329545557},
month: {3.0355951313091745, 0.9987996926038037}
],
height: [
day: {1.956263992749136, 0.9747829682259178},
week: {1.956263992749136, 0.9747829682259178},
month: {3.4867331002754054, 0.9997555204670452}
],
bmi: [
day: {1.1977344927294398, 0.8844898016950435},
week: {1.1977344927294398, 0.8844898016950435},
month: {1.5837461190318038, 0.9433742474306444}
]
]
}
"""
@spec new(String.t(), gender(), Date.t(), opts()) :: t()
def new(name, gender, date_of_birth, opts \\ []) do
default_opts()
|> Enum.reduce(
%__MODULE__{
name: name,
gender: gender,
date_of_birth: date_of_birth,
results: []
},
fn {key, default_value}, measurement ->
opts
|> Keyword.get(key, default_value)
|> then(&Map.put(measurement, key, &1))
end
)
|> with_age_in_days()
|> with_age_in_weeks()
|> with_age_in_months()
|> with_bmi()
|> with_results()
end
for precision <- [:day, :week, :month] do
@doc """
Add age with given precision in growth measurement.
"""
@spec unquote(:"with_age_in_#{precision}s")(t()) :: t()
def unquote(:"with_age_in_#{precision}s")(
%__MODULE__{date_of_birth: date_of_birth, date_of_measurement: date_of_measurement} =
growth
)
when not is_nil(date_of_birth) and not is_nil(date_of_measurement) do
age = Age.calculate(unquote(precision), date_of_birth, date_of_measurement)
Map.put(growth, unquote(:"age_in_#{precision}s"), age)
end
def unquote(:"with_age_in_#{precision}s")(%__MODULE__{date_of_birth: date_of_birth} = growth)
when not is_nil(date_of_birth) do
unquote(:"with_age_in_#{precision}s")(%{growth | date_of_measurement: Date.utc_today()})
end
def unquote(:"with_age_in_#{precision}s")(growth) do
growth
end
end
def with_bmi(%__MODULE__{weight: weight, height: height} = growth)
when is_number(weight) and is_number(height) do
%{growth | bmi: BMI.calculate(:metric, weight, height)}
end
def with_bmi(growth) do
growth
end
def with_results(growth) do
Score.Scorer.results(growth, [
Score.BMI,
Score.Height,
Score.Weight,
Score.TricepsSkinfold,
Score.SubscapularSkinfold,
Score.ArmCircumference,
Score.HeadCircumference
])
end
defp default_opts do
Enum.map(@valid_measures, fn
:date_of_measurement = key ->
{key, Date.utc_today()}
key ->
{key, nil}
end)
end
end

View File

@ -0,0 +1,328 @@
defmodule Growth.Indicators.Download do
@moduledoc """
To calculate z-scores for the different growth measurements, the system must:
1. Fetch indicators from World Health Organization
2. Extract data from excel sheets
3. Convert the data into proper format, specially, handle with decimal values
3. Add metadata to make search for the parameters possible
The following indicators to construct z-scores are fetched:
* [height for age 0 to 5 years](https://www.who.int/tools/child-growth-standards/standards/length-height-for-age)
* [height for age 5 to 19 years](https://www.who.int/tools/growth-reference-data-for-5to19-years/indicators/height-for-age)
* [weight for age 0 to 5 years](https://www.who.int/tools/child-growth-standards/standards/weight-for-age)
* [weight for age 5 to 10 years](https://www.who.int/tools/growth-reference-data-for-5to19-years/indicators/weight-for-age-5to10-years)
* [weight for height](https://www.who.int/tools/child-growth-standards/standards/weight-for-length-height)
* [body mass index (bmi) for age 0 to 5 years](https://www.who.int/toolkits/child-growth-standards/standards/body-mass-index-for-age-bmi-for-age)
* [body mass index (bmi) for age 5 to 19 years](https://www.who.int/tools/growth-reference-data-for-5to19-years/indicators/bmi-for-age)
* [head circumference for age 0 to 5 years](https://www.who.int/tools/child-growth-standards/standards/head-circumference-for-age)
* [arm circumference for age 3 months to 5 years](https://www.who.int/tools/child-growth-standards/standards/arm-circumference-for-age)
* [subscapular skinfold for age 3 months to 5 years](https://www.who.int/tools/child-growth-standards/standards/subscapular-skinfold-for-age)
* [triceps skinfold for age 3 months to 5 years](https://www.who.int/tools/child-growth-standards/standards/triceps-skinfold-for-age)
"""
NimbleCSV.define(IndicatorParser, separator: ",", escape: "\"")
@urls %{
height_for_age: %{
female: %{
age_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-13-weeks_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-2-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_2-to-5-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/height-for-age-(5-19-years)/hfa-girls-z-who-2007-exp.xlsx
],
expanded_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/expandable-tables/lhfa-girls-zscore-expanded-tables.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/height-for-age-(5-19-years)/hfa-girls-z-who-2007-exp.xlsx
]
},
male: %{
age_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_0-to-13-weeks_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_0-to-2-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_2-to-5-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/height-for-age-(5-19-years)/hfa-boys-z-who-2007-exp.xlsx
],
expanded_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/expandable-tables/lhfa-boys-zscore-expanded-tables.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/height-for-age-(5-19-years)/hfa-boys-z-who-2007-exp.xlsx
]
}
},
weight_for_age: %{
female: %{
age_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/wfa_girls_0-to-13-weeks_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/wfa_girls_0-to-5-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/weight-for-age-(5-10-years)/hfa-girls-z-who-2007-exp_7ea58763-36a2-436d-bef0-7fcfbadd2820.xlsx
],
expanded_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/expanded-tables/wfa-girls-zscore-expanded-tables.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/weight-for-age-(5-10-years)/hfa-girls-z-who-2007-exp_7ea58763-36a2-436d-bef0-7fcfbadd2820.xlsx
]
},
male: %{
age_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/wfa_boys_0-to-13-weeks_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/wfa_boys_0-to-5-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/weight-for-age-(5-10-years)/hfa-boys-z-who-2007-exp_0ff9c43c-8cc0-4c23-9fc6-81290675e08b.xlsx
],
expanded_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-age/expanded-tables/wfa-boys-zscore-expanded-tables.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/weight-for-age-(5-10-years)/hfa-boys-z-who-2007-exp_0ff9c43c-8cc0-4c23-9fc6-81290675e08b.xlsx
]
}
},
weight_for_height: %{
female: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/wfl_girls_0-to-2-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/wfh_girls_2-to-5-years_zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/expanded-tables/wfl-girls-zscore-expanded-table.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/expanded-tables/wfh-girls-zscore-expanded-tables.xlsx
)
},
male: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/wfl_boys_0-to-2-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/wfh_boys_2-to-5-years_zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/expanded-tables/wfl-boys-zscore-expanded-table.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/weight-for-length-height/expanded-tables/wfh-boys-zscore-expanded-tables.xlsx
)
}
},
bmi_for_age: %{
female: %{
age_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_girls_0-to-13-weeks_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_girls_0-to-2-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_girls_2-to-5-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/bmi-for-age-(5-19-years)/bmi-girls-z-who-2007-exp.xlsx
],
expanded_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/expanded-tables/bfa-girls-zscore-expanded-tables.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/bmi-for-age-(5-19-years)/bmi-girls-z-who-2007-exp.xlsx
]
},
male: %{
age_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_boys_0-to-13-weeks_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_boys_0-to-2-years_zcores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/bmi_boys_2-to-5-years_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/bmi-for-age-(5-19-years)/bmi-boys-z-who-2007-exp.xlsx
],
expanded_tables: ~w[
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/body-mass-index-for-age/expanded-tables/bfa-boys-zscore-expanded-tables.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/growth-reference-5-19-years/bmi-for-age-(5-19-years)/bmi-boys-z-who-2007-exp.xlsx
]
}
},
head_circumference_for_age: %{
female: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/hcfa-girls-0-13-zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/hcfa-girls-0-5-zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/expanded-tables/hcfa-girls-zscore-expanded-tables.xlsx
)
},
male: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/hcfa-boys-0-13-zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/hcfa-boys-0-5-zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/head-circumference-for-age/expanded-tables/hcfa-boys-zscore-expanded-tables.xlsx
)
}
},
arm_circumference_for_age: %{
female: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/arm-circumference-for-age/acfa-girls-3-5-zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/arm-circumference-for-age/expanded-tables/acfa-girls-zscore-expanded-tables.xlsx
)
},
male: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/arm-circumference-for-age/acfa-boys-3-5-zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/arm-circumference-for-age/expanded-tables/acfa-boys-zscore-expanded-tables.xlsx
)
}
},
subscapular_skinfold_for_age: %{
female: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/subscapular-skinfold-for-age/ssfa-girls-3-5-zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/subscapular-skinfold-for-age/expanded-tables/ssfa-girls-zscore-expanded-table.xlsx
)
},
male: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/subscapular-skinfold-for-age/ssfa-boys-3-5-zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/subscapular-skinfold-for-age/expanded-tables/ssfa-boys-zscore-expanded-table.xlsx
)
}
},
triceps_skinfold_for_age: %{
female: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/triceps-skinfold-for-age/tsfa-girls-3-5-zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/triceps-skinfold-for-age/expanded-tables/tsfa-girls-zscore-expanded-tables.xlsx
)
},
male: %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/triceps-skinfold-for-age/tsfa-boys-3-5-zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/triceps-skinfold-for-age/expanded-tables/tsfa-boys-zscore-expanded-tables.xlsx
)
}
}
}
def process_all do
@urls
|> Enum.map(&Task.async(__MODULE__, :process_measure, [&1]))
|> Task.await_many()
end
def process_measure({measure, urls}) do
urls
|> process_genders()
|> as_csv()
|> save(measure)
end
def process_genders(%{
female: %{age_tables: female_urls, expanded_tables: e_female_urls},
male: %{age_tables: male_urls, expanded_tables: e_male_urls}
}) do
[
{:female, :age, female_urls},
{:male, :age, male_urls},
{:female, :expanded, e_female_urls},
{:male, :expanded, e_male_urls}
]
|> Enum.map(&Task.async(__MODULE__, :process_gender, [&1]))
|> Task.await_many()
|> merge()
end
def process_gender({gender, category, urls}) do
urls
|> Enum.map(&Task.async(__MODULE__, :process, [gender, category, &1]))
|> Task.await_many()
|> merge()
end
def process(gender, category, url) do
url
|> fetch!()
|> extract!(url)
|> convert(gender, category, url)
end
def fetch!(url) do
req =
[url: url]
|> Keyword.merge(
:wabanex
|> Application.get_env(__MODULE__, [])
|> Keyword.get(:who_req_options, [])
)
|> Req.new()
case Req.get(req) do
{:ok, %{status: 200, body: body}} ->
body
_ ->
raise("fetch failed for url #{url}")
end
end
def extract!(content, url) do
with {:ok, package} <- XlsxReader.open(content, source: :binary),
[sheet_name | _] <- XlsxReader.sheet_names(package),
{:ok, data} <- XlsxReader.sheet(package, sheet_name) do
data
else
_ -> raise("failed to extract excel for #{url}")
end
end
@common_header ~w(source category gender age_unit age l m s sd3neg sd2neg sd1neg sd0 sd1 sd2 sd3)
# FIX: (jpd) weight for lenght/height does not have an age in the header row
def convert([header | rows], gender, category, url) do
age_unit = header |> hd() |> String.downcase()
fixed_header = header |> tl() |> Enum.map(&String.downcase/1) |> Enum.map(&String.trim/1)
parsed_header = ["source" | ["category" | ["gender" | ["age_unit" | ["age" | fixed_header]]]]]
# NOTE: (jpd): parsing the rows consist in:
# 1. convert row values to decimal
# 2. prepend the values url source, gender, and age unit
# 3. convert row to keyword list using the parsed header
# 4. convert from keyword list to map
# 5. fetch common values based on common headers
# 6. sort row values based on common headers
parsed_rows =
rows
|> Stream.map(fn row -> Enum.map(row, &Decimal.new/1) end)
|> Stream.map(&[url | [category | [gender | [age_unit | &1]]]])
|> Stream.map(&Enum.zip(parsed_header, &1))
|> Stream.map(&Map.new/1)
|> Stream.map(&Map.take(&1, @common_header))
|> Enum.map(fn row ->
Enum.map(@common_header, fn key -> Map.get(row, key) end)
end)
[@common_header | parsed_rows]
end
def merge(datum) do
datum
|> Stream.with_index()
|> Stream.map(fn
{data, 0} ->
data
{[_ | data], _} ->
data
end)
|> Enum.reduce([], fn data, accum ->
Enum.concat(accum, data)
end)
end
def as_csv(data) do
IndicatorParser.dump_to_iodata(data)
end
def save(data, measurement) do
:wabanex
|> Application.app_dir(["priv", "growth", "indicators", "#{measurement}.csv"])
|> File.write(data)
end
end

View File

@ -0,0 +1,190 @@
defmodule Growth.Indicators.Load do
@moduledoc """
Load local indicators csv files into ets.
For each measurement an ets table is created, so the system has the following tables:
* `Growth.Score.ArmCircumference`
* `Growth.Score.BMI`
* `Growth.Score.HeadCircumference`
* `Growth.Score.Height`
* `Growth.Score.SubscapularSkinfold`
* `Growth.Score.TricepsSkinfold`
* `Growth.Score.Weight`
* `Growth.Score.WeightForHeight`
The rows in the csv files are converted to two tuple, representing the a key and value, with the following format:
* **key**: `{gender, unit, t-value}`
* **value**: map with the keys:
* l
* m
* s
* sd3neg
* sd2neg
* sd1neg
* sd0
* sd1
* sd2
* sd3
* source
* category
"""
use Task
require Logger
alias Growth.Score
@measure_to_score [
arm_circumference_for_age: Score.ArmCircumference,
bmi_for_age: Score.BMI,
head_circumference_for_age: Score.HeadCircumference,
height_for_age: Score.Height,
subscapular_skinfold_for_age: Score.SubscapularSkinfold,
triceps_skinfold_for_age: Score.TricepsSkinfold,
weight_for_age: Score.Weight,
weight_for_height: Score.WeightForHeight
]
@doc false
def start_link(_) do
_ =
Enum.map(@measure_to_score, fn {_, measure} ->
create_ets(measure)
end)
Task.start_link(__MODULE__, :all, [])
end
@spec all :: [[boolean()]]
@doc """
Load indicators csv files into their own ets tables.
"""
def all do
Logger.debug("load growth indicators")
:wabanex
|> Application.app_dir(["priv", "growth", "indicators", "*.csv"])
|> Path.wildcard()
|> Enum.map(&create_ets_from_filename/1)
|> Enum.map(&Task.async(__MODULE__, :load_measure, [&1]))
|> Task.await_many()
end
@spec create_ets(module()) :: module()
@doc """
Create a public ets table for the growth module, using it as the table name.
Returns the given module.
"""
def create_ets(measure) do
try do
:ets.new(measure, [:set, :public, :named_table])
rescue
_ ->
nil
end
measure
end
@spec create_ets_from_filename(String.t()) :: {atom(), String.t()}
@doc """
Create ets table based on filename and return a tuple with the ets table name and filename.
"""
def create_ets_from_filename(filename) do
measure =
filename
|> Path.basename()
|> Path.rootname()
|> String.to_atom()
|> then(&Keyword.get(@measure_to_score, &1, &1))
|> create_ets()
{measure, filename}
end
@spec load_measure({atom(), String.t()}) :: [boolean()]
@doc """
Read, convert, and load a measure/filename into the proper ets table.
"""
def load_measure({measure, filename}) do
Logger.debug("load data from #{filename} into #{measure}")
filename
|> read()
|> convert()
|> load(measure)
end
@spec read(String.t()) :: Enumerable.t()
@doc false
def read(filename) do
filename
|> File.stream!()
|> IndicatorParser.parse_stream()
end
@spec convert(Enumerable.t()) :: Enumerable.t()
@doc false
def convert(data) do
Stream.map(data, fn [
source,
category,
gender,
unit,
t,
l,
m,
s,
sd3neg,
sd2neg,
sd1neg,
sd0,
sd1,
sd2,
sd3
] ->
converted_t =
if unit in ~w(day week month) do
as_integer(t)
else
as_float(t)
end
key = {String.to_atom(gender), String.to_atom(unit), converted_t}
value = %{
l: as_float(l),
m: as_float(m),
s: as_float(s),
sd3neg: as_float(sd3neg),
sd2neg: as_float(sd2neg),
sd1neg: as_float(sd1neg),
sd0: as_float(sd0),
sd1: as_float(sd1),
sd2: as_float(sd2),
sd3: as_float(sd3),
source: source,
category: category
}
{key, value}
end)
end
@spec load(Enumerable.t(), atom()) :: [boolean()]
@doc false
def load(data, ets_table) do
Enum.map(data, fn {key, value} ->
:ets.insert_new(ets_table, {key, value})
end)
end
defp as_integer(value), do: value |> Integer.parse() |> elem(0)
defp as_float(value), do: value |> Float.parse() |> elem(0)
end

View File

@ -0,0 +1,18 @@
defmodule Growth.Score.ArmCircumference do
@moduledoc """
Calculate z-score for arm circumference for age.
"""
@behaviour Growth.Score.Scorer
alias Growth.Score.Scorer
@impl Scorer
@spec measure_name() :: atom()
@doc """
Name of the measurement used in arm circumference indicator.
"""
def measure_name do
:arm_circumference
end
end

18
lib/growth/score/bmi.ex Normal file
View File

@ -0,0 +1,18 @@
defmodule Growth.Score.BMI do
@moduledoc """
Calculate z-score for body mass index for age.
"""
@behaviour Growth.Score.Scorer
alias Growth.Score.Scorer
@impl Scorer
@spec measure_name() :: atom()
@doc """
Name of the measurement used in BMI indicator.
"""
def measure_name do
:bmi
end
end

View File

@ -0,0 +1,18 @@
defmodule Growth.Score.HeadCircumference do
@moduledoc """
Calculate z-score for head circumference for age.
"""
@behaviour Growth.Score.Scorer
alias Growth.Score.Scorer
@impl Scorer
@spec measure_name() :: atom()
@doc """
Name of the measurement used in head circumference indicator.
"""
def measure_name do
:head_circumference
end
end

View File

@ -0,0 +1,18 @@
defmodule Growth.Score.Height do
@moduledoc """
Calculate z-score for height for age.
"""
@behaviour Growth.Score.Scorer
alias Growth.Score.Scorer
@impl Scorer
@spec measure_name() :: atom()
@doc """
Name of the measurement used in height indicator.
"""
def measure_name do
:height
end
end

View File

@ -0,0 +1,96 @@
defmodule Growth.Score.Scorer do
@moduledoc """
Behaviour defining common interface to calculate z-score and percentile for a given measurement.
"""
alias Growth.Calc.Percentile
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 percentile 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: Keyword.put(growth.results, indicator.measure_name(), result)}
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
@doc """
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
growth
|> Map.get(indicator.measure_name())
|> z_score(l, m, s)
|> then(fn score -> {score, percentile(score)} end)
end
@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
def z_score(value, l, m, s) do
ZScore.compute(value, l, m, s)
end
@doc """
Check `Growth.Calc.Percentile.compute/1`.
"""
@spec percentile(Growth.measure()) :: Growth.measure()
def percentile(nil) do
nil
end
def percentile(score) do
Percentile.compute(score)
end
end

View File

@ -0,0 +1,18 @@
defmodule Growth.Score.SubscapularSkinfold do
@moduledoc """
Calculate z-score for subscapular skinfold for age.
"""
@behaviour Growth.Score.Scorer
alias Growth.Score.Scorer
@impl Scorer
@spec measure_name() :: atom()
@doc """
Name of the measurement used in subscapular skinfold indicator.
"""
def measure_name do
:subscapular_skinfold
end
end

View File

@ -0,0 +1,18 @@
defmodule Growth.Score.TricepsSkinfold do
@moduledoc """
Calculate z-score for triceps skinfold for age.
"""
@behaviour Growth.Score.Scorer
alias Growth.Score.Scorer
@impl Scorer
@spec measure_name() :: atom()
@doc """
Name of the measurement used in triceps skinfold indicator.
"""
def measure_name do
:triceps_skinfold
end
end

View File

@ -0,0 +1,18 @@
defmodule Growth.Score.Weight do
@moduledoc """
Calculate z-score for weight for age.
"""
@behaviour Growth.Score.Scorer
alias Growth.Score.Scorer
@impl Scorer
@spec measure_name() :: atom()
@doc """
Name of the measurement used in weight indicator.
"""
def measure_name do
:weight
end
end

View File

@ -0,0 +1,5 @@
defmodule Growth.Score.WeightForHeight do
@moduledoc """
Calculate z-score for weight for height.
"""
end

View File

@ -9,6 +9,7 @@ defmodule Wabanex.Application do
Wabanex.Repo,
WabanexWeb.Telemetry,
{Phoenix.PubSub, name: Wabanex.PubSub},
{Growth.Indicators.Load, []},
{DNSCluster,
query: Application.get_env(:wabanex, :dns_cluster_query) || :ignore,
log: :info,

10
mix.exs
View File

@ -35,25 +35,31 @@ defmodule Wabanex.MixProject do
ref: "5de21da279938d1a10642f94d5e9c9fbdc846149"},
{:credo, "~> 1.7.0", only: [:dev, :test], runtime: false},
{:crudry, "~> 2.4.0"},
{:decimal, "~> 2.3.0"},
{:dialyxir, "~> 1.4.0", only: [:dev, :test], runtime: false},
{:dns_cluster, "~> 0.2.0"},
{:ecto_sql, "~> 3.12.0"},
{:elixlsx, "~> 0.6.0", only: :test},
{:ex_doc, "~> 0.37.0", only: :dev, runtime: false},
{:gettext, "~> 0.26.0"},
{:jason, "~> 1.4.0"},
{:junit_formatter, "~> 3.4.0", only: [:test]},
{:lcov_ex, "~> 0.3.0", only: [:dev, :test], runtime: false},
{:mix_audit, "~> 2.1.0", only: [:dev, :test], runtime: false},
{:nimble_csv, "~> 1.2.0"},
{:pg_ranges, "~> 1.1.0"},
{:phoenix, "~> 1.7.0"},
{:phoenix_ecto, "~> 4.6.0"},
{:phoenix_view, "~> 2.0.0"},
{:phoenix_live_dashboard, "~> 0.8.0"},
{:phoenix_view, "~> 2.0.0"},
{:plug_cowboy, "~> 2.7.0"},
{:postgrex, "~> 0.20.0"},
{:prom_ex, "~> 1.11.0"},
{:req, "~> 0.5.0"},
{:sobelow, "~> 0.13", only: [:dev, :test], runtime: false},
{:telemetry_metrics, "~> 1.1.0"},
{:telemetry_poller, "~> 1.1.0"}
{:telemetry_poller, "~> 1.1.0"},
{:xlsx_reader, "~> 0.8.0"}
]
end

View File

@ -12,9 +12,12 @@
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
"elixlsx": {:hex, :elixlsx, "0.6.0", "858c2c821ab52f4ca0988adce188d19f3b239a4fff8b36b26cd81ec8af9b2ab3", [:mix], [], "hexpm", "c4766f47afea075a85950a5c6fe981e98b8b8a30cc076382aaacf2bb8dbcd25d"},
"erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
"ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
@ -23,9 +26,13 @@
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"},
"lcov_ex": {:hex, :lcov_ex, "0.3.4", "f48aed787db0d1cff1409db391f442cdbc0af0f860dbc326430030027e6ded36", [:mix], [], "hexpm", "c3987b6aeadd78d4b7933fa9cc4de3bd69d34f0fae39bad908f79a7ceea957e5"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"},
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
@ -40,12 +47,14 @@
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"},
"prom_ex": {:hex, :prom_ex, "1.11.0", "1f6d67f2dead92224cb4f59beb3e4d319257c5728d9638b4a5e8ceb51a4f9c7e", [:mix], [{:absinthe, ">= 1.7.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.11.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "76b074bc3730f0802978a7eb5c7091a65473eaaf07e99ec9e933138dcc327805"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"},
"saxy": {:hex, :saxy, "1.6.0", "02cb4e9bd045f25ac0c70fae8164754878327ee393c338a090288210b02317ee", [:mix], [], "hexpm", "ef42eb4ac983ca77d650fbdb68368b26570f6cc5895f0faa04d34a6f384abad3"},
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
@ -54,6 +63,7 @@
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"xlsx_reader": {:hex, :xlsx_reader, "0.8.8", "fbb29049548ff687f03a2873f2eb0d9057e47eb69cafb07f44988f030fb620b7", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:saxy, "~> 1.5", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "642d979a3a156b150bb76a89998a130483e1c399fa32e8d3a66abc1d9799dbd7"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
defmodule Growth.Calc.AgeTest do
@moduledoc false
use ExUnit.Case, async: true
alias Growth.Calc.Age
describe "calculate/2" do
test "consider today as date of measurement" do
date_of_birth = Date.add(Date.utc_today(), -7)
assert 7 == Age.calculate(:day, date_of_birth)
end
end
describe "calculate/3" do
test "calculate age in days" do
date_of_measurement = ~D[2024-06-09]
days_of_birth = [{-1, 1}, {-7, 7}, {-30, 30}]
Enum.map(days_of_birth, fn {days_ago, expected_age} ->
date_of_birth = Date.add(date_of_measurement, days_ago)
assert expected_age == Age.calculate(:day, date_of_birth, date_of_measurement)
end)
end
test "calculate age in completed weeks" do
date_of_measurement = ~D[2024-06-09]
days_of_birth = [{-1, 0}, {-7, 1}, {-16, 2}, {-30, 4}]
Enum.map(days_of_birth, fn {days_ago, expected_age} ->
date_of_birth = Date.add(date_of_measurement, days_ago)
assert expected_age == Age.calculate(:week, date_of_birth, date_of_measurement)
end)
end
test "calculate age in completed months" do
date_of_measurement = ~D[2024-06-09]
days_of_birth = [{-1, 0}, {-16, 0}, {-31, 1}, {-70, 2}, {-93, 3}]
Enum.map(days_of_birth, fn {days_ago, expected_age} ->
date_of_birth = Date.add(date_of_measurement, days_ago)
assert expected_age == Age.calculate(:month, date_of_birth, date_of_measurement)
end)
end
end
end

View File

@ -0,0 +1,23 @@
defmodule Growth.Calc.BMITest do
@moduledoc false
use ExUnit.Case, async: true
alias Growth.Calc.BMI
describe "calculate/3" do
test "in metric system" do
weight_kg = 78.5
height_cm = 168.2
assert 27.74710475751505 == BMI.calculate(:metric, weight_kg, height_cm)
end
test "in english system" do
weight_lb = 173.1
height_in = 66.2
assert 27.770202207557876 == BMI.calculate(:english, weight_lb, height_in)
end
end
end

View File

@ -0,0 +1,24 @@
defmodule Growth.Calc.CentileTest do
@moduledoc false
use ExUnit.Case, async: true
import Growth.Data, only: [sample: 0]
doctest Growth.Calc.Centile
alias Growth.Calc.Centile
describe "compute/4" do
for %{key: key} = params <- sample() do
Review
The table-driven test is based on: https://blog.jpalardy.com/posts/a-table-driven-test-template-for-elixir/
@tag params: params
test "returns the measure given a z-score and box-cox fitted values #{key}", %{
params: params
} do
%{zscore: zscore, measure: measure, l: l, m: m, s: s} = params
assert_in_delta Centile.compute(zscore, l, m, s), measure, 0.05
end
end
end
end

View File

@ -0,0 +1,26 @@
defmodule Growth.Calc.PercentileTest do
@moduledoc false
use ExUnit.Case, async: true
doctest Growth.Calc.Percentile
alias Growth.Calc.Percentile
describe "compute/1" do
for {zscore, _} = params <- [
{-3, 0.0013498125},
{-2, 0.0227502617},
{-1, 0.1586553192},
{0, 0.5000000000},
{1, 0.8413446808},
{2, 0.9772497383},
{3, 0.9986501875}
] do
@tag params: params
test "returns the percentile for z-score #{zscore}", %{params: {zscore, percentile}} do
assert_in_delta Percentile.compute(zscore), percentile, 0.0000005
end
end
end
end

View File

@ -0,0 +1,24 @@
defmodule Growth.Calc.ZScoreTest do
@moduledoc false
use ExUnit.Case, async: true
import Growth.Data, only: [sample: 0]
doctest Growth.Calc.ZScore
alias Growth.Calc.ZScore
describe "compute/4" do
for %{key: key} = params <- sample() do
@tag params: params
test "returns a z-score given a measurement and box-cox fitted values #{key}", %{
params: params
} do
%{zscore: zscore, measure: measure, l: l, m: m, s: s} = params
assert_in_delta ZScore.compute(measure, l, m, s), zscore, 0.12
end
end
end
end

131
test/growth/growth_test.exs Normal file
View File

@ -0,0 +1,131 @@
defmodule GrowthTest do
@moduledoc false
use ExUnit.Case, async: true
doctest Growth
@child %{
name: "Jane Doe",
gender: :female,
date_of_measurement: Date.utc_today(),
age_in_days: 91,
weight: 5.84,
height: 59.78,
head_circumference: 39.51,
arm_circumference: 13.02,
subscapular_skinfold: 7.79,
triceps_skinfold: 9.75
}
@child_date_of_birth Date.shift(@child.date_of_measurement, day: @child.age_in_days * -1)
describe "new/4" do
test "create a new growth measurement" do
growth =
Growth.new(@child.name, @child.gender, @child_date_of_birth,
date_of_measurement: @child.date_of_measurement,
weight: @child.weight,
height: @child.height,
head_circumference: @child.head_circumference,
arm_circumference: @child.arm_circumference,
subscapular_skinfold: @child.subscapular_skinfold,
triceps_skinfold: @child.triceps_skinfold
)
assert @child.name == growth.name
assert @child.gender == growth.gender
assert @child_date_of_birth == growth.date_of_birth
assert @child.date_of_measurement == growth.date_of_measurement
assert @child.age_in_days == growth.age_in_days
assert 13 == growth.age_in_weeks
assert 2 == growth.age_in_months
assert @child.weight == growth.weight
assert @child.height == growth.height
assert @child.head_circumference == growth.head_circumference
assert @child.arm_circumference == growth.arm_circumference
assert @child.subscapular_skinfold == growth.subscapular_skinfold
assert @child.triceps_skinfold == growth.triceps_skinfold
end
end
describe "with_bmi/1" do
test "calculate bmi from measurement" do
growth =
%Growth{
name: @child.name,
gender: @child.gender,
date_of_birth: @child_date_of_birth,
date_of_measurement: @child.date_of_measurement,
weight: @child.weight,
height: @child.height,
head_circumference: @child.head_circumference,
arm_circumference: @child.arm_circumference,
subscapular_skinfold: @child.subscapular_skinfold,
triceps_skinfold: @child.triceps_skinfold
}
|> Growth.with_age_in_days()
|> Growth.with_age_in_weeks()
|> Growth.with_age_in_months()
|> Growth.with_bmi()
assert 16.341842694989243 == growth.bmi
end
end
describe "with_results/1" do
test "calculate z-score and percentiles from measurement" do
growth =
%Growth{
name: @child.name,
gender: @child.gender,
date_of_birth: @child_date_of_birth,
date_of_measurement: @child.date_of_measurement,
weight: @child.weight,
height: @child.height,
head_circumference: @child.head_circumference,
arm_circumference: @child.arm_circumference,
subscapular_skinfold: @child.subscapular_skinfold,
triceps_skinfold: @child.triceps_skinfold,
results: []
}
|> Growth.with_age_in_days()
|> Growth.with_age_in_weeks()
|> Growth.with_age_in_months()
|> Growth.with_bmi()
|> Growth.with_results()
assert [
day: {9.496948997971584e-4, 0.5003788733920584},
week: {9.496948997971584e-4, 0.5003788733920584},
month: {1.0060928051683196, 0.8428145353253619}
] == Keyword.get(growth.results, :weight)
assert [
day: {0.0012831717968983271, 0.5005119113423219},
week: {0.0012831717968983271, 0.5005119113423219},
month: {1.3322618635180914, 0.9086129228760054}
] == Keyword.get(growth.results, :height)
assert [
day: {-0.007440424136462911, 0.4970317276150869},
week: {-0.007440424136462911, 0.4970317276150869},
month: {0.3827194919327071, 0.6490361198066796}
] == Keyword.get(growth.results, :bmi)
assert [
day: {-0.008864109494641385, 0.4964637782528006},
week: {-0.008864109494641385, 0.4964637782528006},
month: {1.0380198575748647, 0.8503695948597997}
] == Keyword.get(growth.results, :head_circumference)
assert [day: {-0.004182676756535293, 0.49833135826198854}] ==
Keyword.get(growth.results, :arm_circumference)
assert [day: {0.001811404894950268, 0.5007226456043324}] ==
Keyword.get(growth.results, :subscapular_skinfold)
assert [day: {-0.0019309186728254878, 0.49922967538007906}] ==
Keyword.get(growth.results, :triceps_skinfold)
end
end
end

View File

@ -0,0 +1,297 @@
defmodule Growth.Indicators.DownloadTest do
@moduledoc false
use ExUnit.Case, async: true
alias Elixlsx.Sheet
alias Elixlsx.Workbook
alias Growth.Indicators.Download
setup do
mock_who_request()
end
describe "process/3" do
test "fetch excel from url and convert it to a list of lists" do
gender = :female
category = :age_tables
url =
"https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-13-weeks_zscores.xlsx"
content = Download.process(gender, category, url)
expected_content =
[
~w(source category gender age_unit age l m s sd3neg sd2neg sd1neg sd0 sd1 sd2 sd3),
[
url,
category,
gender,
"week",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.1477"),
Decimal.new("0.0379"),
Decimal.new("43.6"),
Decimal.new("45.4"),
Decimal.new("47.3"),
Decimal.new("49.1"),
Decimal.new("51"),
Decimal.new("52.9"),
Decimal.new("54.7")
]
]
assert expected_content == content
end
end
describe "process_gender/1" do
test "merge multiple contents into a single one" do
category = :age_tables
gender = :female
urls =
[
"https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-13-weeks_zscores.xlsx",
"https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-2-years_zscores.xlsx"
]
content = Download.process_gender({gender, category, urls})
expected_content =
[
~w(source category gender age_unit age l m s sd3neg sd2neg sd1neg sd0 sd1 sd2 sd3),
[
List.first(urls),
category,
gender,
"week",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.1477"),
Decimal.new("0.0379"),
Decimal.new("43.6"),
Decimal.new("45.4"),
Decimal.new("47.3"),
Decimal.new("49.1"),
Decimal.new("51"),
Decimal.new("52.9"),
Decimal.new("54.7")
],
[
List.last(urls),
category,
gender,
"month",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.1477"),
Decimal.new("0.0379"),
Decimal.new("43.6"),
Decimal.new("45.4"),
Decimal.new("47.3"),
Decimal.new("49.1"),
Decimal.new("51"),
Decimal.new("52.9"),
Decimal.new("54.7")
]
]
assert expected_content == content
end
end
describe "process_genders/1" do
test "merge multiple genders and contents into a single one" do
female_urls = %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-13-weeks_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_girls_0-to-2-years_zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/expandable-tables/lhfa-girls-zscore-expanded-tables.xlsx
)
}
male_urls = %{
age_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_0-to-13-weeks_zscores.xlsx
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/lhfa_boys_0-to-2-years_zscores.xlsx
),
expanded_tables: ~w(
https://cdn.who.int/media/docs/default-source/child-growth/child-growth-standards/indicators/length-height-for-age/expandable-tables/lhfa-boys-zscore-expanded-tables.xlsx
)
}
content = Download.process_genders(%{female: female_urls, male: male_urls})
expected_content =
[
~w(source category gender age_unit age l m s sd3neg sd2neg sd1neg sd0 sd1 sd2 sd3),
[
female_urls |> Map.get(:age_tables) |> List.first(),
:age,
:female,
"week",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.1477"),
Decimal.new("0.0379"),
Decimal.new("43.6"),
Decimal.new("45.4"),
Decimal.new("47.3"),
Decimal.new("49.1"),
Decimal.new("51"),
Decimal.new("52.9"),
Decimal.new("54.7")
],
[
female_urls |> Map.get(:age_tables) |> List.last(),
:age,
:female,
"month",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.1477"),
Decimal.new("0.0379"),
Decimal.new("43.6"),
Decimal.new("45.4"),
Decimal.new("47.3"),
Decimal.new("49.1"),
Decimal.new("51"),
Decimal.new("52.9"),
Decimal.new("54.7")
],
[
male_urls |> Map.get(:age_tables) |> List.first(),
:age,
:male,
"week",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.8842"),
Decimal.new("0.03795"),
Decimal.new("44.2"),
Decimal.new("46.1"),
Decimal.new("48"),
Decimal.new("49.9"),
Decimal.new("51.8"),
Decimal.new("53.7"),
Decimal.new("55.6")
],
[
male_urls |> Map.get(:age_tables) |> List.last(),
:age,
:male,
"month",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.8842"),
Decimal.new("0.03795"),
Decimal.new("44.2"),
Decimal.new("46.1"),
Decimal.new("48"),
Decimal.new("49.9"),
Decimal.new("51.8"),
Decimal.new("53.7"),
Decimal.new("55.6")
],
[
female_urls |> Map.get(:expanded_tables) |> List.first(),
:expanded,
:female,
"day",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.1477"),
Decimal.new("0.0379"),
Decimal.new("43.56"),
Decimal.new("45.422"),
Decimal.new("47.285"),
Decimal.new("49.148"),
Decimal.new("51.01"),
Decimal.new("52.873"),
Decimal.new("54.736")
],
[
male_urls |> Map.get(:expanded_tables) |> List.first(),
:expanded,
:male,
"day",
Decimal.new("0"),
Decimal.new("1"),
Decimal.new("49.8842"),
Decimal.new("0.03795"),
Decimal.new("44.205"),
Decimal.new("46.098"),
Decimal.new("47.991"),
Decimal.new("49.884"),
Decimal.new("51.777"),
Decimal.new("53.67"),
Decimal.new("55.564")
]
]
assert expected_content == content
end
end
defp mock_who_request do
Req.Test.stub(Growth.Indicators.Download.WHO, fn %Plug.Conn{path_info: path_info} = conn ->
filename = List.last(path_info)
rows =
case filename do
"lhfa_girls_0-to-13-weeks_zscores.xlsx" ->
[
~w(Week L M S SD SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3),
~w(0 1 49.1477 0.0379 1.8627 43.6 45.4 47.3 49.1 51 52.9 54.7)
]
"lhfa_girls_0-to-2-years_zscores.xlsx" ->
[
~w(Month L M S SD SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3),
~w(0 1 49.1477 0.0379 1.8627 43.6 45.4 47.3 49.1 51 52.9 54.7)
]
"lhfa-girls-zscore-expanded-tables.xlsx" ->
[
~w(Day L M S SD4neg SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3 SD4),
~w(0 1 49.1477 0.0379 41.697 43.56 45.422 47.285 49.148 51.01 52.873 54.736 56.598)
]
"lhfa_boys_0-to-13-weeks_zscores.xlsx" ->
[
~w(Week L M S SD SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3),
~w(0 1 49.8842 0.03795 1.8931 44.2 46.1 48 49.9 51.8 53.7 55.6)
]
"lhfa_boys_0-to-2-years_zscores.xlsx" ->
[
~w(Month L M S SD SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3),
~w(0 1 49.8842 0.03795 1.8931 44.2 46.1 48 49.9 51.8 53.7 55.6)
]
"lhfa-boys-zscore-expanded-tables.xlsx" ->
[
~w(Day L M S SD4neg SD3neg SD2neg SD1neg SD0 SD1 SD2 SD3 SD4),
~w(0 1 49.8842 0.03795 42.312 44.205 46.098 47.991 49.884 51.777 53.67 55.564 57.457)
]
end
{:ok, {_charlist_content, content}} =
%Sheet{name: "zscore", rows: rows}
|> then(&%Workbook{sheets: [&1]})
|> Elixlsx.write_to_memory(filename)
conn
|> Plug.Conn.put_resp_content_type(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
|> Plug.Conn.send_resp(200, content)
end)
end
end

115
test/support/growth_data.ex Normal file
View File

@ -0,0 +1,115 @@
defmodule Growth.Data do
@moduledoc """
Sample data for growth tests based on WHO csv files containing:
* source measure
* gender
* age_unit
* box-cox fitted params:
* l
* m
* s
* expeceted measure values at given z-scores:
* -3
* -2
* -1
* 0
* 1
* 2
* 3
"""
NimbleCSV.define(Growth.DataCase, separator: ",", escape: "\"")
# NOTE: (jpd) this combined table will be used on zscore and centile test to cross-validate the results using
# different genders, ages units, and measures.
@sample_combined_csv """
source,gender,age_unit,age,l,m,s,sd3neg,sd2neg,sd1neg,sd0,sd1,sd2,sd3
arm-circumference-for-age,female,month,3,-0.17330000000000001,13.0284,0.082629999999999995,10.199999999999999,11.1,12,13,14.2,15.4,16.8
arm-circumference-for-age,female,day,91,-0.17330000000000001,13.0245,0.082619999999999999,10.218,11.066000000000001,11.999000000000001,13.023999999999999,14.154999999999999,15.401999999999999,16.78
arm-circumference-for-age,male,month,3,0.39279999999999998,13.4817,0.074749999999999997,10.7,11.6,12.5,13.5,14.5,15.6,16.7
arm-circumference-for-age,male,day,91,0.39329999999999998,13.4779,0.074740000000000001,10.657999999999999,11.554,12.493,13.478,14.507999999999999,15.585000000000001,16.709
body-mass-index-for-age,female,day,28,0.36370000000000002,14.4208,0.095769999999999994,10.646000000000001,11.824,13.081,14.420999999999999,15.843999999999999,17.353999999999999,18.952999999999999
body-mass-index-for-age,female,week,4,0.36370000000000002,14.4208,0.095769999999999994,10.6,11.8,13.1,14.4,15.8,17.399999999999999,19
body-mass-index-for-age,female,month,1,0.3448,14.5679,0.095560000000000006,10.8,12,13.2,14.6,16,17.5,19.100000000000001
body-mass-index-for-age,male,day,28,0.28810000000000002,14.7714,0.090719999999999995,11.125999999999999,12.26,13.474,14.771000000000001,16.155000000000001,17.629000000000001,19.196000000000002
body-mass-index-for-age,male,week,4,0.28810000000000002,14.7714,0.090719999999999995,11.1,12.3,13.5,14.8,16.2,17.600000000000001,19.2
body-mass-index-for-age,male,month,1,0.27079999999999999,14.944100000000001,0.090270000000000003,11.3,12.4,13.6,14.9,16.3,17.8,19.399999999999999
head-circumference-for-age,female,day,28,1,36.376100000000001,0.032149999999999998,32.868000000000002,34.036999999999999,35.207000000000001,36.375999999999998,37.545999999999999,38.715000000000003,39.884999999999998
head-circumference-for-age,female,week,4,1,36.376100000000001,0.032149999999999998,32.9,34,35.200000000000003,36.4,37.5,38.700000000000003,39.9
head-circumference-for-age,female,month,1,1,36.546300000000002,0.032099999999999997,33,34.200000000000003,35.4,36.5,37.700000000000003,38.9,40.1
head-circumference-for-age,male,day,28,1,37.092599999999997,0.031480000000000001,33.590000000000003,34.756999999999998,35.924999999999997,37.093000000000004,38.26,39.427999999999997,40.595999999999997
head-circumference-for-age,male,week,4,1,37.092599999999997,0.031480000000000001,33.6,34.799999999999997,35.9,37.1,38.299999999999997,39.4,40.6
head-circumference-for-age,male,month,1,1,37.2759,0.031329999999999997,33.799999999999997,34.9,36.1,37.299999999999997,38.4,39.6,40.799999999999997
length-height-for-age,female,day,28,1,53.380899999999997,0.036470000000000002,47.54,49.487000000000002,51.433999999999997,53.381,55.328000000000003,57.274999999999999,59.220999999999997
length-height-for-age,female,week,4,1,53.380899999999997,0.036470000000000002,47.5,49.5,51.4,53.4,55.3,57.3,59.2
length-height-for-age,female,month,1,1,53.687199999999997,0.036400000000000002,47.8,49.8,51.7,53.7,55.6,57.6,59.5
length-height-for-age,male,day,28,1,54.388100000000001,0.035700000000000003,48.563000000000002,50.505000000000003,52.445999999999998,54.387999999999998,56.33,58.271000000000001,60.213000000000001
length-height-for-age,male,week,4,1,54.388100000000001,0.035700000000000003,48.6,50.5,52.4,54.4,56.3,58.3,60.2
length-height-for-age,male,month,1,1,54.724400000000003,0.035569999999999997,48.9,50.8,52.8,54.7,56.7,58.6,60.6
subscapular-skinfold-for-age,female,day,91,-0.2019,7.7873999999999999,0.18428,4.6109999999999998,5.4580000000000002,6.4989999999999997,7.7869999999999999,9.3960000000000008,11.422000000000001,13.994999999999999
subscapular-skinfold-for-age,female,month,3,-0.2026,7.7846000000000002,0.18428,4.5999999999999996,5.5,6.5,7.8,9.4,11.4,14
subscapular-skinfold-for-age,male,day,91,-0.30299999999999999,7.6920000000000002,0.17019000000000001,4.7850000000000001,5.5640000000000001,6.516,7.6920000000000002,9.1609999999999996,11.016999999999999,13.395
subscapular-skinfold-for-age,male,month,3,-0.30330000000000001,7.6898999999999997,0.17019999999999999,4.8,5.6,6.5,7.7,9.1999999999999993,11,13.4
triceps-skinfold-for-age,female,day,91,0.18820000000000001,9.7532999999999994,0.17524999999999999,5.6070000000000002,6.7869999999999999,8.1609999999999996,9.7530000000000001,11.589,13.695,16.102
triceps-skinfold-for-age,female,month,3,0.1875,9.7515999999999998,0.17535000000000001,5.6,6.8,8.1999999999999993,9.8000000000000007,11.6,13.7,16.100000000000001
triceps-skinfold-for-age,male,day,91,0.0030000000000000001,9.7658000000000005,0.16611000000000001,5.931,7.0039999999999996,8.2710000000000008,9.766,11.53,13.612,16.068000000000001
triceps-skinfold-for-age,male,month,3,0.0027000000000000001,9.7638999999999996,0.16617999999999999,5.9,7,8.3000000000000007,9.8000000000000007,11.5,13.6,16.100000000000001
weight-for-age,female,day,28,0.1789,4.0987,0.13805000000000001,2.665,3.0880000000000001,3.5640000000000001,4.0990000000000002,4.6980000000000004,5.3659999999999997,6.1120000000000001
weight-for-age,female,week,4,0.1789,4.0987,0.13805000000000001,2.7,3.1,3.6,4.0999999999999996,4.7,5.4,6.1
weight-for-age,female,month,1,0.1714,4.1872999999999996,0.13724,2.7,3.2,3.6,4.2,4.8,5.5,6.2
weight-for-age,male,day,28,0.2331,4.3670999999999998,0.13497000000000001,2.8540000000000001,3.3050000000000002,3.8069999999999999,4.367,4.9880000000000004,5.6740000000000004,6.43
weight-for-age,male,week,4,0.2331,4.3670999999999998,0.13497000000000001,2.9,3.3,3.8,4.4000000000000004,5,5.7,6.4
weight-for-age,male,month,1,0.22969999999999999,4.4709000000000003,0.13395000000000001,2.9,3.4,3.9,4.5,5.0999999999999996,5.8,6.6
"""
@doc """
Sample data to validate calculation made to convert measure into z-score and vice-versa.
This data is based on values extracted from WHO csv files.
"""
@spec sample :: [
%{
key: String.t(),
zscore: number(),
measure: number(),
l: number(),
m: number(),
s: number()
}
]
def sample do
@sample_combined_csv
|> Growth.DataCase.parse_string()
|> Enum.flat_map(fn [
source,
gender,
age_unit,
age,
l,
m,
s,
sd3n,
sd2n,
sd1n,
sd0,
sd1,
sd2,
sd3
] ->
key = "#{source}:#{gender}:#{age_unit}:#{age}"
[l, m, s, sd3n, sd2n, sd1n, sd0, sd1, sd2, sd3] =
Enum.map(
[l, m, s, sd3n, sd2n, sd1n, sd0, sd1, sd2, sd3],
&(&1 |> Float.parse() |> elem(0))
)
[-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]
|> Enum.zip([sd3n, sd2n, sd1n, sd0, sd1, sd2, sd3])
|> Enum.map(fn {zscore, measure} ->
%{key: "#{key}:#{zscore}", zscore: zscore, measure: measure, l: l, m: m, s: s}
end)
end)
end
end