{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Purpose\n",
"\n",
"Weighter is designed to be a hacky weight tracking app using Slack as a frontend and Google Sheets as a database! \n",
"Weights are entered through a Slack Channel, stored in a Google Sheet, and reported back to users through Slack. Users will have the option to view various stats and graphs by sending different slack messages. \n",
"\n",
"Weighter also features additive modeling forecasts using the Facebook Prophet library. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup Libraries and Access to the Google Sheet"
]
},
{
"cell_type": "code",
"execution_count": 64,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# pandas and numpy for data manipulation\n",
"import pandas as pd\n",
"import numpy as np\n",
"\n",
"# fbprophet for additive models\n",
"import fbprophet\n",
"\n",
"# gspread for Google Sheets access\n",
"import gspread\n",
"\n",
"# slacker for interacting with Slack\n",
"from slacker import Slacker\n",
"\n",
"# oauth2client for authorizing access to Google Sheets\n",
"from oauth2client.service_account import ServiceAccountCredentials\n",
"\n",
"# os for deleting images\n",
"import os"
]
},
{
"cell_type": "code",
"execution_count": 213,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# matplotlib for plotting in the notebook\n",
"import matplotlib.pyplot as plt\n",
"%matplotlib inline\n",
"\n",
"import matplotlib.patches as mpatches\n",
"import matplotlib"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Google Sheet Access\n",
"\n",
"The json file is the credentials for accessing the google sheet generated from the Google Developers API. To access a specific sheet, you need to share the sheet with the email address in the json file. "
]
},
{
"cell_type": "code",
"execution_count": 264,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"INFO:oauth2client.client:Refreshing access_token\n"
]
}
],
"source": [
"# google sheets access\n",
"scope = ['https://spreadsheets.google.com/feeds']\n",
"\n",
"# Use local stored credentials in json file\n",
"# make sure to first share the sheet with the email in the json file\n",
"credentials = ServiceAccountCredentials.from_json_keyfile_name('C:/Users/Will Koehrsen/Desktop/weighter-2038ffb4e5a6.json', scope)\n",
"\n",
"# Authorize access\n",
"gc = gspread.authorize(credentials);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Set up Slack Access"
]
},
{
"cell_type": "code",
"execution_count": 265,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Slack api key is stored as text file\n",
"with open('C:/Users/Will Koehrsen/Desktop/slack_api.txt', 'r') as f:\n",
" slack_api_key = f.read()"
]
},
{
"cell_type": "code",
"execution_count": 266,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"slack = Slacker(slack_api_key)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Open the sheet and convert to a pandas dataframe"
]
},
{
"cell_type": "code",
"execution_count": 267,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"# Open the sheet, need to share the sheet with email specified in json file\n",
"gsheet = gc.open('Auto Weight Challenge').sheet1\n",
"\n",
"# List of lists with each row in the sheet as a list\n",
"weight_lists = gsheet.get_all_values()\n",
"\n",
"# Headers are the first list\n",
"# Pop returns the element (list in this case) and removes it from the list\n",
"headers = weight_lists.pop(0)\n",
"\n",
"# Convert list of lists to a dataframe with specified column header\n",
"weights = pd.DataFrame(weight_lists, columns=headers)\n",
"\n",
"# Record column should be a boolean\n",
"weights['Record'] = weights['Record'].astype(bool)\n",
"\n",
"# Name column is a string\n",
"weights['Name'] = weights['Name'].astype(str)\n",
"\n",
"# Convert dates to datetime, then set as index, then set the time zone\n",
"weights['Date'] = pd.to_datetime(weights['Date'], unit='s')\n",
"weights = weights.set_index('Date', drop = True).tz_localize(tz='US/Eastern')\n",
"\n",
"# Drop any extra entries\n",
"weights = weights.drop('NaT')"
]
},
{
"cell_type": "code",
"execution_count": 268,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Name | \n",
" Entry | \n",
" Record | \n",
"
\n",
" \n",
" Date | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" 2017-08-18 00:00:00-04:00 | \n",
" koehrcl | \n",
" 235.2 | \n",
" True | \n",
"
\n",
" \n",
" 2017-08-19 00:00:00-04:00 | \n",
" koehrcl | \n",
" 235.6 | \n",
" True | \n",
"
\n",
" \n",
" 2017-08-20 00:00:00-04:00 | \n",
" koehrcl | \n",
" 233 | \n",
" True | \n",
"
\n",
" \n",
" 2017-08-21 00:00:00-04:00 | \n",
" koehrcl | \n",
" 232.6 | \n",
" True | \n",
"
\n",
" \n",
" 2017-08-22 00:00:00-04:00 | \n",
" koehrcl | \n",
" 234.4 | \n",
" True | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Name Entry Record\n",
"Date \n",
"2017-08-18 00:00:00-04:00 koehrcl 235.2 True\n",
"2017-08-19 00:00:00-04:00 koehrcl 235.6 True\n",
"2017-08-20 00:00:00-04:00 koehrcl 233 True\n",
"2017-08-21 00:00:00-04:00 koehrcl 232.6 True\n",
"2017-08-22 00:00:00-04:00 koehrcl 234.4 True"
]
},
"execution_count": 268,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"weights.head()"
]
},
{
"cell_type": "code",
"execution_count": 269,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Name | \n",
" Entry | \n",
" Record | \n",
"
\n",
" \n",
" Date | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" 2018-01-21 16:09:42-05:00 | \n",
" koehrcl | \n",
" 220.2 | \n",
" True | \n",
"
\n",
" \n",
" 2018-01-21 20:09:43-05:00 | \n",
" willkoehrsen | \n",
" analysis | \n",
" False | \n",
"
\n",
" \n",
" 2018-01-21 20:09:46-05:00 | \n",
" willkoehrsen | \n",
" predict | \n",
" False | \n",
"
\n",
" \n",
" 2018-01-21 20:10:10-05:00 | \n",
" willkoehrsen | \n",
" percent | \n",
" False | \n",
"
\n",
" \n",
" 2018-01-21 20:10:12-05:00 | \n",
" willkoehrsen | \n",
" history | \n",
" False | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Name Entry Record\n",
"Date \n",
"2018-01-21 16:09:42-05:00 koehrcl 220.2 True\n",
"2018-01-21 20:09:43-05:00 willkoehrsen analysis False\n",
"2018-01-21 20:09:46-05:00 willkoehrsen predict False\n",
"2018-01-21 20:10:10-05:00 willkoehrsen percent False\n",
"2018-01-21 20:10:12-05:00 willkoehrsen history False"
]
},
"execution_count": 269,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"weights.tail()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"+ Date is the index (in Eastern time here)\n",
"+ Name is the slack username\n",
"+ Entry is either weight or a string to display results\n",
"+ Record is whether or not the entry has been processed by weighter"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Weighter Class\n",
"\n",
"The class will include a number of different methods for analyzing the data and graphing results. These results can then be sent back to Slack depending on the message entered by the user."
]
},
{
"cell_type": "code",
"execution_count": 270,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"class Weighter():\n",
" \n",
" \"\"\"\n",
" When weighter is initialized, we need to convert the usernames,\n",
" get a dictionary of the unrecorded entries, construct a dictionary\n",
" of the actions to take, and make sure all data is formatted correctly\n",
" \"\"\"\n",
" \n",
" def __init__(self, weights):\n",
" \n",
" # Weights is a dataframe\n",
" self.weights = weights.copy()\n",
" \n",
" # Users is a list of the unique users in the data\n",
" self.users = list(set(self.weights['Name']))\n",
" \n",
" correct_names = []\n",
" \n",
" # Name Changes\n",
" for user in self.weights['Name']:\n",
" \n",
" # Have to hardcode in name Changes\n",
" if user == 'koehrcl':\n",
" correct_names.append('Craig')\n",
" elif user == 'willkoehrsen':\n",
" correct_names.append('Will')\n",
" elif user == 'fletcher':\n",
" correct_names.append('Fletcher')\n",
" \n",
" # Currently do not handle new users\n",
" else:\n",
" print('New User Detected')\n",
" return\n",
" \n",
" self.weights['Name'] = correct_names\n",
" \n",
" # Users is a list of the unique users in the data\n",
" self.users = list(set(self.weights['Name']))\n",
" \n",
" # Create a dataframe of the unrecorded entries\n",
" self.unrecorded = self.weights[self.weights['Record'] != True]\n",
" \n",
" # Process the unrecorded entries\n",
" self.process_unrecorded()\n",
" \n",
" # The remaning entries will all be weights\n",
" self.weights['Entry'] = [float(weight) for weight in self.weights['Entry']]\n",
" \n",
" # Build the user dictionary\n",
" self.build_user_dict()\n",
" \n",
" # Calculate the change and percentage change columns\n",
" self.calculate_columns()\n",
" \n",
" \"\"\" \n",
" Constructs a dictionary for each user with critical information\n",
" This forms the basis for the summarize function\n",
" \"\"\"\n",
" \n",
" def build_user_dict(self):\n",
" \n",
" user_dict = {}\n",
" \n",
" user_goals = {'Craig': 215.0, 'Fletcher': 200.0, 'Will': 155.0}\n",
" user_colors = {'Craig': 'forestgreen', 'Fletcher': 'navy', 'Will': 'darkred'}\n",
" \n",
" for i, user in enumerate(self.users):\n",
" \n",
" user_weights = self.weights[self.weights['Name'] == user]\n",
" goal = user_goals.get(user)\n",
"\n",
" start_weight = user_weights.ix[min(user_weights.index), 'Entry'] \n",
" start_date = min(user_weights.index)\n",
" \n",
" # Find minimum weight and date on which it occurs\n",
" min_weight = min(user_weights['Entry'])\n",
" min_weight_date = ((user_weights[user_weights['Entry'] == min_weight].index)[0])\n",
" \n",
" # Find maximum weight and date on which it occurs\n",
" max_weight = max(user_weights['Entry'])\n",
" max_weight_date = ((user_weights[user_weights['Entry'] == max_weight].index)[0])\n",
" \n",
" most_recent_weight = user_weights.ix[max(user_weights.index), 'Entry']\n",
" \n",
" if goal < start_weight:\n",
" change = start_weight - most_recent_weight\n",
" obj = 'lose'\n",
" elif goal > start_weight:\n",
" change = most_recent_weight - start_weight\n",
" obj = 'gain'\n",
" \n",
" pct_change = 100 * change / start_weight\n",
" \n",
" pct_to_goal = 100 * (change / abs(start_weight - goal) )\n",
" \n",
" # Color for plotting\n",
" user_color = user_colors[user]\n",
" \n",
" user_dict[user] = {'min_weight': min_weight, 'max_weight': max_weight,\n",
" 'min_date': min_weight_date, 'max_date': max_weight_date,\n",
" 'recent': most_recent_weight, 'abs_change': change,\n",
" 'pct_change': pct_change, 'pct_towards_goal': pct_to_goal,\n",
" 'start_weight': start_weight, 'start_date': start_date,\n",
" 'goal_weight': goal, 'objective': obj, 'color': user_color}\n",
" \n",
" self.user_dict = user_dict\n",
" \n",
" \"\"\"\n",
" Builds a dictionary of unrecorded entries where each key is the user\n",
" and the value is a list of weights and methods called for by the user.\n",
" This dictionary is saved as the entries attribute of the class.\n",
" Removes the none weights from the data and from the google sheet.\n",
" \"\"\"\n",
" \n",
" def process_unrecorded(self):\n",
" \n",
" entries = {name:[] for name in self.users}\n",
" drop = []\n",
" \n",
" location = {}\n",
" \n",
" for index in self.unrecorded.index:\n",
"\n",
" entry = self.unrecorded.ix[index, 'Entry']\n",
" user = str(self.unrecorded.ix[index, 'Name'])\n",
" \n",
" # Try and except does not seem like the best way to handle this\n",
" try:\n",
" entry = float(entry)\n",
" entries[user].append(entry)\n",
" location[index] = True\n",
" \n",
" except: \n",
" entry = str(entry)\n",
" entries[user].append(entry.strip())\n",
" location[index] = 'remove'\n",
" \n",
" drop.append(index)\n",
" \n",
" self.weights.ix[index, 'Record'] = True\n",
" \n",
" # Indexes of new entries\n",
" self.location = location\n",
" \n",
" # Update the Google Sheet before dropping\n",
" self.update_sheet()\n",
" \n",
" # Drop the rows which do not contain a weight\n",
" self.weights.drop(drop, axis=0, inplace=True)\n",
"\n",
" # Entries is all of the new entries\n",
" self.entries = entries\n",
" \n",
" \"\"\" \n",
" Update the Google Spreadsheet. This involves removing the rows without weight\n",
" entries and putting a True in the record column for all weights. \n",
" \"\"\"\n",
"\n",
" def update_sheet(self):\n",
" delete_count = 0\n",
" \n",
" # Iterate through the locations and update as appropriate\n",
" for index, action in self.location.items():\n",
" cell_row = (np.where(self.weights.index == index))[0][0] + 2 - delete_count\n",
" if action == 'remove':\n",
" gsheet.delete_row(index = cell_row)\n",
" delete_count += 1\n",
" elif action:\n",
" gsheet.update_acell(label='D%d' % cell_row, val = 'True')\n",
" \n",
" \"\"\" \n",
" Iterates through the unrecorded entries and delegates \n",
" each one to the appropriate method.\n",
" Updates the record cell in the google sheet \n",
" \"\"\"\n",
" def process_entries(self):\n",
" for user, user_entries in self.entries.items():\n",
" for entry in user_entries:\n",
" \n",
" # If a weight, display the basic message\n",
" if type(entry) == float:\n",
" self.basic_message(user)\n",
" \n",
" # If the message is a string hand off to the appropriate function\n",
" else:\n",
" \n",
" # Require at lesat 8 days of data\n",
" if len(self.weights[self.weights['Name'] == user]) < 8:\n",
" message = \"\\nAt least 8 days of data required for detailed analysis.\"\n",
" slack.chat.post_message(channel='#test_python', text = message, username = \"Weight Tracker Data Management\")\n",
" \n",
" elif entry.lower() == 'summary':\n",
" self.summary(user)\n",
"\n",
" elif entry.lower() == 'percent':\n",
" self.percentage_plot()\n",
"\n",
" elif entry.lower() == 'history':\n",
" self.history_plot(user)\n",
"\n",
" elif entry.lower() == 'future':\n",
" self.future_plot(user)\n",
"\n",
" elif entry.lower() == 'analysis':\n",
" self.analyze(user)\n",
" \n",
" # Display a help message if the string is not valid\n",
" else:\n",
" message = (\"\\nPlease enter a valid message:\\n\"\n",
" \"Your weight\"\n",
" \"'Summary' to see a personal summary\"\n",
" \"'Percent' to see a plot of all users percentage changes\"\n",
" \"'History' to see a plot of your personal history\"\n",
" \"'Future' to see your predictions for the next thirty days\"\n",
" \"'Analysis' to view personalized advice\\n\"\n",
" \"For more help, contact @koehrsen_will on Twitter.\\n\")\n",
"\n",
" slack.chat.post_message(channel='#test_python', text = message, username = \"Weight Tracker Help\")\n",
" \n",
" \n",
" \"\"\" \n",
" Adds the change and percentage change columns to the self.weights df\n",
" \"\"\"\n",
" def calculate_columns(self):\n",
" \n",
" self.weights = self.weights.sort_values('Name')\n",
" self.weights['change'] = 0\n",
" self.weights['pct_change'] = 0\n",
" self.weights.reset_index(level=0, inplace = True)\n",
" \n",
" for index in self.weights.index:\n",
" user = self.weights.ix[index, 'Name']\n",
" weight = self.weights.ix[index, 'Entry']\n",
" start_weight = self.user_dict[user]['start_weight']\n",
" objective = self.user_dict[user]['objective']\n",
" \n",
" if objective == 'lose':\n",
" \n",
" self.weights.ix[index, 'change'] = start_weight - weight\n",
" self.weights.ix[index, 'pct_change'] = 100 * (start_weight - weight) / start_weight\n",
" \n",
" elif objective == 'gain':\n",
" self.weights.ix[index, 'change'] = weight - start_weight\n",
" self.weights.ix[index, 'pct_change'] = 100 * (weight - start_weight) / start_weight\n",
"\n",
" self.weights.set_index('Date', drop=True, inplace=True)\n",
" \n",
" \n",
" \"\"\" \n",
" This method is automatically run for each new weight\n",
" \"\"\"\n",
" def basic_message(self, user):\n",
" \n",
" # Find information for user, construct message, post message to Slack\n",
" user_info = self.user_dict.get(user)\n",
"\n",
" message = (\"\\n{}: Total Weight Change = {:.2f} lbs.\\n\\n\"\n",
" \"Percentage Weight Change = {:.2f}%\\n\").format(user, user_info['abs_change'],\n",
" user_info['pct_change'])\n",
"\n",
" slack.chat.post_message('#test_python', text=message, username='Weight Challenge Update')\n",
" \n",
" \"\"\" \n",
" Displays comprehensive stats about the user\n",
" \"\"\"\n",
" \n",
" def summary(self, user):\n",
" user_info = self.user_dict.get(user)\n",
" message = (\"\\n{}, your most recent weight was {:.2f} lbs.\\n\\n\"\n",
" \"Absolute weight Change = {:.2f} lbs, percentage weight Change = {:.2f}%.\\n\\n\"\n",
" \"Minimum weight = {:.2f} lbs on {} and maximum weight = {:.2f} lbs on {}.\\n\\n\"\n",
" \"Your goal weight = {:.2f} lbs. and you are {:.2f}% of the way there.\\n\\n\"\n",
" \"You started at {:.2f} lbs on {}. Congratulations on the progress!\\n\").format(user, \n",
" user_info['recent'], user_info['abs_change'], user_info['pct_change'], \n",
" user_info['min_weight'], str(user_info['min_date'].date()),\n",
" user_info['max_weight'], str(user_info['max_date'].date()),\n",
" user_info['goal_weight'], user_info['pct_towards_goal'], \n",
" user_info['start_weight'], str(user_info['start_date'].date()))\n",
" \n",
" slack.chat.post_message('#test_python', text=message, username='%s Summary' % user)\n",
" \n",
" \"\"\"\n",
" Reset the plot and institute basic parameters\n",
" \"\"\"\n",
" @staticmethod\n",
" def reset_plot():\n",
" matplotlib.rcParams.update(matplotlib.rcParamsDefault)\n",
" matplotlib.rcParams['text.color'] = 'k'\n",
" \n",
" \"\"\"\n",
" Plot of all users percentage changes.\n",
" Includes polynomial fits (degree may need to be adjusted).\n",
" \"\"\"\n",
" \n",
" def percentage_plot(self):\n",
" \n",
" self.reset_plot()\n",
" \n",
" plt.style.use('fivethirtyeight')\n",
" plt.figure(figsize=(10,8))\n",
"\n",
" for i, user in enumerate(weighter.users):\n",
" \n",
" user_color = self.user_dict[user]['color']\n",
"\n",
" # Select the user and order dataframe by date\n",
" df = self.weights[self.weights['Name'] == user]\n",
" df.sort_index(inplace=True)\n",
" \n",
" # List is used for fitting polynomial\n",
" xvalues = list(range(len(df)))\n",
"\n",
" # Create a polynomial fit\n",
" z = np.polyfit(xvalues, df['pct_change'], deg=6)\n",
"\n",
" # Create a function from the fit\n",
" p = np.poly1d(z)\n",
"\n",
" # Map the x values to y values\n",
" fit_data = p(xvalues)\n",
"\n",
" # Plot the actual points and the fit\n",
" plt.plot(df.index, df['pct_change'], 'o', color = user_color, label = '%s' % user)\n",
" plt.plot(df.index, fit_data, '-', color = user_color, linewidth = 5, label = '%s' % user)\n",
"\n",
"\n",
" # Plot formatting\n",
" plt.xlabel('Date'); plt.ylabel('Percentage Change')\n",
" plt.title('Percentage Changes')\n",
" plt.grid(color='k', alpha=0.4)\n",
" plt.legend(prop={'size':14})\n",
" plt.savefig(fname='percentage_plot.png');\n",
" \n",
" slack.files.upload('percentage_plot.png', channels='#test_python')\n",
" \n",
" os.remove('percentage_plot.png')\n",
" \n",
" \"\"\" \n",
" Plot of a single user's history.\n",
" Also plot a polynomial fit on the observations.\n",
" \"\"\"\n",
" def history_plot(self, user):\n",
" \n",
" self.reset_plot()\n",
" plt.style.use('fivethirtyeight')\n",
" plt.figure(figsize=(10, 8))\n",
" \n",
" df = self.weights[self.weights['Name'] == user]\n",
" df.sort_index(inplace=True) \n",
" user_color = self.user_dict[user]['color']\n",
" \n",
" # List is used for fitting polynomial\n",
" xvalues = list(range(len(df)))\n",
"\n",
" # Create a polynomial fit\n",
" z = np.polyfit(xvalues, df['Entry'], deg=6)\n",
"\n",
" # Create a function from the fit\n",
" p = np.poly1d(z)\n",
"\n",
" # Map the x values to y values\n",
" fit_data = p(xvalues)\n",
"\n",
" # Make a simple plot and upload to slack\n",
" plt.plot(df.index, df['Entry'], 'ko', ms = 8, label = 'Observed')\n",
" plt.plot(df.index, fit_data, '-', color = user_color, linewidth = 5, label = 'Smooth Fit')\n",
" plt.xlabel('Date'); plt.ylabel('Weight (lbs)'); plt.title('%s Weight History' % user)\n",
" plt.legend(prop={'size': 14});\n",
" \n",
" plt.savefig(fname='history_plot.png')\n",
" slack.files.upload('history_plot.png', channels='#test_python')\n",
" \n",
" # Remove the plot from local storage\n",
" os.remove('history_plot.png')\n",
" \n",
" \"\"\" \n",
" Create a prophet model for forecasting and trend analysis.\n",
" Might need to adjust model hyperparameters.\n",
" \"\"\"\n",
" \n",
" def prophet_model(self):\n",
" model = fbprophet.Prophet(daily_seasonality=False, yearly_seasonality=False)\n",
" return model\n",
" \n",
" \"\"\" \n",
" Plot the prophet forecast for the next thirty days\n",
" Print the expected weight at the end of the forecast\n",
" \"\"\"\n",
" def future_plot(self, user):\n",
" self.reset_plot()\n",
" \n",
" df = self.weights[self.weights['Name'] == user]\n",
" dates = [date.date() for date in df.index]\n",
" df['ds'] = dates\n",
" df['y'] = df['Entry']\n",
" \n",
" df.sort_index(inplace=True)\n",
"\n",
" # Prophet model\n",
" model = self.prophet_model()\n",
" model.fit(df)\n",
" \n",
" # Future dataframe for predictions\n",
" future = model.make_future_dataframe(periods=30, freq='D')\n",
" future = model.predict(future)\n",
" \n",
" color = self.user_dict[user]['color']\n",
" \n",
" # Write a message and post to slack\n",
" message = ('{} Your predicted weight on {} = {:.2f} lbs.'.format(\n",
" user, max(future['ds']).date(), future.ix[len(future) - 1, 'yhat']))\n",
" \n",
" slack.chat.post_message(channel=\"#test_python\", text=message, username = 'Future Prediction')\n",
" \n",
" # Create the plot and upload to slack\n",
" fig, ax = plt.subplots(1, 1, figsize=(10, 8))\n",
" ax.plot(df['ds'], df['y'], 'o', color = 'k', ms = 8, label = 'observations')\n",
" ax.plot(future['ds'], future['yhat'], '-', color = color, label = 'modeled')\n",
" ax.fill_between(future['ds'].dt.to_pydatetime(), future['yhat_upper'], future['yhat_lower'], facecolor = color, \n",
" alpha = 0.4, edgecolor = 'k', linewidth = 1.8, label = 'confidence interval')\n",
" plt.xlabel('Date'); plt.ylabel('Weight (lbs)'); plt.title('%s 30 Day Prediction' % user)\n",
" plt.legend()\n",
" plt.savefig('future_plot.png')\n",
" \n",
" slack.files.upload('future_plot.png', channels=\"#test_python\")\n",
" \n",
" os.remove('future_plot.png')\n",
" \n",
" \"\"\" \n",
" Analyze user trends and provide recommendations. \n",
" Determine if the user is on track to meet their goal.\n",
" \"\"\"\n",
" \n",
" def analyze(self, user):\n",
" \n",
" self.reset_plot()\n",
" \n",
" # Get user info and sort dataframe by date\n",
" info = self.user_dict.get(user)\n",
" goal_weight = info['goal_weight']\n",
" df = self.weights[self.weights['Name'] == user]\n",
" df = df.sort_index()\n",
" df['ds'] = [date.date() for date in df.index]\n",
" df['y'] = df['Entry']\n",
" \n",
" model = self.prophet_model()\n",
" model.fit(df)\n",
" \n",
" prediction_days = 2 * len(df)\n",
" \n",
" future = model.make_future_dataframe(periods = prediction_days, freq = 'D')\n",
" future = model.predict(future)\n",
" \n",
" # lbs change per day \n",
" change_per_day = info['abs_change'] / (max(df['ds']) - min(df['ds'])).days\n",
" \n",
" days_to_goal = abs(int((info['recent'] - goal_weight) / change_per_day))\n",
" date_for_goal = max(df['ds']) + pd.DateOffset(days=days_to_goal)\n",
" \n",
" # future dataframe where the user in above goal\n",
" goal_future = future[future['yhat'] < goal_weight]\n",
" \n",
" # The additive model predicts the user will meet their goal\n",
" if len(goal_future) > 0:\n",
" model_goal_date = min(goal_future['ds'])\n",
" message = (\"\\n{} Your average weight change per day is {:.2f} lbs\\n\"\n",
" \"Extrapolating the average loss per day, you will reach your goal of {} lbs in {} days on {}.\\n\\n\"\n",
" \"The additive model predicts you will reach your goal on {}\\n\".format(\n",
" user, change_per_day, goal_weight, days_to_goal, date_for_goal.date(), model_goal_date.date()))\n",
" \n",
" # The additive model does not predict the user will meet their goal\n",
" else:\n",
" final_future_date = max(future['ds'])\n",
" message = (\"\\n{} Your average weight change per day is {:.2f} lbs\\n\\n\"\n",
" \"Extrapolating the average loss per day, you will reach your goal of {} lbs in {} days on {}.\\n\\n\"\n",
" \"The additive model does not forecast you reaching your goal by {}.\\n\".format(\n",
" user, change_per_day, goal_weight, days_to_goal, date_for_goal.date(), final_future_date))\n",
" \n",
" \n",
" slack.chat.post_message(channel=\"test_python\", text=message, username=\"Weight Tracker Analysis\")\n",
" \n",
" # Identify Weekly Trends\n",
" future['weekday'] = [date.weekday() for date in future['ds']]\n",
" future_weekly = future.groupby('weekday').mean()\n",
" future_weekly.index = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']\n",
" \n",
" # Color labels based on the users objective\n",
" colors = ['red' if ( ((weight > 0) & (info['objective'] == 'lose')) | ((weight < 0) & (info['objective'] == 'gain'))) else 'green' for weight in future_weekly['weekly']]\n",
" \n",
" # Create a bar plot with labels for positive and negative changes\n",
" plt.figure(figsize=(10, 8))\n",
" xvalues = list(range(len(future_weekly)))\n",
" plt.bar(xvalues, future_weekly['weekly'], color = colors, edgecolor = 'k', linewidth = 2)\n",
" plt.xticks(xvalues, list(future_weekly.index))\n",
" red_patch = mpatches.Patch(color='red', edgecolor='k', linewidth = 2, label='Needs Work')\n",
" green_patch = mpatches.Patch(color='green', edgecolor = 'k', linewidth = 2, label='Solid')\n",
" plt.legend(handles=[red_patch, green_patch])\n",
" plt.xlabel('Day of Week')\n",
" plt.ylabel('Trend (lbs)')\n",
" plt.title('%s Weekly Trends' % user)\n",
" plt.savefig('weekly_plot.png')\n",
" \n",
" # Upload the image to slack and delete local file\n",
" slack.files.upload('weekly_plot.png', channels = '#test_python')\n",
" os.remove('weekly_plot.png')"
]
},
{
"cell_type": "code",
"execution_count": 271,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"if len(weights) > np.count_nonzero(weights['Record']):\n",
" weighter = Weighter(weights)\n",
" weighter.process_entries()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.0"
}
},
"nbformat": 4,
"nbformat_minor": 2
}