Skip to content

Commit

Permalink
fix: new additives, non-nutritive sweeteners for new Nutri-Score (#9005)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephanegigandet authored Sep 20, 2023
1 parent d4a8bae commit 691627f
Show file tree
Hide file tree
Showing 83 changed files with 2,280 additions and 616 deletions.
15 changes: 9 additions & 6 deletions docs/api/ref/schemas/product_nutrition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ type: object
description: |
Nutrition fields of a product
properties:
fruits-vegetables-nuts_100g_estimate:
type: integer
description: |
An estimate, from ingredients list of the percentage of vegetable and nuts.
This is an important information for Nutri-Score computation.
no_nutrition_data:
type: string
description: |
Expand Down Expand Up @@ -45,8 +40,16 @@ properties:
type: number
fat:
type: number
fruits-vegetables-nuts-estimate-from-ingredients_100g:
fruits-vegetables-legumes-estimate-from-ingredients:
type: number
description: |
An estimate, from the ingredients list of the percentage of fruits, vegetable and legumes.
This is an important information for Nutri-Score (2023 version) computation.
fruits-vegetables-nuts-estimate-from-ingredients:
type: number
description: |
An estimate, from the ingredients list of the percentage of fruits, vegetable and nuts.
This is an important information for Nutri-Score (2021 version) computation.
nova-group:
type: integer
nutrition-score-fr:
Expand Down
12 changes: 10 additions & 2 deletions lib/ProductOpener/Food.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1265,7 +1265,7 @@ sub compute_nutriscore_data ($product_ref, $prepared, $nutriments_field, $versio

# The 2021 and 2023 version of the Nutri-Score need different nutrients
if ($version eq "2021") {
# fruits, vegetables, nuts, olive / rapeseed / walnut oils
# fruits, vegetables, nuts, olive / rapeseed / walnut oils - 2021
my $fruits_vegetables_nuts_colza_walnut_olive_oils
= compute_nutriscore_2021_fruits_vegetables_nuts_colza_walnut_olive_oil($product_ref, $prepared);

Expand Down Expand Up @@ -1302,12 +1302,14 @@ sub compute_nutriscore_data ($product_ref, $prepared, $nutriments_field, $versio
}
}
else {
# fruits, vegetables, legumes - 2023
my $fruits_vegetables_legumes = compute_nutriscore_2023_fruits_vegetables_legumes($product_ref, $prepared);

my $is_fat_oil_nuts_seeds = is_fat_oil_nuts_seeds_for_nutrition_score($product_ref);
my $is_beverage = $product_ref->{nutrition_score_beverage};

$nutriscore_data_ref = {
is_beverage => $product_ref->{nutrition_score_beverage},
is_beverage => $is_beverage,
is_water => is_water_for_nutrition_score($product_ref),
is_cheese => is_cheese_for_nutrition_score($product_ref),
is_fat_oil_nuts_seeds => $is_fat_oil_nuts_seeds,
Expand Down Expand Up @@ -1335,6 +1337,12 @@ sub compute_nutriscore_data ($product_ref, $prepared, $nutriments_field, $versio
$nutriscore_data_ref->{energy_from_saturated_fat} = $nutriscore_data_ref->{saturated_fat} * 37;
}
}

if ($is_beverage) {
if (defined $product_ref->{with_non_nutritive_sweeteners}) {
$nutriscore_data_ref->{with_non_nutritive_sweeteners} = $product_ref->{with_non_nutritive_sweeteners};
}
}
}

# tweak data to take into account special cases
Expand Down
54 changes: 41 additions & 13 deletions lib/ProductOpener/Ingredients.pm
Original file line number Diff line number Diff line change
Expand Up @@ -6040,16 +6040,44 @@ sub extract_ingredients_classes_from_text ($product_ref) {
= $product_ref->{ingredients_that_may_be_from_palm_oil_n} + $product_ref->{ingredients_from_palm_oil_n};
}

# Determine if the product has sweeteners, and non nutritive sweeteners
determine_if_the_product_contains_sweeteners($product_ref);

return;
}

=head2 determine_if_the_product_contains_sweeteners
Check if the product contains sweeteners and non nutritive sweeteners (used for the Nutri-Score for beverages)
The NNS / Non nutritive sweeteners listed in the Nutri-Score Update report beverages_31 01 2023-voted
have been added as a non_nutritive_sweetener:en:yes property in the additives taxonomy.
=cut

sub determine_if_the_product_contains_sweeteners ($product_ref) {

delete $product_ref->{with_sweeteners};
if (defined $product_ref->{'additives_tags'}) {
foreach my $additive (@{$product_ref->{'additives_tags'}}) {
my $e = $additive;
$e =~ s/\D//g;
if (($e >= 950) and ($e <= 968)) {
$product_ref->{with_sweeteners} = 1;
last;
}
}
delete $product_ref->{with_non_nutritive_sweeteners};

if (
get_matching_regexp_property_from_tags(
'additives', $product_ref->{'additives_tags'},
'additives_classes:en', 'sweetener'
)
)
{
$product_ref->{with_sweeteners} = 1;
}

if (
get_matching_regexp_property_from_tags(
'additives', $product_ref->{'additives_tags'},
'non_nutritive_sweetener:en', 'yes'
)
)
{
$product_ref->{with_non_nutritive_sweeteners} = 1;
}

return;
Expand Down Expand Up @@ -6422,8 +6450,8 @@ sub add_ingredients_matching_function ($ingredients_ref, $match_function_ref) {
if (defined $ingredient_ref->{percent}) {
$count += $ingredient_ref->{percent};
}
elsif (defined $ingredient_ref->{percent_min}) {
$count += $ingredient_ref->{percent_min};
elsif (defined $ingredient_ref->{percent_estimate}) {
$count += $ingredient_ref->{percent_estimate};
}
# We may not have percent_min if the ingredient analysis failed because of seemingly impossible values
# in that case, try to get the possible percent values in nested sub ingredients
Expand All @@ -6441,7 +6469,7 @@ sub add_ingredients_matching_function ($ingredients_ref, $match_function_ref) {

=head2 estimate_ingredients_matching_function ( $product_ref, $match_function_ref, $nutrient_id = undef )
This function analyzes the ingredients to estimate the minimum percentage of ingredients of a specific type
This function analyzes the ingredients to estimate the percentage of ingredients of a specific type
(e.g. fruits/vegetables/legumes for the Nutri-Score).
=head3 Parameters
Expand All @@ -6458,7 +6486,7 @@ If the $nutrient_id argument is defined, we also store the nutrient value in $pr
=head3 Return value
Minimum percentage of ingredients matching the function.
Estimated percentage of ingredients matching the function.
=cut

Expand Down
9 changes: 5 additions & 4 deletions lib/ProductOpener/Nutriscore.pm
Original file line number Diff line number Diff line change
Expand Up @@ -718,17 +718,18 @@ sub compute_nutriscore_score_2023 ($nutriscore_data_ref) {

# Beverages with non-nutritive sweeteners have 4 extra negative points
if ($nutriscore_data_ref->{is_beverage}) {
if ($nutriscore_data_ref->{has_sweeteners}) {
$nutriscore_data_ref->{"sweeteners_points"} = 4;
if ($nutriscore_data_ref->{with_non_nutritive_sweeteners}) {
$nutriscore_data_ref->{"non_nutritive_sweeteners_points"} = 4;
}
else {
$nutriscore_data_ref->{"sweeteners_points"} = 0;
$nutriscore_data_ref->{"non_nutritive_sweeteners_points"} = 0;
}
}

# Negative points

$nutriscore_data_ref->{negative_nutrients} = [$energy, "sugars", $saturated_fat, "salt", "sweeteners"];
$nutriscore_data_ref->{negative_nutrients}
= [$energy, "sugars", $saturated_fat, "salt", "non_nutritive_sweeteners"];
$nutriscore_data_ref->{negative_points} = 0;
foreach my $nutrient (@{$nutriscore_data_ref->{negative_nutrients}}) {
$nutriscore_data_ref->{negative_points} += ($nutriscore_data_ref->{$nutrient . "_points"} || 0);
Expand Down
101 changes: 91 additions & 10 deletions lib/ProductOpener/Tags.pm
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ BEGIN {
&get_property
&get_property_with_fallbacks
&get_inherited_property
&get_property_from_tags
&get_inherited_property_from_tags
&get_matching_regexp_property_from_tags
&get_inherited_property_from_categories_tags
&get_inherited_properties
&get_tags_grouped_by_property
Expand Down Expand Up @@ -386,6 +389,89 @@ sub get_inherited_property ($tagtype, $canon_tagid, $property) {
return;
}

=head2 get_property_from_tags ($tagtype, $tags_ref, $property)
Return the value of a property for the first tag of a list that has this property.
=head3 Parameters
=head4 $tagtype
=head4 $tags_ref Reference to a list of tags
=head4 $property
=cut

sub get_property_from_tags ($tagtype, $tags_ref, $property) {

my $value;
if (defined $tags_ref) {
foreach my $tagid (@$tags_ref) {
$value = get_property($tagtype, $tagid, $property);
last if $value;
}
}
return $value;
}

=head2 get_inherited_property_from_tags ($tagtype, $tags_ref, $property)
Return the value of an inherited property for the first tag of a list that has this property.
=head3 Parameters
=head4 $tagtype
=head4 $tags_ref Reference to a list of tags
=head4 $property
=cut

sub get_inherited_property_from_tags ($tagtype, $tags_ref, $property) {

my $value;
if (defined $tags_ref) {
foreach my $tagid (@$tags_ref) {
$value = get_inherited_property($tagtype, $tagid, $property);
last if $value;
}
}
return $value;
}

=head2 get_matching_regexp_property_from_tags ($tagtype, $tags_ref, $property, $regexp)
Return the value of a property for the first tag of a list that has this property that matches the regexp.
=head3 Parameters
=head4 $tagtype
=head4 $tags_ref Reference to a list of tags
=head4 $property
=head4 $regexp
=cut

sub get_matching_regexp_property_from_tags ($tagtype, $tags_ref, $property, $regexp) {

my $matching_value;
if (defined $tags_ref) {
foreach my $tagid (@$tags_ref) {
my $value = get_property($tagtype, $tagid, $property);
if ((defined $value) and ($value =~ /$regexp/)) {
$matching_value = $value;
last;
}
}
}
return $matching_value;
}

=head2 get_inherited_property_from_categories_tags ($product_ref, $property) {
Iterating from the most specific category, try to get a property for a tag by exploring the taxonomy (using parents).
Expand All @@ -402,18 +488,13 @@ The property if found.
=cut

sub get_inherited_property_from_categories_tags ($product_ref, $property) {
my $category_match;

if ((defined $product_ref->{categories_tags}) and (scalar @{$product_ref->{categories_tags}} > 0)) {

# Start with most specific category first
foreach my $category (reverse @{$product_ref->{categories_tags}}) {

$category_match = get_inherited_property("categories", $category, $property);
last if $category_match;
}
if (defined $product_ref->{categories_tags}) {
# We reverse the list of categories in order to have the most specific categories first
return get_inherited_property_from_tags("categories", [reverse @{$product_ref->{categories_tags}}], $property);
}
return $category_match;

return;
}

=head2 get_inherited_properties ($tagtype, $canon_tagid, $properties_names_ref, $fallback_lcs = ["xx", "en"]) {
Expand Down
1 change: 1 addition & 0 deletions stop_words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ naturel
nd
NGO
NGINX
NNS
nodejs
nutri
Nutri
Expand Down
Loading

0 comments on commit 691627f

Please sign in to comment.