Home · Open source · Writing · Contact

02 · Research code, offline study

Handful

A handful of recipes, not a thousand.

Research by Khushi Bag, implementation by Rish Sadh

MIT licence · Python 3.11+ · Offline study

Open the project page · Repository on GitHub

Personalised recipe shortlisting using linear programming: an offline study of Food.com ratings.

Given what a Food.com user rated highly before a cutoff date, choose a shortlist of six recipes that maximises estimated relevance while keeping the shortlist spread across preparation-time bands. Then measure, offline, whether that constraint costs anything in retrieval of the recipes the user actually went on to rate highly.

Secondary data only. No surveys, no recruited participants, no primary data collection, no real food ordering, and no invented research data.

The method in one page

  • Relevance. A frozen sentence-transformers/all-MiniLM-L6-v2 encoder turns each recipe's text (name, ingredients, whitelisted descriptive tags, short description) into a unit vector. A user's profile is the normalised mean of the vectors of the recipes they rated 4 or 5 before the cutoff. The score is s(u,i) = (1 + cos(p_u, e_i)) / 2, which lands in [0, 1]. It estimates semantic relevance. It is not a predicted rating, a probability, or a satisfaction estimate.
  • Selection. A linear programme over the eligible unseen candidates. There are four disjoint preparation-time bands from reported minutes: 0 to 15, 15 to 30, 30 to 60 and over 60, upper-inclusive.
maximise    Σ_{i ∈ C_u} s_ui · x_i
subject to  Σ_{i ∈ C_u} x_i = k                      k = 6
            Σ_{i ∈ G_g} x_i ≤ b_g   for each band   b = 3
            0 ≤ x_i ≤ 1

Why continuous variables suffice. The bands partition the candidates, so the constraint matrix is that of a min-cost flow with integer supplies and capacities, whose basic optimal solutions are integral. Every solve verifies this rather than assuming it, and is cross-checked against exhaustive enumeration, a greedy oracle, and the identity that with b_g = k the LP optimum equals the unconstrained top-k sum.

The honest claim. A score-sorted, capacity-respecting greedy selection also solves this restricted case. The LP is not claimed to be computationally necessary. Its contribution is a transparent formulation, an exact feasibility condition, a certified optimum and an interpretable dual.

What was found

The final evaluation used the 2018 labels and ran once, on 11 September 2026, after the configuration was frozen. It is recorded as run final-20260911T120041Z-5af9c102.

20,557 users had rated at least five distinct recipes 4 or 5 before 1 January 2018. Of those, 582 rated something in the 2018 label window, and 530 rated at least one eligible unseen recipe 4 or 5 in it. Those 530 users are the evaluated population. Each was offered a shortlist of six recipes drawn from an average of about 230,000 eligible candidates.

The popularity baseline retrieved more of the recipes users went on to rate highly than the personalised method did. Personalised relevance with the preparation time cap retrieved almost nothing.

Final evaluation results for four methods, 530 users
MethodRecall@6NDCG@6Users with a hitMean bands represented (of 4)Mean cap violations
Random (feasible, capped)0.00000.00000 of 5303.340.00
Popularity, capped LP0.00850.004910 of 5302.920.00
AI relevance, top 6, no cap0.00000.00061 of 5302.860.51
AI relevance, capped LP (proposed)0.00000.00061 of 5303.070.00

Recall@6 for the proposed method is 0.00003 before rounding: one user out of 530 had one of six recipes match a later rating. Random was averaged over 20 draws per user and never matched.

Paired differences, proposed method minus popularity, with a 95% bootstrap interval over 2,000 resamples of users (seed 42): Recall@6 is -0.0084, interval [-0.0150, -0.0029]. NDCG@6 is -0.0043, interval [-0.0081, -0.0010]. Neither interval includes zero, so within this benchmark the popularity baseline did better on both metrics. It is a near-zero result for the proposed method, not evidence that it works.

What the band cap did. It raised mean band representation from 2.86 to 3.07 bands and removed the 0.51 cap violations per shortlist that uncapped top-6 produces. Because recall sits at the floor for both, the identical scores show only that the cap did not lose a hit that was there. They do not show that the cap is free in general.

How much weight this can bear

The differences rest on very few users: 10 users with a hit for popularity, 1 for the proposed method, out of 530. Absolute recall is tiny for every method.

The bootstrap resamples users as if they were independent. They are not: users share many of the same recipes, so the intervals understate the real uncertainty. They describe resampling within this benchmark and nothing wider.

No p-values are reported, by design. This is one platform, one year of labels, and only users who already had history. It says nothing about consumers in general.

What it can and cannot conclude

It can report retrieval of later positive ratings, ranking quality, preparation-time band representation, constraint compliance, feasibility, machine runtime, and sensitivity to k and b.

It cannot report, and must never claim, reduced choice overload, reduced stress, greater confidence, less regret, faster human decisions, better meal satisfaction, better nutrition, allergy safety, clinical outcomes, or anything about consumers in general. Choice overload is the motivation for shortlisting. It is not an outcome this study observed.

Run it

Python 3.11 or later (built and tested on 3.12).

python tasks.py setup      # creates .venv and installs the core stack
python tasks.py smoke      # end-to-end run on the synthetic fixture
python tasks.py test       # the test suite
python tasks.py demo       # launch the local Streamlit demo

torch and sentence-transformers are about 2.5 GB. They are not needed for the test suite, which uses a deterministic hash-based test encoder, but they are required for any real scoring run (python tasks.py setup-ai).

Every research run records its configuration, dataset file hashes, git revision, package versions, model revision and seeds under runs/<run_id>/, and every table and figure it reports carries the run id that produced it.

Licence

Code: MIT. The Food.com dataset is not included and is not covered by this licence: Kaggle lists it as "Data files (c) Original Authors", so each user downloads it themselves.

Also on Reidify: reidify.design/open-source/handful/

By Rish Sadh, founder of Reidify, an AI-first design studio in Mumbai.