feat(growth): add weight-for-height calculation
All checks were successful
continuous-integration/drone/pr Build is passing

This commit is contained in:
João Paulo Dubas 2025-04-30 22:40:45 +00:00
parent 49e990935c
commit c481c42e5b
Signed by: joao.dubas
SSH Key Fingerprint: SHA256:V1mixgOGRc/YMhGx/DNkOSmJxgA2vHNrDZEk3wt/kOA
3 changed files with 240 additions and 0 deletions

View File

@ -1,5 +1,95 @@
defmodule Growth.Score.WeightForHeight do
@moduledoc """
Calculate z-score for weight for height.
This module calculates the z-score for weight relative to height, which is an important
indicator for assessing whether a child's weight is appropriate for their height,
regardless of age.
**Limitation**: the memasurements do not differentiate between length and height, and
always assume height.
"""
@behaviour Growth.Score.Scorer
alias Growth.Score.Scorer
@impl Scorer
@spec measure_name() :: atom()
@doc """
Name of the measurement used in weight for height indicator.
"""
def measure_name do
:weight_for_height
end
@doc """
Custom implementation for weight-for-height lookup, as it requires both weight and height.
This overrides the default implementation in the Scorer module.
"""
@spec lms(Growth.t(), module()) :: [{String.t(), {number(), number(), number()}}]
def lms(%Growth{gender: gender, weight: weight, height: height}, _indicator)
when is_number(weight) and is_number(height) do
# For weight-for-height, we use height as the lookup value instead of age
key = {{gender, :height, height}, :_}
case :ets.match_object(__MODULE__, key) do
[{{^gender, _, ^height}, %{l: l, m: m, s: s}} | _] ->
[{"height", {l, m, s}}]
_ ->
# Try to find the closest height value
find_closest_height(gender, height)
end
end
def lms(_growth, _indicator) do
[]
end
@doc """
Find the closest height value in the ETS table when an exact match isn't found.
"""
@spec find_closest_height(Growth.gender(), number()) :: [
{String.t(), {number(), number(), number()}}
]
def find_closest_height(gender, height) do
# Fetch all entries for the given gender and height measurement
matcher = [
{
{{:"$1", :"$2", :"$3"}, :_},
[
{:andalso,
{:andalso, {:andalso, {:==, :"$1", gender}, {:==, :"$2", :height}},
{:>, :"$3", height - 1.0}}, {:<, :"$3", height + 1.0}}
],
[:"$_"]
}
]
gender_height_entries = :ets.select(__MODULE__, matcher)
case gender_height_entries do
[] ->
# No entries found for this gender
[]
entries ->
# Find the entry with the height closest to the target height
closest_entry =
entries
|> Enum.filter(fn {{_, _, entry_height}, _lms_data} ->
abs(entry_height - height) <= 0.5
end)
|> Enum.min_by(fn {{_, _, entry_height}, _lms_data} ->
abs(entry_height - height)
end)
# Extract the LMS data from the closest entry
{_, %{l: l, m: m, s: s}} = closest_entry
[{"height", {l, m, s}}]
end
end
end

View File

@ -0,0 +1,142 @@
# test/growth/score/weight_for_height_test.exs
defmodule Growth.Score.WeightForHeightTest do
use ExUnit.Case, async: true
alias Growth
alias Growth.Score.WeightForHeight
describe "measure_name/0" do
test "returns the correct measure name atom" do
assert WeightForHeight.measure_name() == :weight_for_height
end
end
describe "lms/2" do
test "returns LMS values for exact height match" do
growth = %Growth{
gender: :male,
weight: 7.3,
height: 65.0,
name: "Joe Doe",
date_of_measurement: Date.utc_today(),
date_of_birth: Date.shift(Date.utc_today(), day: -91)
}
assert WeightForHeight.lms(growth, WeightForHeight) == [
{"height", {-0.35210000000000002, 7.4326999999999996, 0.082170000000000007}}
]
end
test "returns LMS values for closest height match when exact is not found" do
growth = %Growth{
gender: :male,
weight: 7.3,
height: 64.9,
name: "Joe Doe",
date_of_measurement: Date.utc_today(),
date_of_birth: Date.shift(Date.utc_today(), day: -91)
}
assert WeightForHeight.lms(growth, WeightForHeight) == [
{"height", {-0.35210000000000002, 7.4326999999999996, 0.082170000000000007}}
]
growth_closer_to_higher = %Growth{
gender: :male,
weight: 7.4,
height: 65.09,
name: "Joe Doe",
date_of_measurement: Date.utc_today(),
date_of_birth: Date.shift(Date.utc_today(), day: -91)
}
assert WeightForHeight.lms(growth_closer_to_higher, WeightForHeight) == [
{"height", {-0.35210000000000002, 7.4562999999999997, 0.082159999999999997}}
]
end
test "returns empty list if weight or height is missing" do
growth_no_weight = %Growth{
gender: :male,
height: 60.0,
name: "Joe Doe",
date_of_measurement: Date.utc_today(),
date_of_birth: Date.shift(Date.utc_today(), day: -91)
}
assert WeightForHeight.lms(growth_no_weight, WeightForHeight) == []
growth_no_height = %Growth{
gender: :male,
weight: 5.0,
name: "Joe Doe",
date_of_measurement: Date.utc_today(),
date_of_birth: Date.shift(Date.utc_today(), day: -91)
}
assert WeightForHeight.lms(growth_no_height, WeightForHeight) == []
growth_nil_values = %Growth{
gender: :male,
weight: nil,
height: nil,
name: "Joe Doe",
date_of_measurement: Date.utc_today(),
date_of_birth: Date.shift(Date.utc_today(), day: -91)
}
assert WeightForHeight.lms(growth_nil_values, WeightForHeight) == []
end
test "returns empty list if no matching gender data exists" do
# Assuming no :unknown gender data was inserted
growth = %Growth{
gender: :unknown,
weight: 5.0,
height: 60.0,
name: "Joe Doe",
date_of_measurement: Date.utc_today(),
date_of_birth: Date.shift(Date.utc_today(), day: -91)
}
assert WeightForHeight.lms(growth, WeightForHeight) == []
end
end
describe "find_closest_height/2" do
test "finds the lower closest height" do
assert WeightForHeight.find_closest_height(:male, 64.99) == [
{"height", {-0.35210000000000002, 7.4326999999999996, 0.082170000000000007}}
]
end
test "finds the higher closest height" do
assert WeightForHeight.find_closest_height(:male, 65.61) == [
{"height", {-0.35210000000000002, 7.5738000000000003, 0.082140000000000005}}
]
end
test "finds the lower closest height when exactly midway" do
expected_lms =
[
[{"height", {-0.35210000000000002, 7.5034000000000001, 0.082150000000000001}}],
[{"height", {-0.35210000000000002, 7.4798999999999998, 0.082159999999999997}}]
]
lms = WeightForHeight.find_closest_height(:male, 65.25)
assert Enum.any?(expected_lms, fn entity_lms -> entity_lms == lms end)
end
test "do not match when below minimum height" do
assert WeightForHeight.find_closest_height(:male, 44.0) == []
end
test "do not match when above maximum height" do
assert WeightForHeight.find_closest_height(:male, 121.0) == []
end
test "returns empty list when no data exists for the gender" do
assert WeightForHeight.find_closest_height(:unknown, 60.0) == []
end
end
end

View File

@ -61,6 +61,14 @@ defmodule Growth.Data do
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
weight-for-height,female,length,65,-0.38329999999999997,7.0811999999999999,0.091189999999999993,5.4589999999999996,5.9370000000000003,6.4740000000000002,7.0810000000000004,7.77,8.5549999999999997,9.4540000000000006
weight-for-height,female,length,65.099999999999994,-0.38329999999999997,7.1040999999999999,0.091179999999999997,5.4770000000000003,5.9560000000000004,6.4950000000000001,7.1040000000000001,7.7949999999999999,8.5820000000000007,9.484
weight-for-height,female,height,65,-0.38329999999999997,7.2401999999999997,0.091130000000000003,5.5830000000000002,6.0709999999999997,6.62,7.24,7.944,8.7460000000000004,9.6639999999999997
weight-for-height,female,height,65.099999999999994,-0.38329999999999997,7.2626999999999997,0.091120000000000007,5.6,6.09,6.64,7.2629999999999999,7.9690000000000003,8.7729999999999997,9.6940000000000008
weight-for-height,male,length,65,-0.35210000000000002,7.2666000000000004,0.082229999999999998,5.7359999999999998,6.1929999999999996,6.7009999999999996,7.2670000000000003,7.899,8.6080000000000005,9.4060000000000006
weight-for-height,male,length,65.099999999999994,-0.35210000000000002,7.2904999999999998,0.082220000000000001,5.7549999999999999,6.2130000000000001,6.7229999999999999,7.29,7.9249999999999998,8.6359999999999992,9.4369999999999994
weight-for-height,male,height,65,-0.35210000000000002,7.4326999999999996,0.082170000000000007,5.8680000000000003,6.335,6.8540000000000001,7.4329999999999998,8.0790000000000006,8.8040000000000003,9.6189999999999998
weight-for-height,male,height,65.099999999999994,-0.35210000000000002,7.4562999999999997,0.082159999999999997,5.8869999999999996,6.3550000000000004,6.8760000000000003,7.4560000000000004,8.1050000000000004,8.8309999999999995,9.6489999999999991
"""
@doc """