Compare commits

..

76 Commits

Author SHA1 Message Date
0bf11bb809
feat(deps): upgrade transitive dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
* `earmark_parser` from 1.4.43 to 1.4.44
* `makeup` from 1.1.2 to 1.2.1
* `makeup_elixir` from 0.16.2 to 1.0.1
* `makeup_erlang` from 1.0.1 to 1.0.2
* `plug` from 1.16.1 to 1.17.0
* `saxy` from 1.5.0 to 1.6.0
2025-03-14 17:47:57 +00:00
706f065dc8
feat(deps): upgrade dependencies
* `ex_doc` from 0.36.1 to 0.37.3
* `xlsx_reader` from 0.8.7 to 0.8.8
2025-03-14 17:45:52 +00:00
8d07a04b95
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
Conflicts:
    mix.lock
2025-03-14 17:38:28 +00:00
81ea8bc06b
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2025-02-14 22:07:52 +00:00
18759bf234
feat(deps): upgrade dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
* `decimal`: from 2.1.1 to 2.3.0
* `earmark_parser`: from 1.4.41 to 1.4.43
* `ex_doc`: from 0.34.2 to 0.36.1
* `req`: from 0.5.7 to 0.5.8

https://diff.hex.pm/diffs?diffs[]=decimal:2.1.1:2.3.0&diffs[]=earmark_parser:1.4.41:1.4.43&diffs[]=ex_doc:0.34.2:0.36.1&diffs[]=req:0.5.7:0.5.8&
2025-01-26 15:10:24 +00:00
d385451a5e
merge: updates from upstream
Conflicts:
    mix.exs
    mix.lock
2025-01-26 15:07:39 +00:00
97b1ef9a6b
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-12-07 15:50:18 +00:00
4b643202c3
feat: update deps
All checks were successful
continuous-integration/drone/pr Build is passing
* `req` from 0.5.6 to 0.5.7
2024-11-02 14:24:09 +00:00
adfb85533b
merge: updates from upstream
Conflicts:
    mix.lock
2024-11-02 14:22:34 +00:00
f5d001d419
merge: updates from upstream 2024-11-02 13:30:15 +00:00
aef062dcf5
fix(growth): remove unused match
All checks were successful
continuous-integration/drone/pr Build is passing
2024-11-02 12:56:38 +00:00
02540e1046
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
Conflicts:
  mix.exs
  mix.lock
2024-10-30 19:56:46 +00:00
69cd36d7e1
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-21 02:16:01 +00:00
f6cfff14e7
merge: update from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-21 02:14:48 +00:00
ffa2a68b7a
fix(growth): use Application.app_dir to write/read priv dir
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-21 01:53:14 +00:00
fb38910137
test(growth): add test for happy download paths
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-07 19:27:27 +00:00
6ba171343d
test(growth): add dep to write in-memory excel data
This will be used to mock data from WHO site.
2024-10-07 19:23:26 +00:00
0fc8c56c41
chore(growth): add separated method to merge genders data
This makes easier to test aggregation logic for multiple gender
configuration.
2024-10-07 19:22:14 +00:00
c77d4eb97d
chore(growth): allow one to customize Req options
With this change it's possible to change the way the application make
requests to WHO website.
2024-10-07 19:21:27 +00:00
df16f2ce1c
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-07 12:17:42 +00:00
25cb7508bb
test(growth): add main module tests
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-05 15:11:33 +00:00
84cf6758bb
fix(growth): results is now a keyword list 2024-10-05 15:10:40 +00:00
7e041ae6c9
chore(growth): adjust indicator lookup to use atoms for unit 2024-10-05 15:09:44 +00:00
82d7009b34
chore(growth): adjust key in ets tables to contain atoms
The `ets` tables containing growth indicator has a key in the form of
`{gender :: atom(), unit :: atom(), value :: number()}`.
2024-10-05 15:07:10 +00:00
fa65a36852
test(growth): fix doctest for centile and percentile
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-05 12:58:36 +00:00
1e36c29180
test(growth): add percentile test 2024-10-05 12:58:04 +00:00
8c89b55487
test(growth): add centile compute test 2024-10-05 12:57:20 +00:00
a427e11958
test(growth): add zscore compute test 2024-10-05 12:56:28 +00:00
c9e3bf68ae
test(growth): add bmi test 2024-10-05 12:55:44 +00:00
7670dce630
test(growth): add sample data to be used in tests 2024-10-05 12:53:56 +00:00
f3846cad24
chore(growth): update doc to reflect percentile values
All checks were successful
continuous-integration/drone/pr Build is passing
This fixes an issue were the returned value represented the measurment
at a given z-score (the centile value).
2024-10-03 22:16:15 +00:00
27a7a05584
fix(growth): improve doc for centile calculation
This module converts a given z-score and Box-Cox values into the related
measurement.
2024-10-03 22:14:56 +00:00
fc0d9db7a5
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
Conflicts:
  mix.exs
  mix.lock
2024-10-02 12:31:04 +00:00
d9dcfa3846
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-02 10:40:04 +00:00
3a69b8d4ff
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-10-01 14:29:26 +00:00
7c15e1fae6
chore: execute dialyzer with short format
All checks were successful
continuous-integration/drone/pr Build is passing
2024-09-26 00:32:44 +00:00
8328bbe9fb
merge: updates from tracking
All checks were successful
continuous-integration/drone/pr Build is passing
2024-09-25 23:48:32 +00:00
90613635fb
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
Conflicts:
    .gitignore
    lib/wabanex/application.ex
    mix.lock
2024-09-22 19:06:02 +00:00
dc6e00d54b
chore: ignore elixir tools folder
All checks were successful
continuous-integration/drone/pr Build is passing
2024-09-18 11:49:23 +00:00
5c044e69d9
fix(growth): use centile and percentile correctly
1. The calculation made in `Grownth.Calc.Centile.compute/4` converts a
   given z-score into the expected measurement value.
   This can be used to get specific values for key z-scores, such as,
   -3, -2, 2 and 3.
2. To calculate the percentile of a given z-score the system uses the
   cumulative distribution function.
2024-08-28 13:03:45 +00:00
824a70452f
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-08-13 14:17:32 +00:00
b696cc129d
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
Conflicts:
    mix.exs
    mix.lock
2024-08-12 20:19:36 +00:00
c923b2efb7
test(growth): work on age calculation
All checks were successful
continuous-integration/drone/pr Build is passing
2024-07-12 11:18:40 +00:00
8e9907a3e8
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-07-12 11:15:30 +00:00
3914bd5240
merge: updates from upstream
All checks were successful
continuous-integration/drone/pr Build is passing
2024-07-08 21:55:02 +00:00
8dffb86d01
chore(deps): update deps of deps
All checks were successful
continuous-integration/drone/pr Build is passing
2024-07-08 21:36:37 +00:00
866730238e
feat(deps): upgrade req from 0.5.0 to 0.5.2 2024-07-08 21:28:35 +00:00
eeb6964348
merge: updates from upstream
Conflicts:
    mix.lock
2024-07-08 21:25:21 +00:00
0d7a4cc9c3
chore(growth): move nil logic to scorer behaviour
All checks were successful
continuous-integration/drone/pr Build is passing
Handle `nil` measurement in `Growth.Score.Scorer`.
2024-06-11 14:53:19 +00:00
7fab746ea8
feat: add ex_doc to generate documentation
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-11 12:18:18 +00:00
c81e4fda67
chore(growth): add log in loader
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-11 12:02:14 +00:00
bc9c2124f1
chore(growth): add example for Growth.new/4 2024-06-11 12:01:50 +00:00
c14a26a184
fix(growth): measure for subscapular skinfold 2024-06-11 12:01:25 +00:00
79aec9f7e5
feat(growth): calculate z-score/centile for all indicators
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-11 11:42:57 +00:00
2545a796c3
chore(growth): return nil when measure isn't set 2024-06-11 11:42:17 +00:00
88b8e811a8
chore(growth): apply scorer behaviour in indicators 2024-06-11 11:41:31 +00:00
fa022b9592
feat(growth): calculate z-score/centile for all indicators
The `Growth.Score.Scorer` behaviour implements the methods needed to
calculate z-score and centile for any indicator.
2024-06-11 11:38:18 +00:00
0ac434c61d
fix(growth): ignore enum result explicitly
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-10 19:24:41 +00:00
06b35cbceb
wip(growth): implement results for bmi
Some checks failed
continuous-integration/drone/pr Build is failing
This implementation should be made generic for all other scores.
2024-06-10 19:09:35 +00:00
28f0546bcb
chore(growth): add default value for results 2024-06-10 18:35:12 +00:00
c175c3edfd
fix(growth): typespec and doc for centile calc 2024-06-10 18:34:24 +00:00
4234b2e917
chore(growth): load is independent from task
Some checks failed
continuous-integration/drone/pr Build is failing
Ensure that `Growth.Indicators.Load.all/0` can be run independently from
the task.

So, the `ets` table is create based on the filename processed.
2024-06-10 13:01:44 +00:00
04fff60541
wip(growth): add score module
Some checks failed
continuous-integration/drone/pr Build is failing
This module is responsible for calculate the z score/centile for each
measurement.
2024-06-10 12:15:16 +00:00
b415b38184
feat(growth): load data on application start
To make this possible, the following changes were made:

* Use `Task` to start the load process
* Create `ets` table before start the async task
  * This is needed to keep the tables alive after the task process exits
* Adjust the application to start the `Growth.Indicators.Load` task
2024-06-10 12:11:18 +00:00
412250d7ef
feat(growth): add struct to represent measurement
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-09 20:00:49 +00:00
d3c1aa9b6b
chore(growth): improve age documentation and typing 2024-06-09 19:59:33 +00:00
025ab1537d
chore(growth): name ets tables with score modules 2024-06-09 19:58:34 +00:00
52c1d0c028
feat(growth): load csv indicators into ets tables
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-07 13:28:13 +00:00
902a22c947
chore(growth): fetch extended data for indicators
All checks were successful
continuous-integration/drone/pr Build is passing
Also, improve module doc.
2024-06-06 22:49:52 +00:00
a8466f0c1d
chore(growth): add documentation and examples
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-06 20:39:10 +00:00
Joao P Dubas
bb79aa2f3a fix(growth): alias z-score and use if
Some checks failed
continuous-integration/drone/pr Build is failing
2024-06-06 11:34:02 +00:00
500d0e89bb
wip(growth): calculate key metrics for growth
Some checks failed
continuous-integration/drone/pr Build is failing
1. calculate age in days, weeks, and months
2. calculate body mass index for metric and english unities
3. calculate z-score for a given measurement and box-cox parameters (l,
   m, s)
4. calculate centile score for a given measurement and box-cox
   parameters
2024-06-06 01:36:11 +00:00
8be9fa38bb
wip(growth): add fix note on convert/3 method
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-04 23:56:27 +00:00
9c8e1d20ee
wip(growth): improve data parsing
1. Fetch common data for all indicators
2. Remove headers during merge

Also, re-generate files.
2024-06-04 23:55:03 +00:00
2fedb4ecf0
wip(growth): change from with to case
All checks were successful
continuous-integration/drone/pr Build is passing
2024-06-04 14:36:20 +00:00
58f14c0346
wip(growth): fetch|extract|save indicators from WHO
Initial implementation to fetch growth indicators from WHO.

For now, the system merges the measurements for each gender and age
group in a single `csv` file.

The final solution should do this, saving a `dets` file.
2024-06-04 13:57:32 +00:00
38 changed files with 32836 additions and 4 deletions

View File

@ -27,11 +27,15 @@ credo: ## run credo
.PHONY: dialyzer .PHONY: dialyzer
dialyzer: ## run dialyzer dialyzer: ## run dialyzer
@mix dialyzer --no-check --quiet --ignore-exit-status --format short @mix dialyzer --format short
.PHONY: static_code_analysis .PHONY: static_code_analysis
static_code_analysis: check_format check_compile credo dialyzer ## run 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 .PHONY: test
test: ## run tests test: ## run tests
@mix test --cover --trace --slowest 10 @mix test --cover --trace --slowest 10

View File

@ -1,5 +1,8 @@
import Config import Config
config :wabanex, Growth.Indicators.Download,
who_req_options: [plug: {Req.Test, Growth.Indicators.Download.WHO}]
config :wabanex, Wabanex.Repo, config :wabanex, Wabanex.Repo,
database: "wabanex_test#{System.get_env("MIX_TEST_PARTITION")}", database: "wabanex_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox 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, Wabanex.Repo,
WabanexWeb.Telemetry, WabanexWeb.Telemetry,
{Phoenix.PubSub, name: Wabanex.PubSub}, {Phoenix.PubSub, name: Wabanex.PubSub},
{Growth.Indicators.Load, []},
{DNSCluster, {DNSCluster,
query: Application.get_env(:wabanex, :dns_cluster_query) || :ignore, query: Application.get_env(:wabanex, :dns_cluster_query) || :ignore,
log: :info, log: :info,

10
mix.exs
View File

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

View File

@ -12,9 +12,12 @@
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "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"}, "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"}, "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": {: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"}, "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"}, "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"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "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"}, "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"}, "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"}, "junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"},
"lcov_ex": {:hex, :lcov_ex, "0.3.4", "f48aed787db0d1cff1409db391f442cdbc0af0f860dbc326430030027e6ded36", [:mix], [], "hexpm", "c3987b6aeadd78d4b7933fa9cc4de3bd69d34f0fae39bad908f79a7ceea957e5"}, "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"}, "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"}, "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"}, "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_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "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_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_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"}, "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_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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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": {: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"}, "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"}, "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"}, "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
@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