Skip to content

Commit

Permalink
feat: Eco-Score improvements and change of scale: A+ to F (#10829)
Browse files Browse the repository at this point in the history
The Eco-Score has an improved formula:

https://docs.score-environnemental.com/more/changelog#id-19-septembre-2024

There are 2 small changes:
- Previously products that had a non recycled and non non recyclable
packaging were capped to Eco-Score B at best. This cap has been removed.
- The country enviromental product index bonus/malus is now only applied
if there isn't already a bonus for the production system.

And there is a bigger more visible change:
- the scale is now from A+ to F, instead of A to E, in order to better
differentiate products (to avoid having most of the products of the same
category have the same Eco-Score).

Note: this is a draft. In particular, the images for the new A+ and F
grades are for testing purposes only.

To ease testing of mobile apps etc. this PR has been deployed to the
world.openfoodfacts.dev server (login and password: off)
  • Loading branch information
stephanegigandet authored Oct 21, 2024
1 parent edb6c13 commit 87df665
Show file tree
Hide file tree
Showing 137 changed files with 8,820 additions and 5,059 deletions.
618 changes: 618 additions & 0 deletions html/images/attributes/src/ecoscore-a-plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,228 changes: 917 additions & 311 deletions html/images/attributes/src/ecoscore-a.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,228 changes: 917 additions & 311 deletions html/images/attributes/src/ecoscore-b.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,228 changes: 917 additions & 311 deletions html/images/attributes/src/ecoscore-c.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,228 changes: 917 additions & 311 deletions html/images/attributes/src/ecoscore-d.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,228 changes: 917 additions & 311 deletions html/images/attributes/src/ecoscore-e.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions html/images/attributes/src/ecoscore-f.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 12 additions & 17 deletions lib/ProductOpener/Attributes.pm
Original file line number Diff line number Diff line change
Expand Up @@ -637,13 +637,7 @@ The return value is a reference to the resulting attribute data structure.
=head4 % Match
To differentiate products more finely, the match is based on the Eco-Score score
that is used to define the Eco-Score grade from A to E.
- Eco-Score A: 80 to 100
- Eco-Score B: 60 to 79
- Eco-Score C: 40 to 59
- Eco-Score D: 20 to 39
- Eco-Score E: 0 to 19
that is used to define the Eco-Score grade from A+ to F.
=cut

Expand Down Expand Up @@ -675,13 +669,8 @@ sub compute_attribute_ecoscore ($product_ref, $target_lc, $target_cc) {
if $log->is_debug();

# Compute match based on score

my $match = 0;

# Score ranges from 0 to 100 with some maluses and bonuses that can be added
# Warning: a Eco-Score score of 20 means D grade for the Eco-Score, but a match of 20 is E grade for the attributes
# So we add 1 to the Eco-Score score to compute the match.
$match = $score + 1;
# Score ranges from 0 to 100 with some maluses and bonuses that can be added or subtracted
my $match = $score;

if ($score < 0) {
$match = 0;
Expand All @@ -693,12 +682,18 @@ sub compute_attribute_ecoscore ($product_ref, $target_lc, $target_cc) {
$attribute_ref->{match} = $match;

if ($target_lc ne "data") {
my $letter_grade = uc($grade); # A+, A, B, C, D, E, F
my $grade_underscore = $grade;
$grade_underscore =~ s/\-/_/; # a-plus -> a_plus
if ($grade eq "a-plus") {
$letter_grade = "A+";
}
$attribute_ref->{title}
= sprintf(lang_in_other_lc($target_lc, "attribute_ecoscore_grade_title"), uc($grade));
= sprintf(lang_in_other_lc($target_lc, "attribute_ecoscore_grade_title"), $letter_grade);
$attribute_ref->{description}
= lang_in_other_lc($target_lc, "attribute_ecoscore_" . $grade . "_description");
= lang_in_other_lc($target_lc, "attribute_ecoscore_" . $grade_underscore . "_description");
$attribute_ref->{description_short}
= lang_in_other_lc($target_lc, "attribute_ecoscore_" . $grade . "_description_short");
= lang_in_other_lc($target_lc, "attribute_ecoscore_" . $grade_underscore . "_description_short");
}
$attribute_ref->{icon_url} = "$static_subdomain/images/attributes/dist/ecoscore-$grade.svg";
}
Expand Down
50 changes: 40 additions & 10 deletions lib/ProductOpener/Display.pm
Original file line number Diff line number Diff line change
Expand Up @@ -2038,6 +2038,17 @@ sub display_list_of_tags ($request_ref, $query_ref) {
$stats{all_tags_products} += $count;
}

# For the Eco-Score, we want to display A+ before A even though A+ is after A in alphabetical order
# If the tagid "a" is followed by tagid "a-plus", invert them
if (($tagtype eq 'ecoscore') and (defined $tags[1])) {

if (($tags[0]{_id} eq 'a') and ($tags[1]{_id} eq 'a-plus')) {
my $tags_tmp = $tags[0];
$tags[0] = $tags[1];
$tags[1] = $tags_tmp;
}
}

foreach my $tagcount_ref (@tags) {

$i++;
Expand Down Expand Up @@ -2198,9 +2209,12 @@ sub display_list_of_tags ($request_ref, $query_ref) {

my $tag_link = $main_link . $link;

$html .= "<tr><td>";
$html .= "<tr>";

my $display = '';
# For Eco-Score, we add a data-sort attribute to sort the A+ grade before the A grade in Datatables.js
my $data_sort;

my @sameAs = ();
if ($tagtype eq 'nutrition_grades') {
my $grade;
Expand All @@ -2214,29 +2228,37 @@ sub display_list_of_tags ($request_ref, $query_ref) {
$grade = lang("unknown");
}
$display
= "<img src=\"/images/attributes/dist/nutriscore-$tagid.svg\" alt=\"$Lang{nutrition_grade_fr_alt}{$lc} "
= "<img src=\"/images/attributes/dist/nutriscore-$tagid.svg\" alt=\"Nutri-Score "
. $grade
. "\" title=\"$Lang{nutrition_grade_fr_alt}{$lc} "
. "\" title=\"Nutri-Score "
. $grade
. "\" style=\"max-height:80px;\"> "
. $grade;
}
elsif ($tagtype eq 'ecoscore') {
my $grade;

if ($tagid =~ /^[abcde]$/) {
$grade = uc($tagid);
if ($tagid eq "a-plus") {
$grade = "A+";
$data_sort = "A+";
}
elsif ($tagid =~ /^[abcdef]$/) {
$grade = " " . uc($tagid);
$data_sort = "X-" . $grade;
}
elsif ($tagid eq "not-applicable") {
$grade = lang("not_applicable");
$data_sort = "Z";
}
else {
$grade = lang("unknown");
$data_sort = "Y";
}

$display
= "<img src=\"/images/attributes/dist/ecoscore-$tagid.svg\" alt=\"$Lang{ecoscore}{$lc} "
= "<img src=\"/images/attributes/dist/ecoscore-$tagid.svg\" alt=\"Eco-Score "
. $grade
. "\" title=\"$Lang{ecoscore}{$lc} "
. "\" title=\"Eco-Score "
. $grade
. "\" style=\"max-height:80px;\"> "
. $grade;
Expand Down Expand Up @@ -2270,6 +2292,13 @@ sub display_list_of_tags ($request_ref, $query_ref) {
$percent = ' (' . sprintf("%2.2f", $products / $stats{all_tags_products} * 100) . '%)';
}

if (defined $data_sort) {
$html .= "<td data-sort=\"$data_sort\">";
}
else {
$html .= "<td>";
}

$css_class =~ s/^\s+|\s+$//g;
$info .= ' class="' . $css_class . '"';
$html .= "<a href=\"$tag_link\"$info$nofollow>" . $display . "</a>";
Expand Down Expand Up @@ -2433,10 +2462,11 @@ HTML
}
}
elsif ($request_ref->{groupby_tagtype} eq 'ecoscore') {
$categories = "'A','B','C','D','E','" . lang("not_applicable") . "','" . lang("unknown") . "'";
$colors = "'#1E8F4E','#60AC0E','#EEAE0E','#FF6F1E','#DF1F1F','#a0a0a0','#a0a0a0'";
$categories
= "'A+','A','B','C','D','E',,'F','" . lang("not_applicable") . "','" . lang("unknown") . "'";
$colors = "'#1E8F4E','#1E8F4E','#60AC0E','#EEAE0E','#FF6F1E','#DF1F1F','#DF1F1F','#a0a0a0','#a0a0a0'";
$series_data = '';
foreach my $ecoscore_grade ('a', 'b', 'c', 'd', 'e', 'not-applicable', 'unknown') {
foreach my $ecoscore_grade ('a-plus', 'a', 'b', 'c', 'd', 'e', 'f', 'not-applicable', 'unknown') {
$series_data .= ($products{$ecoscore_grade} + 0) . ',';
}
}
Expand Down
48 changes: 22 additions & 26 deletions lib/ProductOpener/Ecoscore.pm
Original file line number Diff line number Diff line change
Expand Up @@ -849,39 +849,29 @@ sub compute_ecoscore ($product_ref) {

$product_ref->{ecoscore_data}{"scores"}{$cc} += $bonus;

# Assign A to E grade
# Assign A+ to F grade
# SI(AO3>=90;"A+";SI(AO3>=75;"A";SI(AO3>=60;"B";SI(AO3>=45;"C";SI(AO3>=30;"D";SI(AO3>=15;"E";"F"))))));"")

if ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 80) {
if ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 90) {
$product_ref->{ecoscore_data}{"grades"}{$cc} = "a-plus";
}
elsif ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 75) {
$product_ref->{ecoscore_data}{"grades"}{$cc} = "a";
}
elsif ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 60) {
$product_ref->{ecoscore_data}{"grades"}{$cc} = "b";
}
elsif ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 40) {
elsif ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 45) {
$product_ref->{ecoscore_data}{"grades"}{$cc} = "c";
}
elsif ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 20) {
elsif ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 30) {
$product_ref->{ecoscore_data}{"grades"}{$cc} = "d";
}
else {
elsif ($product_ref->{ecoscore_data}{"scores"}{$cc} >= 15) {
$product_ref->{ecoscore_data}{"grades"}{$cc} = "e";
}

# If a product has the grade A and it contains a non-biodegradable and non-recyclable material, downgrade to B
if (
($product_ref->{ecoscore_data}{"grades"}{$cc} eq "a")
and ($product_ref->{ecoscore_data}{adjustments}{packaging}
{non_recyclable_and_non_biodegradable_materials} > 0)
)
{
$product_ref->{ecoscore_data}{"downgraded"} = "non_recyclable_and_non_biodegradable_materials";
# For France, save the original score
if ($cc eq 'fr') {
$product_ref->{ecoscore_data}{"scores"}{$cc . "_orig"}
= $product_ref->{ecoscore_data}{"scores"}{$cc};
}
$product_ref->{ecoscore_data}{"grades"}{$cc} = "b";
$product_ref->{ecoscore_data}{"scores"}{$cc} = 79;
else {
$product_ref->{ecoscore_data}{"grades"}{$cc} = "f";
}

$log->debug(
Expand Down Expand Up @@ -1395,6 +1385,8 @@ $product_ref->{adjustments}{origins_of_ingredients} hash with:
- transportation_value_[country code]
- aggregated origins: sorted array of origin + percent to show the % of ingredients by country used in the computation
Note: the country EPI is not taken into account if the product already has a bonus for the production system.
=cut

sub compute_ecoscore_origins_of_ingredients_adjustment ($product_ref) {
Expand Down Expand Up @@ -1518,6 +1510,13 @@ sub compute_ecoscore_origins_of_ingredients_adjustment ($product_ref) {
{aggregated_origins => \@aggregated_origins})
if $log->is_debug();

# EPI score is not counted if we already have a bonus for the production system
# In this case, we set the EPI score to 0
if ($product_ref->{ecoscore_data}{adjustments}{production_system}{value} > 0) {
$epi_score = 0;
$epi_value = 0;
}

$product_ref->{ecoscore_data}{adjustments}{origins_of_ingredients} = {
origins_from_origins_field => \@origins_from_origins_field,
origins_from_categories => \@origins_from_categories,
Expand Down Expand Up @@ -1578,12 +1577,9 @@ sub compute_ecoscore_packaging_adjustment ($product_ref) {

my $warning;

# If we do not have packagings info, return the maximum malus, and indicate the product can contain non recyclable materials
# If we do not have packagings info, return the maximum malus
if ((not defined $product_ref->{packagings}) or (scalar @{$product_ref->{packagings}} == 0)) {
$product_ref->{ecoscore_data}{adjustments}{packaging} = {
value => -15,
non_recyclable_and_non_biodegradable_materials => 1,
};
$product_ref->{ecoscore_data}{adjustments}{packaging} = {value => -15,};
# indicate that we are missing key data
# this is to indicate to 3rd party that the computed Eco-Score should not be displayed without warnings
$product_ref->{ecoscore_data}{missing_key_data} = 1;
Expand Down
Loading

0 comments on commit 87df665

Please sign in to comment.