This was posted originally at my WordPress blog,
here.
On July 6, 2009, I weighed 252 pounds, fasting and naked. That evening, I joined Weight Watchers.
On December 12, 2009, I weighed 177.8 pounds, fasting and fully clothed, one kilogram below my ultimate weight goal of 180 pounds.
Through this entire time, I was using their POINTS system, which helps you determine how much to eat. In fact, I’m still using it. At your first meeting, you calculate a daily POINTS value based on your weight, age, and some other information. This value goes down as you lose weight. When you’re done losing weight, you adjust it to a value where you are neither losing nor maintaining.
Every food item also has a POINTS value: a whole number calculated based on how many dietary calories, how much fiber, and how much fat is in a serving of that food. Since all this information must be made available to consumers in the United States in these forms, it’s quite trivial to calculate the number of POINTS a food item has in it. As you eat food, you write it down and subtract its POINTS value from your running daily tally. If you go over, you deduct the difference from a weekly allowance of 35 POINTS. If you stick within that, you will lose weight. You can add to that 35 POINTS by exercising1.
Weight Watchers sells calculators to help you determine the POINTS value of various food items. Their current model only calculates and tracks POINTS, but previous calculators were full four function calculators.
Today, I finally got around to sitting down to reverse engineering the algorithm with of Gvim and Python and the assistance of a current model POINTS calculator (which I carry with me from the time I get dressed until the time I shower).
Ultimately, the following should pass:
#!/usr/bin/env python
import unittest
import points
class PointsTestFunctions(unittest.TestCase):
...
def testPoints(self):
#Real points values, based on packaging)
#Roasted Almond Sensation bars, 2 points
self.assertEqual(points.points(calories = 120, fat = 7, fiber = 9), 2)
#French Vanilla smoothie, made with water, 1 point
self.assertEqual(points.points(calories = 100, fat = 1, fiber = 4), 1)
#French Vanilla smoothie, made with skim milk), 3 points
self.assertEqual(points.points(calories = 180, fat = 1, fiber = 4), 3)
#Mint cookie crisp mini bars, 1 point
self.assertEqual(points.points(calories = 70, fat = 2, fiber = 3), 1)
These are all food items from Weight Watchers own product line, and come with their points value labeled. I took the nutritional information from the packaging (as I said, it’s required in the US). However, as with all algorithms, there are edge cases. For example, what does the algorithm do if I say that a food item has no calories, but a non-zero fiber or fat count2? Turning to my calculator, I ask it for the POINTS value of a food item that has 4 grams of fiber, but no calories or fat. The calculator returns 0. Presumably, it would return the same for any fewer grams of fiber, as the fiber content of a food up to four grams decreases its effective calories for calculating POINTS (as anyone who looks at the slide rule in the Weight Watchers Pocket Guide can tell you). Any fiber past 4 grams doesn’t count. According to my reverse engineered algorithm, this food item of unobtainium would actually net -1 POINTS. However, the calculator is unable to handle negative numbers except for your current POINTS count. However, since we’re not going to deviate from what the calculator said, we need to add this line for the edge case:
self.assertEqual(points.points(calories = 0, fat = 0, fiber = 4), 0)
What about fat? If we have a food with no fiber or calories, but, say, 5 grams of fat, the food has 0 POINTS. However, if we add a gram of fat, we get a food item that has a POINTS value of 1. Thus, we add these lines to the test as well
#6 grams of fat = 54 calories, 2 points
self.assertEqual(points.points(calories = 0, fat = 6, fiber = 0), 1)
#5 grams of fat = 45 calories, 1 point
self.assertEqual(points.points(calories = 0, fat = 5, fiber = 0), 1)
Thus, the test that must pass looks like this:
def testPoints(self):
#Real points values, based on packaging)
#Roasted Almond Sensation bars, 2 points
self.assertEqual(points.points(calories = 120, fat = 7, fiber = 9), 2)
#French Vanilla smoothie, made with water, 1 point
self.assertEqual(points.points(calories = 100, fat = 1, fiber = 4), 1)
#French Vanilla smoothie, made with skim milk), 3 points
self.assertEqual(points.points(calories = 180, fat = 1, fiber = 4), 3)
#Mint cookie crisp mini bars, 1 point
self.assertEqual(points.points(calories = 70, fat = 2, fiber = 3), 1)
#Absurdities in the points system
#These things aren't possible
#6 grams of fat = 54 calories, 2 points
self.assertEqual(points.points(calories = 0, fat = 6, fiber = 0), 1)
#5 grams of fat = 45 calories, 1 point
self.assertEqual(points.points(calories = 0, fat = 5, fiber = 0), 0)
#4 grams of fiber = 16 calories, 0 points
self.assertEqual(points.points(calories = 0, fat = 0, fiber = 4), 0)
I have a Python module that passes this test. Without posting code that will get me a takedown notice, here’s the function itself:
def points(calories, fiber, fat):
points = int(round(rawPoints(calMod(calories, fiber)) + fatMod(fat)))
if points < 0:
points = 0
return points
Other things to note that were obtained from the exercise: you'll note that the core formula, rawPoints(calmod(calories, fiber)) + fatMod(fat)3 returns something that can be rounded to 0 decimal places. This is because both values its adding are the result of true division. An observant Weight Watchers member with the Pocket Guide slide rule will note where this occurs.
Numerous other tests were created to get to this point. These tests correspond to functions that weed out type and value errors.
ETA: I wanted to say one other thing. Someone once asked me if a food could have a negative POINTS value. The answer to this question is no, though just barely. Exactly 4 grams of undigestable carbohydrates (a form of fiber, 16 dietary calories and obviously 0 grams of fat) would produce the lowest POINTS value possible. The fiber offset is not quite large enough to make it a negative POINTS food.
1. This is a simplified version of the POINTS system for the purposes of explaining the code that follows. A full explanation can be found at a Weight Watchers meeting near you.
2. This isn't physically possible. Any form of fiber has 4 dietary calories per gram, and fats have 9 dietary calories per gram. As such, don't go hunting for such foods: they don't actually exist. However, the algorithm neither knows nor cares about chemistry: it's just a bit of math.
3. This formula isn't quite what I have. Nor is their simplified version.
DISCLAIMER: This post references multiple trademarks, some of which may be registered. I don't own any of them. I don't claim ownership of any patented algorithms, either, which I believe the POINTS algorithm is.