GraphGists

Introduction

I was recently motivated to re-imagine the BeachBody fitness programs and nutritional supplements as a graph with the goal being to recommend fitness programs, nutritional supplements and other users that would be personalized to my user persona. For those that don’t know, BeachBody is responsible for P90X, INSANITY, T25 and a plethora of others that they’ve been distributing via DVD for years.

The Project

In this recommendation demo, I utilized all publicly available data from their existing website and created several user personas to demonstrate the personalized recommendations and connected data benefits of transforming their existing data into the Neo4j graph database. I focused on providing recommendations for a few key recommendations:

  • 1. What fitness programs are best to help me accomplish my workout goals?

  • 2. Which nutritional supplements will help me achieve my eating and workout goals?

  • 3. Are there any other users in the community that I can workout with and which workout would be good for us to do together?

FitnessAndNutritionRecommendations

The Concepts

The main concepts that exist on the site today are Fitness Programs, Nutritional Supplements, Gear and Community. For the initial pass I chose to ignore Gear.

The main ideas that I could see fitting nicely between these stated concepts that would allow me to make the recommendations I as a consumer of their products and participant in their community would be interested in receiving were:

  • Workout Goals

  • Eating Goals

  • Muscle Groups

  • Body Areas

  • Workout Types

  • Workout Levels

  • Supplement Types

…​.there is one other key element when considering a fitness program and that is any Physical Limitations or Eating Restrictions I as a consumer may have.

Setup

Weighted Edges

Initially we’ll start by exploring the "importance" a user persona places on various WorkoutGoals, EatingGoals, MuscleGroups and BodyAreas, which will be used as part of the scoring algorithm.

// Order User's Values by Importance
MATCH (u:User {username: "ben"})-[r:VALUES]->(x) RETURN x.name, r.importance ORDER BY r.importance DESC;
// Order User's Desires by Importance
MATCH (u:User {username: "ben"})-[r:DESIRES]->(x) RETURN x.name, r.importance ORDER BY r.importance DESC;

Personalization

The preferences a user has are crucial to understanding enough about them to make informed recommendations. Here we start with the user "ben" and traverse one level out to all the direct information we’ve collected through our interactions with this user.

// Show User's Preferences
MATCH (u:User {username: "ben"}) WITH u MATCH (u)-[:HAS]->(pl:PhysicalLimitation), (u)-[:IS_AT]->(wl:WorkoutLevel), (u)-[:PREFERS]->(wt:WorkoutType), (u)-[:VALUES]->(ba:BodyArea), (u)-[:DESIRES]->(mg:MuscleGroup) RETURN u, pl, wl, wt, ba, mg;

Then we look at all the users with the classification nodes between them represented by 'x' From those 'x' nodes we’ll go find all the FitnessPrograms that are also connected to these x, which gives us our set of potential recommendations.

// Show All Users, Preferences/Classifcations and Fitness Programs
MATCH (u:User)-[]-(x)-[]-(u2:User)
OPTIONAL MATCH (x)-[]-(fp:FitnessProgram)
RETURN u, x, u2, fp;

Similarly we can also star with the EatingGoals and see which NutritionalSupplements and Users have those in common.

// Explore Nutritional Supplements, Eating Goals and Users
MATCH (eg:EatingGoal) WITH eg OPTIONAL MATCH (eg)-[]-(x) RETURN eg, x;

Product Cross-Selling

Here we look at which EatingGoals and WorkoutGoals would be considered parallel and from this could examine patterns about a user where they do not have a direct connection.

// Explore Parallel Eating and Workout Goals - Performance/Fat Burning
MATCH (eg:EatingGoal {name: "Performance"})-[]-(x)-[]-(wg:WorkoutGoal {name: "Fat Burning"}) RETURN eg, x, wg;
// Explore Parallel Eating and Workout Goals - Weight Loss
MATCH (eg:EatingGoal {name: "Weight Loss"})-[]-(x)-[]-(wg:WorkoutGoal {name: "Weight Loss"}) RETURN eg, x, wg;
// Explore Parallel Eating and Workout Goals - Weight Loss/Get Healthy/Wellness
MATCH (eg:EatingGoal)-[]-(x)-[]-(wg:WorkoutGoal {name: "Weight Loss"}) WHERE eg.name = "Get Healthy" OR eg.name = "Wellness" RETURN eg, x, wg;

The Recommendations: Recommend User a Fitness Program

We start simple by recommending a User their top five fitness programs.

  1. The User is our starting node and from there we optionally pull in any physical limitations to use as an exclusion in our scoring.

  2. We then go find all the preferences with the importance weighting.

  3. Next we need to get all the fitness programs connected to the users preferences and not exclude by any physical limitation the user has.

  4. Then we build the traits with their weights and score the signifigance of the connection between the user and the fitness programs possible to be recommended.

  5. Then we order by that score and limit the return set to 5.

// Recommend User a Fitness Program
MATCH (u:User) WHERE u.username = "ben" OPTIONAL MATCH (u)-[:HAS]->(pl)
WITH u, pl MATCH (u)-[r:IS_AT|PREFERS|DESIRES|VALUES]->(x)
WITH u, pl, x, coalesce(r.importance, 0.5) AS importance
MATCH (x)<-[]-(x2:FitnessProgram) WHERE NOT (x2)-[:LIMITED_BY]->(pl)
WITH u, x2, collect({name: x.name, weight: importance}) AS traits
WITH u, x2, reduce(s = 0, t IN traits | s + t.weight) AS score
WITH u, x2, score OPTIONAL MATCH (x2)-[]->(x)<-[]-(u)
RETURN x2, collect(x) AS x, u, score ORDER BY score DESC LIMIT 5;

The Recommendations: Recommend User a Nutritional Supplement

The process here is the same as with fitness programs, but is just using nutritional supplements as the thing being recommended. This works because the patterns of relationships is the same across those types.

// Recommend User a Nutritional Supplement
MATCH (u:User) WHERE u.username = "ben" OPTIONAL MATCH (u)-[:HAS]->(pl)
WITH u, pl MATCH (u)-[r:IS_AT|PREFERS|DESIRES|VALUES]->(x)
WITH u, pl, x, coalesce(r.importance, 0.5) AS importance
MATCH (x)<-[]-(x2:NutritionalSupplement) WHERE NOT (x2)-[:LIMITED_BY]->(pl)
WITH u, x2, collect({name: x.name, weight: importance}) AS traits
WITH u, x2, reduce(s = 0, t IN traits | s + t.weight) AS score
WITH u, x2, score OPTIONAL MATCH (x2)-[]->(x)<-[]-(u)
RETURN x2, collect(x) AS x, u, score ORDER BY score DESC LIMIT 5;

The Recommendations: Recommend User a blend of Fitness Programs and Nutrional Supplements

This is the same as the first but simply allows both fitness programs and nutritional supplements as the possible recommendations.

// Recommend User a blend of Fitness Programs and Nutrional Supplements
MATCH (u:User) WHERE u.username = "ben" OPTIONAL MATCH (u)-[:HAS]->(pl)
WITH u, pl MATCH (u)-[r:IS_AT|PREFERS|DESIRES|VALUES]->(x)
WITH u, pl, x, coalesce(r.importance, 0.5) AS importance
MATCH (x)<-[]-(x2) WHERE (x2:FitnessProgram OR x2:NutritionalSupplement) AND NOT (x2)-[:LIMITED_BY]->(pl)
WITH u, x2, collect({name: x.name, weight: importance}) AS traits
WITH u, x2, reduce(s = 0, t IN traits | s + t.weight) AS score
WITH u, x2, score OPTIONAL MATCH (x2)-[]->(x)<-[]-(u)
RETURN x2, collect(x) AS x, u, score ORDER BY score DESC LIMIT 5;

The Recommendations: Recommend Two Users do a Fitness Program Together

Here we makea few adjustments to recommend a FitnessProgram to a pair of Users that should experience it together. The main adjustments here are to:

  1. traverse out from the starting User’s preferences to get other users connected to them as well, which makes the possible set of users to be recommended.

  2. Now we score, order and limit the users to the best one and then proceed to find the best fitness programs for those two users.

  3. The scoring from there on after we find the fitness programs that could be recommended to both is the same as before.

// Recommend Two Users do a Fitness Program Together
MATCH (u:User) WHERE u.username = "ben" OPTIONAL MATCH (u)-[:HAS]->(pl)
WITH u, pl MATCH (u)-[r:IS_AT|PREFERS|DESIRES|VALUES]->(x)
WITH u, pl, x, coalesce(r.importance, 0.5) AS importance
MATCH (x)<-[]-(x2) WHERE (x2:User) AND NOT (x2)-[:LIMITED_BY]->(pl) AND u <> x2
WITH u, pl, x2, collect({name: x.name, weight: importance}) AS traits
WITH u, pl, x2, reduce(s = 0, t IN traits | s + t.weight) AS score
WITH u, pl, x2, score ORDER BY score DESC LIMIT 1
WITH u, pl, x2, score OPTIONAL MATCH (x2)-[]->(x)<-[]-(u), (x)<-[r]-(fp:FitnessProgram) WHERE NOT (fp)-[:LIMITED_BY]->(pl)
WITH u, x2, score, collect(x) AS common, fp, collect({name: x.name, weight: coalesce(r.importance, 0.5)}) AS traits
WITH u, x2, score, common, fp, reduce(s = 0, t IN traits | s + t.weight) AS score2
RETURN u, x2, score, common, fp, score2 ORDER BY score2 DESC LIMIT 1;