Skip to main content

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)], %
  ...
  • ImpossibleInputWarning warns 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).
  • MissingInputWarning summarises 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=False turns both warnings off. A frame only warns once, for the first batch of rows with problems.

Other functions

  • to_cnpm and from_cnpm convert between NPM and cNPM (70 - 2 x the score, s.t. higher is healthier) for numbers, arrays, and polars expressions or series. clip=True clips cNPM to between 0 and 100, and change=True converts 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. drink must 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 their low and high values. 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)

Source distribution for ahl-npm 1.0.0
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)

Table of built distributions (wheels) for ahl-npm 1.0.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page