Browse Source

Added weighter project

Will Koehrsen 7 years ago
parent
commit
2af9eb47ac
3 changed files with 631 additions and 0 deletions
  1. 619 0
      weighter/Weighter Development.ipynb
  2. BIN
      weighter/gs_auth
  3. 12 0
      weighter/weighter-2038ffb4e5a6.json

+ 619 - 0
weighter/Weighter Development.ipynb

@@ -0,0 +1,619 @@
+{
+ "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": 125,
+   "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"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 126,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "# matplotlib for plotting in the notebook\n",
+    "import matplotlib.pyplot as plt\n",
+    "%matplotlib inline\n",
+    "\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": 127,
+   "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('weighter-2038ffb4e5a6.json', scope)\n",
+    "\n",
+    "# Authorize access\n",
+    "gc = gspread.authorize(credentials);"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Open the sheet and convert to a pandas dataframe"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 129,
+   "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')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 130,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Name</th>\n",
+       "      <th>Entry</th>\n",
+       "      <th>Record</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>Date</th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>2017-08-18 00:00:00-04:00</th>\n",
+       "      <td>koehrcl</td>\n",
+       "      <td>235.2</td>\n",
+       "      <td>True</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2017-08-19 00:00:00-04:00</th>\n",
+       "      <td>koehrcl</td>\n",
+       "      <td>235.6</td>\n",
+       "      <td>True</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2017-08-20 00:00:00-04:00</th>\n",
+       "      <td>koehrcl</td>\n",
+       "      <td>233</td>\n",
+       "      <td>True</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2017-08-21 00:00:00-04:00</th>\n",
+       "      <td>koehrcl</td>\n",
+       "      <td>232.6</td>\n",
+       "      <td>True</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2017-08-22 00:00:00-04:00</th>\n",
+       "      <td>koehrcl</td>\n",
+       "      <td>234.4</td>\n",
+       "      <td>True</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "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": 130,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "weights.head()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 131,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>Name</th>\n",
+       "      <th>Entry</th>\n",
+       "      <th>Record</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>Date</th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>2018-01-19 14:34:56-05:00</th>\n",
+       "      <td>koehrcl</td>\n",
+       "      <td>221.1</td>\n",
+       "      <td>False</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2018-01-19 15:30:25-05:00</th>\n",
+       "      <td>willkoehrsen</td>\n",
+       "      <td>137.3</td>\n",
+       "      <td>False</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2018-01-19 18:11:49-05:00</th>\n",
+       "      <td>fletcher</td>\n",
+       "      <td>188.4</td>\n",
+       "      <td>False</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2018-01-20 15:39:12-05:00</th>\n",
+       "      <td>willkoehrsen</td>\n",
+       "      <td>137</td>\n",
+       "      <td>False</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2018-01-20 15:49:52-05:00</th>\n",
+       "      <td>koehrcl</td>\n",
+       "      <td>220.4</td>\n",
+       "      <td>False</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                                   Name  Entry  Record\n",
+       "Date                                                  \n",
+       "2018-01-19 14:34:56-05:00       koehrcl  221.1   False\n",
+       "2018-01-19 15:30:25-05:00  willkoehrsen  137.3   False\n",
+       "2018-01-19 18:11:49-05:00      fletcher  188.4   False\n",
+       "2018-01-20 15:39:12-05:00  willkoehrsen    137   False\n",
+       "2018-01-20 15:49:52-05:00       koehrcl  220.4   False"
+      ]
+     },
+     "execution_count": 131,
+     "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": 136,
+   "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",
+    "        # 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",
+    "        \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",
+    "        \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",
+    "            \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",
+    "            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",
+    "                               'objective': obj}\n",
+    "       \n",
+    "        self.user_dict = user_dict\n",
+    "                                          \n",
+    "    def process_unrecorded(self):\n",
+    "        \n",
+    "        entries = {name:[] for name in self.users}\n",
+    "        drop = []\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:\n",
+    "                entry = float(entry)\n",
+    "                entries[user].append(entry)\n",
+    "                \n",
+    "            except:  \n",
+    "                entry = str(entry)\n",
+    "                entries[user].append(entry)\n",
+    "                \n",
+    "                drop.append(index)\n",
+    "\n",
+    "                \n",
+    "#                 if entry == 'Summary':\n",
+    "#                     self.summarize()\n",
+    "#                 elif entry == 'Future':\n",
+    "#                     self.future()\n",
+    "#                 elif entry == 'Trends':\n",
+    "#                     self.trends()\n",
+    "#                 elif entry == 'Percent':\n",
+    "#                     self.percent()\n",
+    "#                 elif entry == 'Change':\n",
+    "#                     self.changepoints()\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",
+    "    # This will be automatically called for each new entry\n",
+    "    def basic_message(self):\n",
+    "        \n",
+    "        for user in self.users:\n",
+    "            if user in self.entries.keys():\n",
+    "                user_entries = self.entries.get(user)\n",
+    "                user_info = self.user_dict.get(user)\n",
+    "                \n",
+    "                for entry in user_entries:\n",
+    "                    \n",
+    "                    if type(entry) == float:\n",
+    "                        \n",
+    "                        if user_info['objective'] == 'lose':\n",
+    "                            weight_change = user_info['max_weight'] - entry\n",
+    "                            pct_change = 100 * weight_change / user_info['max_weight']\n",
+    "                        \n",
+    "                        elif user_info['objective'] == 'gain':\n",
+    "                            weight_change = entry - user_info['min_weight']\n",
+    "                            pct_change = 100 * weight_change / user_info['min_weight']\n",
+    "                         \n",
+    "                        print('\\nUser: {}'.format(user))\n",
+    "                        print('Total Weight Change = {:.2f} lbs.'.format(weight_change))\n",
+    "                        print('Percentage Weight Change = %{:.2f}.'.format(pct_change))\n",
+    "       \n",
+    "    # Display comprehensive stats about the users progress\n",
+    "    def summary(self, user):\n",
+    "        user_info = self.user_dict.get(user)\n",
+    "        print(user_info)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 133,
+   "metadata": {
+    "collapsed": false,
+    "scrolled": false
+   },
+   "outputs": [],
+   "source": [
+    "weighter = Weighter(weights)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 134,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "User: Will\n",
+      "Total Weight Change = 14.20 lbs.\n",
+      "Percentage Weight Change = %11.49.\n",
+      "\n",
+      "User: Will\n",
+      "Total Weight Change = 13.70 lbs.\n",
+      "Percentage Weight Change = %11.08.\n",
+      "\n",
+      "User: Will\n",
+      "Total Weight Change = 13.40 lbs.\n",
+      "Percentage Weight Change = %10.84.\n",
+      "\n",
+      "User: Fletcher\n",
+      "Total Weight Change = 4.00 lbs.\n",
+      "Percentage Weight Change = %2.17.\n",
+      "\n",
+      "User: Fletcher\n",
+      "Total Weight Change = 3.80 lbs.\n",
+      "Percentage Weight Change = %2.06.\n",
+      "\n",
+      "User: Craig\n",
+      "Total Weight Change = 15.40 lbs.\n",
+      "Percentage Weight Change = %6.54.\n",
+      "\n",
+      "User: Craig\n",
+      "Total Weight Change = 14.50 lbs.\n",
+      "Percentage Weight Change = %6.15.\n",
+      "\n",
+      "User: Craig\n",
+      "Total Weight Change = 15.20 lbs.\n",
+      "Percentage Weight Change = %6.45.\n"
+     ]
+    }
+   ],
+   "source": [
+    "weighter.basic_message()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 135,
+   "metadata": {
+    "collapsed": false
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "{'min_weight': 219.6, 'max_weight': 235.6, 'min_date': Timestamp('2017-12-27 15:14:17-0500', tz='US/Eastern'), 'max_date': Timestamp('2017-08-19 00:00:00-0400', tz='US/Eastern'), 'recent': 220.40000000000001, 'abs_change': 14.799999999999983, 'pct_change': 6.2925170068027141, 'pct_towards_goal': 73.267326732673226, 'objective': 'lose'}\n"
+     ]
+    }
+   ],
+   "source": [
+    "weighter.summary('Craig')"
+   ]
+  }
+ ],
+ "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
+}

BIN
weighter/gs_auth


File diff suppressed because it is too large
+ 12 - 0
weighter/weighter-2038ffb4e5a6.json