Two things from the same bakery counter, run through the same formula in the FatTonic Food Index:
- Croissant (1 medium, 60 g): 39 / 100, band Poor.
- Bagel (1 plain, 100 g): 1 / 100, band Avoid. The same score as a can of cola.
The croissant has six times the fat. So why does it win?
The formula
The score is one line, published with the data:
GL = GI × (carbs − fiber) ÷ 100
Score = 100 − 2.2·GL − 0.8·sugar − 8·processing + 1.6·fiber + 0.7·protein
Amounts are per serving, in grams; processing is 0–3; the result is kept within 1–100. Fat is not in it. The score asks one question: how hard does a serving's carbohydrate land?
The two rows
| Croissant | Bagel | |
|---|---|---|
| Serving | 60 g | 100 g |
| Carbs / fiber (g) | 27 / 1 | 52 / 2 |
| Glycemic index | 67 | 72 |
| Glycemic load | 17 | 36 |
| Sugar (g) | 6 | 8 |
| Protein (g) | 5 | 10 |
| Processing | 3 | 3 |
| Score | 39 | 1 |
The bagel loses on one line. A glycemic load of 36 costs it 79.2 points before anything else is counted. Its extra protein (+7.0) can't pay that back. The croissant's load of 17 costs 37.4.
Two caveats:
- Serving size does a lot of the work. A bagel serving here is 100 g; a croissant is 60 g. Score both per 100 g and the croissant drops to about 13; the gap narrows from 38 points to about 12. We score the serving people actually eat.
- The glycemic index values are third-party. For 163 of the 200 foods the per-food GI source is not yet recorded, and the data page says so. The nutrients are not guesses: 194 of 200 scored foods are checked against a named USDA FoodData Central record (the bagel is record 174899, the croissant 174987).
And one thing the score is not: a verdict on whether a food is good for you. A croissant is still butter and white flour. It just carries half the glycemic load of a bagel, per serving.
Check it yourself
import csv, io, urllib.request
url = "https://fattonic.com/data/export/foods.csv"
rows = csv.DictReader(io.TextIOWrapper(urllib.request.urlopen(url), "utf-8"))
def score(r):
gl = round(int(r["gi"]) * (int(r["carbs_g"]) - int(r["fiber_g"])) / 100)
s = (100 - 2.2 * gl - 0.8 * int(r["sugar_g"]) - 8 * int(r["processed"])
+ 1.6 * int(r["fiber_g"]) + 0.7 * int(r["protein_g"]))
return max(1, min(100, round(s)))
for r in rows:
if r["name"] in ("Croissant", "Bagel", "Cola / Soda", "Sourdough Bread"):
print(f'{r["name"]:<16} GL {r["gl"]:>3} published {r["score"]:>3} recomputed {score(r):>3}')
It recomputes the published score from the published inputs. Sourdough, if you were wondering, scores 61 a slice.
- Every food, its USDA record and its score: fattonic.com/foods
- The formula, the sources and the downloads (CSV, JSON, CC BY 4.0): fattonic.com/data
- How a whole day of eating scores, free: the Metabolic Score
Information, not medical advice.





