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:

  1. 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.
  2. 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.

Information, not medical advice.