Skip to content

Commit

Permalink
feat: on the pro platform, detect Nutri-Score 2023 improvement opport…
Browse files Browse the repository at this point in the history
…unities (#10217)
  • Loading branch information
stephanegigandet authored May 13, 2024
1 parent b783bf0 commit 0b5e927
Show file tree
Hide file tree
Showing 47 changed files with 669 additions and 24 deletions.
3 changes: 2 additions & 1 deletion lib/ProductOpener/Food.pm
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ BEGIN {
&is_fat_oil_nuts_seeds_for_nutrition_score
&is_water_for_nutrition_score
&compute_nutriscore
&check_availability_of_nutrients_needed_for_nutriscore
&compute_nutriscore_data
&compute_nutriscore
&compute_nova_group
&compute_serving_size_data
Expand Down
32 changes: 17 additions & 15 deletions lib/ProductOpener/Nutriscore.pm
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ BEGIN {
&get_value_with_one_less_negative_point
&get_value_with_one_more_positive_point
&get_value_with_one_less_negative_point_2023
&get_value_with_one_more_positive_point_2023
); # symbols to export on request
%EXPORT_TAGS = (all => [@EXPORT_OK]);
Expand Down Expand Up @@ -117,14 +119,19 @@ sub compute_nutriscore_score_and_grade ($nutriscore_data_ref, $version = "2021")

# methods returning the 2021 version for now, to ease switch, later on.
sub get_value_with_one_less_negative_point ($nutriscore_data_ref, $nutrient) {

return get_value_with_one_less_negative_point_2021($nutriscore_data_ref, $nutrient);
}

sub get_value_with_one_more_positive_point ($nutriscore_data_ref, $nutrient) {

return get_value_with_one_more_positive_point_2021($nutriscore_data_ref, $nutrient);
}

sub compute_nutriscore_grade ($nutrition_score, $is_beverage, $is_water) {
sub compute_nutriscore_grade ($nutrition_score, $is_beverage, $is_water, $version = "2021") {
if ($version eq "2023") {
return compute_nutriscore_grade_2023($nutrition_score, $is_beverage, $is_water);
}
return compute_nutriscore_grade_2021($nutrition_score, $is_beverage, $is_water);
}

Expand Down Expand Up @@ -573,7 +580,7 @@ my %points_thresholds_2023 = (
proteins_beverages => [1.2, 1.5, 1.8, 2.1, 2.4, 2.7, 3.0], # g / 100g
);

=head2 get_value_with_one_less_negative_point_2023( $nutriscore_data_ref, $nutrient )
=head2 get_value_with_one_less_negative_point_2023 ($is_beverage, $nutrient, $current_value)
For a given Nutri-Score nutrient value, return the highest smaller value that would result in less negative points.
e.g. for a sugars value of 15 (which gives 3 points), return 13.5 (which gives 2 points).
Expand All @@ -584,11 +591,10 @@ Return undef if the input nutrient value already gives the minimum amount of poi
=cut

sub get_value_with_one_less_negative_point_2023 ($nutriscore_data_ref, $nutrient) {
sub get_value_with_one_less_negative_point_2023 ($is_beverage, $nutrient, $current_value) {

my $nutrient_threshold_id = $nutrient;
if ( (defined $nutriscore_data_ref->{is_beverage})
and ($nutriscore_data_ref->{is_beverage})
if ($is_beverage
and (defined $points_thresholds_2023{$nutrient_threshold_id . "_beverages"}))
{
$nutrient_threshold_id .= "_beverages";
Expand All @@ -597,18 +603,15 @@ sub get_value_with_one_less_negative_point_2023 ($nutriscore_data_ref, $nutrient
my $lower_threshold;

foreach my $threshold (@{$points_thresholds_2023{$nutrient_threshold_id}}) {
# The saturated fat ratio table uses the greater or equal sign instead of greater
if ( (($nutrient eq "saturated_fat_ratio") and ($nutriscore_data_ref->{$nutrient . "_value"} >= $threshold))
or (($nutrient ne "saturated_fat_ratio") and ($nutriscore_data_ref->{$nutrient . "_value"} > $threshold)))
{
if ($current_value > $threshold) {
$lower_threshold = $threshold;
}
}

return $lower_threshold;
}

=head2 get_value_with_one_more_positive_point_2023( $nutriscore_data_ref, $nutrient )
=head2 get_value_with_one_more_positive_point_2023 ($is_beverage, $nutrient, $current_value)
For a given Nutri-Score nutrient value, return the smallest higher value that would result in more positive points.
e.g. for a proteins value of 2.0 (which gives 1 point), return 3.3 (which gives 2 points)
Expand All @@ -619,11 +622,10 @@ Return undef if the input nutrient value already gives the maximum amount of poi
=cut

sub get_value_with_one_more_positive_point_2023 ($nutriscore_data_ref, $nutrient) {
sub get_value_with_one_more_positive_point_2023 ($is_beverage, $nutrient, $current_value) {

my $nutrient_threshold_id = $nutrient;
if ( (defined $nutriscore_data_ref->{is_beverage})
and ($nutriscore_data_ref->{is_beverage})
if ($is_beverage
and (defined $points_thresholds_2023{$nutrient_threshold_id . "_beverages"}))
{
$nutrient_threshold_id .= "_beverages";
Expand All @@ -632,7 +634,7 @@ sub get_value_with_one_more_positive_point_2023 ($nutriscore_data_ref, $nutrient
my $higher_threshold;

foreach my $threshold (@{$points_thresholds_2023{$nutrient_threshold_id}}) {
if ($nutriscore_data_ref->{$nutrient . "_value"} < $threshold) {
if ($current_value < $threshold) {
$higher_threshold = $threshold;
last;
}
Expand All @@ -643,7 +645,7 @@ sub get_value_with_one_more_positive_point_2023 ($nutriscore_data_ref, $nutrient
my $return_value = $higher_threshold;

if ($return_value) {
if ($nutrient eq "fruits_vegetables_nuts_colza_walnut_olive_oils") {
if ($nutrient eq "fruits_vegetables_legumes") {
$return_value += 1;
}
else {
Expand Down
149 changes: 142 additions & 7 deletions lib/ProductOpener/ProducersFood.pm
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ BEGIN {
@EXPORT_OK = qw(
&detect_possible_improvements
&detect_possible_improvements_nutriscore
); # symbols to export on request
%EXPORT_TAGS = (all => [@EXPORT_OK]);
Expand All @@ -47,12 +48,13 @@ BEGIN {
use ProductOpener::Config qw(:all);
use ProductOpener::Store qw(:all);
use ProductOpener::Tags qw(:all);
use ProductOpener::Food qw(%categories_nutriments_per_country);
use ProductOpener::Nutriscore
qw(compute_nutriscore_score_and_grade get_value_with_one_less_negative_point get_value_with_one_more_positive_point);
use ProductOpener::Food
qw(%categories_nutriments_per_country check_availability_of_nutrients_needed_for_nutriscore compute_nutriscore_data);
use ProductOpener::Nutriscore qw(:all);

use Log::Any qw($log);
use Storable qw(dclone);
use Data::DeepAccess qw(deep_get deep_set);

=head1 FUNCTIONS
Expand All @@ -73,16 +75,35 @@ sub detect_possible_improvements ($product_ref) {
return;
}

=head2 detect_possible_improvements_nutriscore( PRODUCT_REF )
=head2 detect_possible_improvements_nutriscore( $product_ref, $version )
Detect products that can get a better NutriScore grade with a slight variation
of nutrients like sugar, salt, saturated fat, fiber, proteins etc.
=cut

sub detect_possible_improvements_nutriscore ($product_ref) {
sub detect_possible_improvements_nutriscore ($product_ref, $version = 2023) {

$log->debug("detect_possible_improvements_nutriscore - start") if $log->debug();
if ($version == 2021) {
detect_possible_improvements_nutriscore_2021($product_ref);
}
else {
detect_possible_improvements_nutriscore_2023($product_ref);
}

return;
}

=head2 detect_possible_improvements_nutriscore_2021 ( $product_ref )
Detect products that can get a better NutriScore grade with a slight variation
of nutrients like sugar, salt, saturated fat, fiber, proteins etc.
=cut

sub detect_possible_improvements_nutriscore_2021 ($product_ref) {

$log->debug("detect_possible_improvements_nutriscore_2021 - start") if $log->debug();

return if not defined $product_ref->{nutriscore_data};

Expand All @@ -96,7 +117,7 @@ sub detect_possible_improvements_nutriscore ($product_ref) {
my $new_nutriscore_data_ref = dclone($product_ref->{nutriscore_data});
$new_nutriscore_data_ref->{$nutrient} = $lower_value;
my ($new_nutriscore_score, $new_nutriscore_grade)
= ProductOpener::Food::compute_nutriscore_score_and_grade($new_nutriscore_data_ref);
= ProductOpener::Food::compute_nutriscore_score_and_grade($new_nutriscore_data_ref, 2023);

# Store the result of the experiment
$product_ref->{nutriscore_data}{$nutrient . "_lower"} = $lower_value;
Expand Down Expand Up @@ -192,6 +213,120 @@ sub detect_possible_improvements_nutriscore ($product_ref) {
return;
}

=head2 detect_possible_improvements_nutriscore_2023 ( $product_ref )
Detect products that can get a better NutriScore grade with a slight variation
of nutrients like sugar, salt, saturated fat, fiber, proteins etc.
=cut

sub detect_possible_improvements_nutriscore_2023 ($product_ref) {

my $version = 2023;

$log->debug("detect_possible_improvements_nutriscore_2023 - start") if $log->debug();

my $nutriscore_ref = $product_ref->{nutriscore}{$version};

return if not((defined $nutriscore_ref) and ($nutriscore_ref->{nutriscore_computed}));

# Go through negative and positive components of the Nutri-Score
foreach my $component_type ("negative", "positive") {

my $components_array_ref = deep_get($nutriscore_ref, "data", "components", $component_type);

next if not defined $components_array_ref; # should not happen as we already check nutriscore_computed

foreach my $component_ref (@{$nutriscore_ref->{data}{components}{$component_type}}) {

# Reduce negative nutrients or increase positive nutrients
my $nutrient = $component_ref->{id};
my $current_value = $component_ref->{value};

# If the current value is undef, don't try to change it
next if not defined $current_value;

my $new_value;
if ($component_type eq "negative") {
$new_value = get_value_with_one_less_negative_point_2023($nutriscore_ref->{is_beverage},
$nutrient, $current_value);
}
# Only try to increase positive components if their value is not 0
elsif ($current_value > 0) {
$new_value = get_value_with_one_more_positive_point_2023($nutriscore_ref->{is_beverage},
$nutrient, $current_value);
}

# Compute the new Nutri-Score if we have a new value
if (defined $new_value) {

# Use a copy of the product_ref to avoid modifying the original data
my $new_product_ref = dclone($product_ref);

# Populate the data structure that will be passed to Food::Nutriscore
my ($nutrients_available, $prepared, $nutriments_field)
= check_availability_of_nutrients_needed_for_nutriscore($new_product_ref);

# Skip products with estimated nutrients
next if $nutriments_field eq "nutriments_estimated";

my $new_nutriscore_ref
= compute_nutriscore_data($new_product_ref, $prepared, $nutriments_field, $version);

# Overwrite the value of the nutrient in the Nutri-Score data
$new_nutriscore_ref->{$nutrient} = $new_value;

# Compute the new Nutri-Score
my ($new_nutriscore_score, $new_nutriscore_grade)
= ProductOpener::Food::compute_nutriscore_score_and_grade($new_nutriscore_ref, $version);

# Store the result of the experiment (useful for debugging, kept only on the producers platform)
$component_ref->{"new_value"} = $new_value;
$component_ref->{"new_score"} = $new_nutriscore_score;
$component_ref->{"new_grade"} = $new_nutriscore_grade;

# If the new grade is better, store the improvement
if ($new_nutriscore_grade lt $nutriscore_ref->{grade}) {

$component_ref->{"current_grade"} = $nutriscore_ref->{grade};
$component_ref->{"current_score"} = $nutriscore_ref->{score};

my $difference = abs($current_value - $new_value);
my $difference_percent = $difference / $current_value * 100;

my $more_or_less = ($component_type eq "negative") ? "less" : "more";

my $improvements_tag;

if ($difference_percent <= 5) {
$improvements_tag = "en:better-nutri-score-with-slightly-$more_or_less-" . $nutrient;
}
elsif ($difference_percent <= 10) {
$improvements_tag = "en:better-nutri-score-with-$more_or_less-" . $nutrient;
}
elsif ($difference_percent <= 20) {
$improvements_tag = "en:better-nutri-score-with-much-$more_or_less-" . $nutrient;
}

if ($improvements_tag) {
push @{$product_ref->{improvements_tags}}, $improvements_tag;
$product_ref->{improvements_data}{$improvements_tag} = {
current_nutriscore_grade => $nutriscore_ref->{grade},
new_nutriscore_grade => $new_nutriscore_grade,
nutrient => $nutrient,
current_value => $current_value,
new_value => $new_value,
difference_percent => $difference_percent,
};
}
}
}
}
}

return;
}

=head2 detect_possible_improvements_compare_nutrition_facts( PRODUCT_REF )
Compare the nutrition facts to other products of the same category to try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,20 +219,29 @@
"negative" : [
{
"id" : "energy",
"new_grade" : "e",
"new_score" : 17,
"new_value" : 3350,
"points" : 10,
"points_max" : 10,
"unit" : "kJ",
"value" : 3378
},
{
"id" : "sugars",
"new_grade" : "e",
"new_score" : 17,
"new_value" : 17,
"points" : 10,
"points_max" : 10,
"unit" : "g",
"value" : 20
},
{
"id" : "saturated_fat",
"new_grade" : "e",
"new_score" : 16,
"new_value" : 4,
"points" : 4,
"points_max" : 10,
"unit" : "g",
Expand All @@ -256,13 +265,19 @@
"positive" : [
{
"id" : "proteins",
"new_grade" : "e",
"new_score" : 17,
"new_value" : 7.3,
"points" : 7,
"points_max" : 7,
"unit" : "g",
"value" : 5
},
{
"id" : "fiber",
"new_grade" : "e",
"new_score" : 16,
"new_value" : 3.1,
"points" : 0,
"points_max" : 5,
"unit" : "g",
Expand Down
Loading

0 comments on commit 0b5e927

Please sign in to comment.