AHL NPM Scoring
Two definitions of the UK's Nutrient Profiling Model, NPM 2004/05 and NPM 2018, scored on numpy arrays or polars frames.
Installing
The package is installable from PyPI on Python 3.10+ with pip, uv, etc.:
uv add ahl-npm
Usage
import ahl_npm as npm
# numpy: numbers or arrays per 100g, which broadcast together (e.g. one panel against multiple possible free sugars values)
scored = npm.score(
scheme=2018, energy_kj=1500, satfat=4.5, free_sugars=[2.0, 6.5], salt=1.25, protein=10, fibre=3, fvns_pct=0
)
scored["npm"] # array([11., 15.]), with each component's points, the A and C points and whether protein counts
# polars: `columns` maps each input to a column or an expression; without it, the frame's columns named as inputs
scored = npm.score_frame(
frame=df,
scheme=2004,
columns={
"energy_kcal": "kcal_per_100g",
"satfat": "satfat_per_100g",
"sugars": "sugar_per_100g",
"sodium": "sodium_per_100g",
"protein": "protein_per_100g",
"fibre": "fibre_per_100g",
"fvn_pct": pl.col("fvn_share") * 100,
},
) # adds energy_points_2004, ..., a_points_2004, c_points_2004, protein_counted_2004, npm_2004, cnpm_2004
outputs picks which columns to add and, when passed as a mapping, renames them (between prefix and suffix), e.g. outputs={"npm": "npm_score", "cnpm": "cnpm_score"} only adds npm_score_2004 and cnpm_score_2004. Inputs a scheme doesn't use are ignored, so the same mapping can be used for both schemes.
score_expr returns the same scoring as a struct expression, and band and band_expr give the points for a single component, e.g. npm.band(2018, "free_sugars", values).
Inputs
All inputs are densities (g/100g) as sold. Where a component can be scored from more than one input, each row uses the first one available: the quantity the scheme scores, then a converted stand-in, then the component's points if given.
| Input | Unit | Scored by |
|---|---|---|
energy_kj, energy_kcal |
kJ | both, using kJ if given, else kcal x 4.184 |
satfat |
g | both |
sugars |
g | NPM 2004/05 (total sugars) |
free_sugars |
g | NPM 2018 |
sodium, salt |
g | NPM 2004/05 scores sodium (else salt / 2.5), NPM 2018 scores salt (else sodium x 2.5) |
protein |
g | both |
fibre |
g | both (AOAC method) |
fvn_pct |
% | NPM 2004/05 (fruit, vegetables and nuts) |
fvns_pct |
% | NPM 2018 (fruit, vegetables, nuts and seeds) |
<component>_points |
both, for points already scored, as an alternative to the densities above |
Points inputs are named the same as the outputs, so a scoring's points can be passed straight back in, and are checked against the points the component can score under the scheme. NPM 2018 includes seeds, so it only uses fvns_pct or fvns_points: if you only have the NPM 2004/05 share, pass it as fvns_pct too. NPM 2018 also needs free sugars. Scoring total sugars as free sugars isn't supported, as it overestimates NPM 2018 scores, but can be done by passing total sugars as free_sugars.
Salt is sodium x 2.5, as UK labelling law defines it (Regulation (EU) No 1169/2011, Annex I, as retained in UK law).
Outputs
Each component's points (<component>_points), a_points, c_points, protein_counted, npm and cnpm (70 - 2 x npm). The numpy functions return floats with NaN where unknown (or integers with strict=True, which raises on missing inputs), and the polars functions return integers (a boolean for protein_counted) with nulls where unknown. Protein stops counting from 11 A points, unless fruit, vegetables and nuts score 5 points.
Checks and warnings
The most likely mistake is an input in the wrong units, so each frame scoring logs (at INFO, to the ahl_npm logger) which column or expression each component reads, in what units, and with any conversion. describe_inputs gives the same description for any inputs:
NPM 2004 reads, per 100 g:
energy: energy_kcal <- "kcal_per_100g", kcal x 4.184 to kJ
sodium: sodium <- "sodium_per_100g", g; else salt <- "salt_per_100g", g / 2.5 to sodium
fvn: fvn_pct <- [(col("fvn_share")) * (dyn int: 100)], %
...
ImpossibleInputWarningwarns of implausible density values, i.e. below 0, or above 100g, 100% or the energy of pure fat (3,700 kJ or 900 kcal), which usually means an input is in the wrong units (e.g. sodium in mg read as g).MissingInputWarningsummarises missing inputs (NaN, or null in polars). A missing input scores NaN points rather than the maximum, so any score it feeds into is missing too.warn=Falseturns both warnings off. A frame only warns once, for the first batch of rows with problems.
Other functions
to_cnpmandfrom_cnpmconvert between NPM and cNPM (70 - 2 x the score, s.t. higher is healthier) for numbers, arrays, and polars expressions or series.clip=Trueclips cNPM to between 0 and 100, andchange=Trueconverts changes in scores (without the offset).less_healthy(scheme, npm, drink)gives whether each product is less healthy, i.e. a food scoring 4 or more, or a drink scoring 1 or more.drinkmust be booleans with none missing (in polars, a null gives a null). Whether a product is in scope of HFSS legislation is a separate question.score_range(scheme, low, high, **fixed)gives the lowest and highest scores for inputs anywhere between theirlowandhighvalues. The score never decreases as an A input increases or a C input decreases, so only two scorings are needed, however many inputs are uncertain.
Development Setup
This project uses uv and direnv. To set up a development environment, run:
direnv allow
uv sync
uv run pre-commit install --install-hooks
uv run pytest
uv run tox runs the tests under each Python version from 3.10 to 3.15, and (lowest) with the oldest numpy, polars and PyYAML versions allowed. CI runs these on every pull request and push to dev or main, and weekly.
Metadata
Release files for ahl-npm 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ahl_npm-1.0.0.tar.gz | 136.4 kB | Without provenance | Details for ahl_npm-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ahl_npm-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for ahl_npm-1.0.0-py3-none-any.whl |
Total release size: 158.3 kB
Release files / ahl_npm-1.0.0.tar.gz
| Download URL | ahl_npm-1.0.0.tar.gz |
|---|---|
| Size | 136.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
34167a702b9fe7817a7638aeba03a780597df85fb546a3042205be9a0c3d5525
|
|
BLAKE2b-256 checksum How to use checksums |
1a82993d1405f17efc75908f4bf550c51e781acfd80d41d06a763d7d1ed52d93
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / ahl_npm-1.0.0-py3-none-any.whl
| Download URL | ahl_npm-1.0.0-py3-none-any.whl |
|---|---|
| Size | 21.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
23f5d28f40713ee9b2ab598f805721c96784c7bfee65417abbb12c58fb3b5705
|
|
BLAKE2b-256 checksum How to use checksums |
c285c1fef3fe09186d2b6c4f3b4e3bec981767cf43b7ec72837d4bd770ccec77
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for ahl-npm 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.