Nutrition and Exercise Habits – Basic Analysis

Click here to skip the code and see results.

The below code will import previously manicured data to offer quantitative advice regarding dietary and exercise logs, as well as draw some descriptive conclusions about our general habits. (see https://josetorres.us/data-science/jefit-etl-with-python/ and https://josetorres.us/data-science/scraping-myfitnesspal-with-python/ for data acquisition, cleaning, and warehousing steps.)

In [1]:
%matplotlib inline
import sqlite3 # importing databases
import numpy as np  # helpful shorthanding and nans
import pandas as pd  # workhorse dataframes
import datetime  # timedelta offsets
import matplotlib.pyplot as plt  # basic plotting
import matplotlib.ticker # for aligning twin y axes
import matplotlib.style as style
import statsmodels.api as sm  # basic regression analysis
import sympy  # for pratio integrals
from scipy.optimize import curve_fit  # for fitting p-ratio
from fractions import Fraction # for expressing floats as rational for sympy polydiviserror
import warnings # to ignore warning messages due to deprecation etc over time
warnings.filterwarnings('ignore')
# mpl.style.use({'figure.figsize':[16,10], 'font.size':14.0, 'axes.titlesize':'large', 
#          'axes.labelsize': 'medium', 'legend.fontsize': 'large', 'xtick.labelsize': 'medium',
#           'ytick.labelsize' : 'medium', 'axes.grid' : True, 'grid.linewidth' : 0.75, 
#           'grid.linestyle' : '-', 'grid.color' : 'white'})
style.use({'figure.figsize':[16,10]})
style.use('fivethirtyeight') # some plot styling

Importing data from MyFitnessPal database and merging it with any potential weight, bodyfat, and circumferential measurements from Jefit (Created in https://josetorres.us/data-science/scraping-myfitnesspal-with-python/)

In [2]:
def load_mfp(mfp_db = "mfp_clean.db", jefit_db = "jefit_clean.db", merge_jefit=True):
    conn = sqlite3.connect(mfp_db)
    daily = pd.read_sql_query("SELECT * FROM mfp_data", conn, index_col="Date")
    daily.index = pd.to_datetime(daily.index)
    
    # creating daily macronutrient ratios to compare to goals
    daily['PMacro'] = (daily.Protein*4)/daily.Calories*100.0
    daily['FMacro'] = (daily.Fat*9)/daily.Calories*100.0
    daily['CMacro'] = (daily.Carbs*4)/daily.Calories*100.0
    
    profile = pd.read_sql_query("SELECT * FROM mfp_profile", conn).drop(["index"], axis=1)
    profile = profile.iloc[0]
    
    conn.close()

    if merge_jefit:
        conn = sqlite3.connect(jefit_db)
        weight_df = pd.read_sql_query("SELECT * FROM weight", conn, index_col="Date")
        weight_df.index = pd.to_datetime(weight_df.index)
        bodyfat_df = pd.read_sql_query("SELECT * FROM bodyfat", conn, index_col="Date")
        bodyfat_df.index = pd.to_datetime(bodyfat_df.index)
        measurements = pd.read_sql_query("SELECT * FROM measurements", conn, index_col="Date")
        measurements.index = pd.to_datetime(measurements.index)
        cols_mfp = daily.columns.tolist()
        cols_meas = measurements.columns.tolist()
        totcols = cols_mfp + cols_meas
        daily = weight_df.combine_first(daily)
        daily = bodyfat_df.combine_first(daily)
        daily = measurements.combine_first(daily)[totcols]
        conn.close()
        
    # get rid of days with no data
    daily.replace('NaN', 0, inplace=True)
    daily = daily.loc[(daily != 0).any(axis=1)]
    daily.replace(0, np.nan, inplace=True)
    
    # check for duplicate indices and keep only the last
    daily = daily[~daily.index.duplicated(keep='last')]

    return profile, daily

Importing data from database created in https://josetorres.us/data-science/jefit-etl-with-python/

In [3]:
def load_jefit(jefit_db = "jefit_clean.db"):
    conn = sqlite3.connect(jefit_db)
    lifts_df = pd.read_sql_query("SELECT * FROM lifts", conn, index_col="Date")
    lifts_df.index = pd.to_datetime(lifts_df.index)
    conn.close()
    return lifts_df

Putting it all together in an easily accessible class

In [4]:
class FitnessLogs(object):
    '''
    Includes methods for viewing and predicting multiple factors including health, performance, and future estimates.
    
    Include jefit=True if jefit data exists, start and end dates optional
    '''

    def __init__(self, mfp_db = "mfp_clean.db", jefit_db = "jefit_clean.db", jefit=True, start_date=None, end_date=None):
        self._profile, self._daily = load_mfp(mfp_db, jefit_db, merge_jefit=jefit)
        
        # load jefit data if jefit
        if jefit:
            self._lifts = load_jefit(jefit_db)

        # slice data using start and end date if given
        if start_date:
            start_date = pd.to_datetime(start_date)
            self._daily = self._daily.loc[start_date:]
            self._lifts = self._lifts.loc[start_date:]
        if end_date:
            end_date = pd.to_datetime(end_date)
            self._daily = self._daily.loc[:end_date]
            self._lifts = self._lifts.loc[:end_date]
            
        # check how much data is missing, will greatly affect accuracy especially total estimate
        days_total = (self._daily.index[-1] - self._daily.index[0]).days + 1
        days_missing = days_total - len(self._daily)
        if days_missing/float(days_total) > 0.25:
            print('There are ' + str(days_missing) + ' days missing from your data spanning ' + str(days_total) + ' days (' + str(int(100*days_missing/float(days_total))) + '%).')
            print('This greatly affects the value of the predicted result which is based on all history; missing weight or caloric logs cannot be accurately estimated (as missing days are often a result of going off diet). It is recommended to use start and end dates without such large gaps.')
            print("Data will be imputed based on current limits in the meantime")
            print('')
            
        # filling na values first with ffill, then mean, 0 if no vals at all
        self._daily.fillna(method='ffill', inplace=True)
        self._daily.fillna(self._daily.mean(), inplace=True)
        self._daily.fillna(0, inplace=True)
        
        # reindexing to fill in missing dates while imputing
        date_range = pd.date_range(start = self._daily.index[0], end = self._daily.index[-1])
        self._daily = self._daily.reindex(date_range, method = 'ffill')
        
        # creating timeframe dataframes in advance
        self._weekly, self._monthly, self._yearly = self._create_means(self._daily)
        
        # creating jefit timeframe dataframes in advance
        if jefit:
            self._historical_performance = self._create_historical_performance()
            self._max_prs = self._calc_max_prs()
            self._lifts_daily, self._lifts_daily_no_body = self._calc_lifts_daily()
            self._lifts_weekly, self._lifts_weekly_no_body = self._calc_lifts_weekly()
            self._lifts_monthly, self._lifts_monthly_no_body = self._calc_lifts_monthly()
            self._lifts_yearly, self._lifts_yearly_no_body = self._calc_lifts_yearly()

        self._calGoal = int(self._profile.CalGoal)
        self._proRatio = self._profile.ProteinRatio
        self._fatRatio = self._profile.FatRatio
        self._carbRatio = self._profile.CarbRatio
        self._weeklyChange = self._profile.WeeklyChange
        self._activity = str(self._profile.Activity)
        self._gender = str(self._profile.Gender)
        self._goalWeight = self._profile.GoalWeight
        self._goalBodyfat = None
        self._imperial = bool(self._profile.Imperial)

        if self._imperial:
            self._wgtunit = 'lbs'
            self._hgtunit = 'inches'
        else:
            self._wgtunit = 'kgs'
            self._hgtunit = 'cm'

        self._height = self._profile.Height
        self._age = self._profile.Age

        # finding most recent entries for weight and bodyfat
        try:
            self._weight = self._daily.loc[self._daily.Weight.last_valid_index()].Weight
        except:
            self._weight = 400
            print('No weight logs found. Please use the set_weight method to enter one manually. Defaulting to 400.')
        try:
            self._bodyfat = self._daily.loc[self._daily.Bodyfat.last_valid_index()].Bodyfat
        except:
            self._bodyfat = 35
            print('No bodyfat logs found. Please use the set_bodyfat method to enter one manually. Defaulting to 35.')
        self._fatlbs = round(self._weight * self._bodyfat/100, 2)

        self._tdee = self._calc_tdee()
        self._otdee = self._calc_otdee('daily', 7)
        self._calc_activity_level()
        self._calc_calories()
        self.set_macros(self._proRatio, self._fatRatio, self._carbRatio)

        # save default p-ratio values and resultant constants
        self._pratio_male_def, self._pratio_female_def = [0, 100.0/3], [-7.0/3, 100.0/3]
        self._pratio_curve_def = np.array([[.60, 0.05],
                                        [.40, 0.075],
                                        [.30, 0.1],
                                        [.25, 0.12],
                                        [.20, 0.15],
                                        [.15, 0.2],
                                        [.10, 0.3],
                                        [.05, 0.6],
                                        [.03, 1]])

        # set default p-ratio constants for user
        self._const_loss = 20
        self._pratio_curve = self._pratio_curve_def
        if self._gender == 'male':
            self._pratio_const = self._pratio_male_def
        else:
            self._pratio_const = self._pratio_female_def

    def _create_means(self, mfp_daily):
        # maybe useless fn
        # weekly means, indexed by start of week i.e. 6/14 Sunday includes 6/14-6/20(Sun-Sat)
        offset = datetime.timedelta(days=1)
        mfp_weekly = mfp_daily.resample('W-SAT', how='mean', label='left', loffset=offset)

        # monthly means, indexed by start of month i.e. 6/2015 covers 6/1/15-6/30/15
        mfp_monthly = mfp_daily.resample('MS', how='mean')

        # annual means, indexed by start of year i.e. 2015/1/1 includes all of 2015 data
        mfp_yearly = mfp_daily.resample('AS', how='mean')

        return mfp_weekly, mfp_monthly, mfp_yearly

    def _check_timeframe(self, timeframe):
        '''
        Translates an input timeframe to output correctly and catch edgecases which will default to daily
        '''
        timeframedict = {'daily': self._daily, 'weekly': self._weekly, 'monthly': self._monthly, 'yearly': self._yearly}
        timeframe = timeframe.lower()
        if timeframe not in timeframedict.keys():
            print('Timeframe not recognized as daily/weekly/monthly/yearly. Defaulting to daily, please try again.')
            return timeframedict['daily']
        return timeframedict[timeframe].copy()

    def _calc_tdee(self, summary=True):
        '''
        Calculates theoretical TDEE based on research formula given weight, gender, height, and activity level
        '''
        activity_mult = {'sedentary': 1.2, 'light': 1.375, 'moderate': 1.55, 'heavy': 1.7, 'extreme': 1.9}
        if self._gender.lower() == 'male':
            tdee = (9.99 * self._weight + 6.25 * self._height - 4.92 * self._age + 5) * activity_mult[self._activity]
        else:
            tdee = (9.99 * self._weight + 6.25 * self._height - 4.92 * self._age - 161) * activity_mult[self._activity]

        if summary:
            print('Predicted TDEE for a ' + self._gender.lower() + ' weighing ' + str(self._weight) + ' ' + self._wgtunit +
                ' and measuring ' + str(self._height) + ' ' + self._hgtunit + ' at age', self._age,  'with an activity level '
                'described as ' + self._activity.lower() + ' is ' + str(int(tdee)) + '.')
            print('')

        return int(tdee)

    def _calc_calories(self):
        '''
        Provides survey information during initialization including whether set values are in range using profile information
        '''
        if not self._imperial:
            calsfat = 68.8
            mult = 3500.0/0.4536
            cals_week = self._weeklyChange*mult
            tdee_loss = (self._tdee - self._calGoal)*7/mult
            otdee_loss = (self._otdee - self._calGoal)*7/mult
        else:
            calsfat = 31.4
            mult = 3500.0
            cals_week = self._weeklyChange*mult
            tdee_loss = (self._tdee - self._calGoal)*7/mult
            otdee_loss = (self._otdee - self._calGoal)*7/mult

        calc_goal = int(self._otdee - cals_week/7)
        max_deficit = int(self._fatlbs * calsfat)

        print('Your caloric goal is currently set to', str(self._calGoal) + '.')

        if abs(calc_goal - self._calGoal) < 150:
            print('This value is reasonably close to our recommendation of', str(calc_goal), 'daily calories for a '
            'weekly loss of', self._weeklyChange, self._wgtunit + '.')
        else:
            print('For a weekly change of ' + str(self._weeklyChange) + ' ' + self._wgtunit + ', it is recommended you '
            'change your daily caloric intake to ' + str(calc_goal) + '.')

        print('')
        print('With a theoretical TDEE of', str(self._tdee), 'you will lose', str(tdee_loss), self._wgtunit,
              'per week.')
        print('With the empirically calculated oTDEE of', self._otdee, 'you will lose', otdee_loss, self._wgtunit,
              'per week.')
        print('')
        print('The maximal daily caloric deficit for fat loss is approximately ' + str(calsfat) + ' cals per ' +
              self._wgtunit[:2] + ' of fat. (See http://www.ncbi.nlm.nih.gov/pubmed/15615615)')
        print('With ' + str(self._fatlbs) + ' ' + self._wgtunit + ' of fat, you can afford a maximum deficit of ' +
              str(max_deficit) + ' cals/day, or ' + str(max_deficit*7/mult) + ' ' + self._wgtunit + '/week.')
        print('')

        if cals_week/7 > max_deficit:
            print('Caloric deficit required to lose ' + str(self._weeklyChange) + ' ' + self._wgtunit + ' per week is greater than what would reasonably come from fat stores.')
            print('As such, you will likely be losing lean body mass (muscle), hindering your efforts to lower bodyfat aka "tone".')
            print('')
            print('It is recommended to lower the weekly weight loss goal to a maximum of ' + str(max_deficit*7/mult) + ' ' + self._wgtunit + '.')
        else:
            pass

    def _calc_otdee(self, timeframe, window_size):
        '''
        Calculating observed TDEE using empirical data. Will warn if missing > 25% data
        Sets TDEE value, oTDEE dataframe, 
        '''
        mfp_df = self._check_timeframe(timeframe)
        # copy df so we don't modify it outside the fn
        mfp_df = mfp_df[['Weight', 'NetCals']]

        # attempt to solve missing data in either cals or weight
        mfp_df.dropna(axis=0, how='any', inplace=True)

        # calculate difference between weights separated by window_size then use that to estimate calorie
        # deficit or surplus per day assuming 3500cal/lb difference
        if self._imperial:
            mult = 3500
        else:
            mult = 3500/0.4536
        mfp_df['WeightDiffCals'] = mfp_df.Weight.diff(window_size)*mult/window_size
        #print(mfp_df)

        # shift weight results up one day so calories of a day/period corresponds to weight of the morning after
        mfp_df[['Weight', 'WeightDiffCals']] = mfp_df[['Weight', 'WeightDiffCals']].shift(-1)

        # calculate moving average of cals with given window size on any given date
        mfp_df['MeanCals'] = pd.rolling_mean(mfp_df.NetCals, window_size)

        # estimate otdee for all overlapping periods
        mfp_df['oTDEE'] = mfp_df.MeanCals - mfp_df.WeightDiffCals

        # then take the average of those averages THIS IS FOR MORE SMOOTHING
        # mfp_df['MeanoTDEE'] = pd.rolling_mean(mfp_df.oTDEE, smoothing_window)
        # mfp_df.MeanoTDEE = mfp_df.MeanoTDEE.shift(-smoothing_window+1
        # shift all data so periods line up with their starting dates
        # mfp_df = mfp_df.shift(-window_size+1)

        # clears data from before 7 day window value returned
        mfp_df = mfp_df.dropna()

        # accurately count days since start of data
        mfp_df['DeltaDays'] = ((mfp_df.index - mfp_df.index[0]).days).astype(int)

        # regress over all data
        X = mfp_df.DeltaDays
        X = sm.add_constant(X)
        fit1 = sm.OLS(mfp_df.oTDEE, X).fit()

        # regress over last 14 days
        X2 = mfp_df.DeltaDays[-14:]
        X2 = sm.add_constant(X2)
        fit2 = sm.OLS(mfp_df.oTDEE[-14:], X2).fit()

        # currentotdee = int(fit1.predict()[-1]) #  change to predicted otdee, use 14 day val instead
        self._otdeeDF = mfp_df
        self._otdeeFit = fit1
        self._otdeeFit2 = fit2

        mean_last14 = int(self._otdeeDF.oTDEE[-14:].mean())

        return mean_last14

    def _calc_activity_level(self):
        '''
        Estimates your actual activity level and compares to self-reported value
        '''
        activity_mult = {'sedentary': 1.2, 'light': 1.375, 'moderate': 1.55, 'heavy': 1.7, 'extreme': 1.9}
        bmr = self._tdee / activity_mult[self._activity]
        avg_mult = round(self._otdee / bmr,2)
        nearest = 5
        for key, val in activity_mult.items():
            if abs(val-avg_mult) < nearest:
                nearest = abs(val-avg_mult)
                nearkey = key

        if nearkey != self._activity:
            print('From your oTDEE, your activity level more closely resembles ' + nearkey + ' activity. Consider '
                'changing your activity level.')

    def _calc_pratio_constant(self):
        '''
        Calculates constants to be used in P-Ratio formula f(BF)
        '''
        def func(B, a, b):
            return 1/(a+b*B)

        pratio = self._pratio_curve
        pratio = np.c_[pratio, 1.0/pratio[:, -1]]
        pratio = pd.DataFrame(pratio, columns=['Bodyfat', 'P-Ratio', 'PInverse'])

        #print(pratio)
        x = pratio.iloc[:, 0]
        y = pratio.iloc[:, 1]

        popt, pcov = curve_fit(func, x, y)
        self._pratio_const = popt

        return popt

    def _calc_pratio_weight(self, final_bodyfat, init_bodyfat, init_weight):
        '''
        Uses input p-ratio curve to calculate at what weight reach your goal bodyfat
        '''
        B = sympy.Symbol("B")
        a, b = self._pratio_const
        a, b = Fraction(a).limit_denominator(), Fraction(b).limit_denominator()
        a, b = float(a), float(b)
        a, b = sympy.sympify(a), sympy.sympify(b)

        try:        
            integral = sympy.integrate((1-B-1/(a+b*B))**(-1), (B, round(init_bodyfat/100.0,2), round(final_bodyfat/100.0,2)))
            integral = np.float(integral)
        except:
            # most accurate numbers failed PolyDivisError, round hard
            a, b = round(a,0), round(b,0)
            integral = sympy.integrate((1-B-1/(a+b*B))**(-1), (B, round(init_bodyfat/100.0,2), round(final_bodyfat/100.0,2)))
            integral = np.float(integral)

        final_weight = np.exp(integral + np.log(init_weight))

        return final_weight

    def set_macros(self, pratio, fratio, cratio):
        '''
        Manually set goal macronutrient ratios
        '''
        if pratio+fratio+cratio == 100:
            mult = 1
        elif pratio+fratio+cratio == 1:
            mult = 100
        else:
            print('Percentages do not add up to 100%. Please check your math.')
            print('')
            return
        self._proRatio = pratio*mult
        self._fatRatio = fratio*mult
        self._carbRatio = cratio*mult
        self._proGoal = int(self._calGoal * self._proRatio/(100.0*4))
        self._fatGoal = int(self._calGoal * self._fatRatio/(100.0*9))
        self._carbGoal = int(self._calGoal * self._carbRatio/(100.0*4))

        # print('Macronutrient goals have been modified. Recomputing macronutrient totals.')
        # print('')
        print('Current macronutrient goals are', str(self._proGoal) + "/" + str(self._fatGoal) + '/' +
              str(self._carbGoal), 'P/F/C in grams, with proportions', str(self._proRatio) + '/' + str(self._fatRatio)
              + '/' + str(self._carbRatio) + '.')

        if self._imperial and self._proGoal/self._weight > 0.82:
            print('Your protein goal is at least 0.82g/lb of body weight, appropriate according to modern research. (See http://www.ncbi.nlm.nih.gov/pubmed/22150425)')
        elif not self._imperial and self._proGoal/self._weight > 1.8:
            print('Your protein goal is at least 1.8g/kg of body weight, appropriate according to modern research. (See http://www.ncbi.nlm.nih.gov/pubmed/22150425)')
        else:
            print('Your protein goal is lower than 0.82g/lb or 1.8g/kg, which may inhibit muscle growth or lean body'
                  'mass retention. It is recommended to increase protein intake. (See http://www.ncbi.nlm.nih.gov/pubmed/22150425)')
        print('')

    def set_goal_calories(self, calories):
        '''
        Manually set goal calories
        '''
        self._calGoal = calories

        print('Caloric goal has been modified. Recomputing macronutrient totals and caloric recommendations.')
        print('')

        self.set_macros(self._proRatio, self._fatRatio, self._carbRatio)

        self._calc_calories()

    def set_weeklychange(self, weeklychange):
        '''
        Manually set goal weight loss per week
        '''
        self._weeklyChange = weeklychange

        print('Weekly weight change has been modified. Recomputing caloric recommendations.')
        print('')

        self._calc_calories()

    def set_activitylevel(self, activitylevel):
        '''
        Manually set an activity level. Options and their multiplicative valuesactivity = {'sedentary': 1.2, 'light': 1.375, 'moderate': 1.55, 'heavy': 1.7, 'extreme': 1.9}
        '''
        self._activity = activitylevel.lower()

        print('Activity level has been modified. Recomputing caloric recommendations.')
        print('')

        self._tdee = self._calc_tdee()

        self._calc_calories()

    def set_otdee(self, calories):
        '''
        in the case one would like to use a custom otdee value, i.e. the mean oTDEE or last 14 days oTDEE instead of
        the calculated linear regression oTDEE
        '''
        self._otdee = calories

        print('Observed TDEE has been modified. Recomputing caloric recommendations.')
        print('')

        self._calc_calories()

    def set_goal_weight(self, goalweight):
        '''
        Change GOAL weight from imported value
        '''
        self._goalWeight = goalweight

        print('Goal Weight has been modified. Recomputing approximate goal date and weight change plot.')
        print('')

        self.plot_weight('daily', showgoal=True)

    def set_bodyfat(self, bodyfat):
        '''
        Change CURRENT body fat from imported value
        '''
        self._bodyfat = bodyfat

        self._fatlbs = round(self._weight * self._bodyfat/100, 2)

        print('Bodyfat has been modified.')
        print('')

    def set_pratio_curve(self, defvals=None):
        '''
        Set your own p-value estimates, entered in matrix form where 
        column 1 = bodyfat in %
        column 2 = ratio of loss in lbm per change in weight
        
        np.array([[.60, 0.05],
                [.40, 0.075],
                [.30, 0.1],
                [.25, 0.12],
                [.20, 0.15],
                [.15, 0.2],
                [.10, 0.3],
                [.05, 0.6],
                [.03, 1]])
        
        Will recompute constants and plot new custom curve
        '''
        if defvals == None:
            defvals = self._pratio_curve_def
        self._pratio_curve = defvals

        self._pratio_const = self._calc_pratio_constant()

        print('P-Ratio values have been modified.')
        print('')

    def set_goal_bodyfat(self, bodyfat, init_bodyfat=None, init_weight=None, const_loss=None):
        '''
        Change GOAL body fat from imported value
        '''
        self._goalBodyfat = bodyfat

        if not const_loss:
            const_loss = self._const_loss

        def present_results(pratio, const):
            pratio.insert(0, round(init_weight - pratio[0], 2))  # total weight lost
            pratio.append(round(pratio[1] * (1 - bodyfat/100), 2))  # final lbm
            pratio.append(round(pratio[2] - init_weight * (1 - init_bodyfat/100), 2))  # change in lbm
            pratio.append(round(abs(pratio[3]*100/pratio[0]), 2))  # effective % lbm loss

            const.insert(0, round(init_weight - const[0], 2))  # total weight lost
            const.append(round(const[1] * (1 - bodyfat/100), 2))  # final lbm
            const.append(round(const[2] - init_weight * (1 - init_bodyfat/100), 2))  # change in lbm
            const.append(const_loss)  # effective % lbm loss

            no_loss = [round((init_weight * (1 - init_bodyfat/100))/(1 - bodyfat/100), 2)]
            no_loss.append(init_weight - no_loss[0])

            self.plot_pratio()

            print('Goal: To drop from ' + str(init_weight) + ' ' + self._wgtunit + ' and ' + str(init_bodyfat) + '% bodyfat to ' + str(bodyfat) + '% bodyfat')
            print('')
            print('Using the P-Ratio curve to calculate LBM loss during your diet:')
            print('Total Weight Lost: ' + str(pratio[0]))
            print('Final Weight: ' + str(pratio[1]))
            print('Final LBM: ' + str(pratio[2]))
            print('Change in LBM: ' + str(pratio[3]))
            print('Effective Rate of LBM Loss (%): ' + str(pratio[4]))
            print('')
            print('Using a flat 20% loss rate to calculate LBM loss during your diet:')
            print('Total Weight Lost: ' + str(const[0]))
            print('Final Weight: ' + str(const[1]))
            print('Final LBM: ' + str(const[2]))
            print('Change in LBM: ' + str(const[3]))
            print('Effective Rate of LBM Loss (%): ' + str(const[4]))
            print('')
            print('If you can manage to lose 100% fat, congratulations! You will reach your goal in only ' +
                  str(no_loss[1]) + ' ' + self._wgtunit + ' at a final weight of ' + str(no_loss[0]) + ' ' +
                  self._wgtunit + '.')
            print('')

        if not init_bodyfat:
            init_bodyfat = self._bodyfat
        if not init_weight:
            init_weight = self._weight

        # p-ratio method, answer gets rounded in pratio weight fn
        pratio_final_weight = [self._calc_pratio_weight(bodyfat, init_bodyfat, init_weight)]

        # const loss method
        const_final_weight = [round(init_weight - init_weight*(bodyfat/100-init_bodyfat/100)/(bodyfat/100-1+const_loss/100), 2)]

        present_results(pratio_final_weight, const_final_weight)

    def set_deadline(self, date, goalweight=None):
        '''
        Set a deadline date for reaching your goal weight to receive precise recommendations on intake
        Optionally include a new goal weight to base calculations on
        '''
        if not goalweight:
            goalweight = self._goalWeight
        end_date = datetime.datetime.strptime(date, '%Y-%m-%d').date()
        
        if end_date < datetime.date.today():
            print("Please choose an end date in the future relative to today")
            return None

        # days_til - 1 to discount day of and the final day, so plan to start diet tomorrow and wake up goal weight morning of
        days_til = (end_date - datetime.date.today()).days - 1

        if self._imperial:
            mult = 3500.0
        else:
            mult = 3500/0.4536
        daily_deficit = int((self._weight - goalweight) * mult / days_til)

        weight_start = self._weight
        self._weight = goalweight
        tdee_final = self._calc_tdee(summary=False)
        self._weight = weight_start
        delta_tdee = self._tdee - tdee_final

        print('------------------------------------------------------------')
        print('Goal: Lose ' + str(self._weight - goalweight) + ' ' + self._wgtunit + ' by ' + date + '.')
        print('')
        print('The following calculations assume you begin the caloric deficit tomorrow, and wake up on deadline '
              'morning weighing your goal weight.')
        print('Please keep in mind that there can be major fluctuations in weight on any particular day up to and '
              'including the goal date due to water retention and other cyclical reasons. If "weight" and not '
              '"weight loss" is of prime importance, begin eating lower carbohydrates in the final week.')
        print('')
        print('You require an initial daily deficit of ' + str(daily_deficit) + ' calories (this is NOT how much to eat).')
        print('Using your theoretical TDEE of ' + str(self._tdee) + ', you should start with a daily goal of under '
            + str(self._tdee - daily_deficit) + ' calories, tapering down to ' + str(self._tdee - daily_deficit
            - delta_tdee) + ' by the final day.')
        if self._otdee:
            print('Using your observed TDEE of ' + str(self._otdee) + ', you should start with a daily goal of under '
                + str(self._otdee - daily_deficit) + ' calories, tapering down to ' + str(self._otdee - daily_deficit
                - delta_tdee) + ' by the final day.')
        print('------------------------------------------------------------')
        print('')
        
        # plot predicted cals/weight over time
        x = [datetime.datetime.today() + datetime.timedelta(days=1) + datetime.timedelta(days=x) for x in range(days_til+1)]
        yotdee = np.linspace(self._otdee + daily_deficit, self._otdee + daily_deficit - delta_tdee, days_til+1)
        ytdee = np.linspace(self._tdee + daily_deficit, self._tdee + daily_deficit - delta_tdee, days_til+1)
        ywgt = np.linspace(self._weight, goalweight, days_til+1)

        # add noise to weight signal based on std of empirical data        
        # np.random.seed(0)
        ywgt[1:-1] += np.random.normal(loc = 0, scale = self._daily.Weight.std(), size = days_til - 1)

        fig = plt.figure()
        ax1 = fig.add_subplot(111)
        ax1.plot(x, ywgt, 'black', lw=3)
        ax1.set_xlabel('Date')
        ax1.set_ylabel('Weight (' + self._wgtunit + ')')
        plt.xticks(rotation=45)
        ax2 = ax1.twinx()
        
        ax2.set_ylabel('Calorie Goal')
        ax2.set_xlim(xmin=x[0], xmax=x[-1])
        ax2.set_ylim(ymin=(min(ytdee[-1], yotdee[-1]) - 100), ymax=(max(ytdee[0], yotdee[0]) + 100))
        
        # aligning twin axes
        nticks = 11
        ax1.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))
        ax2.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))
        
        ax2.plot(0, 0, color = 'black', lw=3, label='Predicted Weight Change')
        ax2.plot(x, yotdee, 'blue', lw=2, alpha=0.5, label='Prescribed Calorie Goal via oTDEE')
        ax2.plot(x, ytdee, 'red', lw=2, alpha=0.5, label='Prescribed Calorie Goal via TDEE')
        ax2.legend()
        plt.show()

    def set_weight(self, weight):
        '''
        Change CURRENT weight from imported value
        '''
        self._weight = weight

        print('Weight has been modified.')
        print('')

    def plot_calories(self, timeframe='daily', window_size=7):
        '''
        Plot caloric intake over time based on input timeframe and compare with your goal, 
        empirical mean, and a running average of size window_size
        '''
        mfp_df = self._check_timeframe(timeframe)
        mv_avg = pd.stats.moments.rolling_mean(mfp_df.Calories, window=window_size)
        mv_avg_label = 'Moving Average W = ' + str(window_size)
        calcmean = int(mfp_df['Calories'].mean())
        mean_label = "Mean = " + str(calcmean)
        goal_label = "Goal = " + str(self._calGoal)
        plt.bar(mfp_df.index, mfp_df.Calories, alpha = 0.2, width=1.0)
        plt.ylim(ymin=500)
        plt.axhline(calcmean, color='r', label=mean_label, linewidth=3)
        plt.axhline(self._calGoal, color='g', label=goal_label, linewidth=3)
        plt.plot(mv_avg.index, mv_avg, 'b-', linewidth=3, label=mv_avg_label)
        plt.legend(loc=8)
        plt.xlabel('Date')
        plt.ylabel('Calories')
        title_label = ('Caloric Intake from ' + str(mfp_df.index[0].month) + '/' + str(mfp_df.index[0].year)
                       + ' to ' + str(mfp_df.index[-1].month) + '/' + str(mfp_df.index[-1].year))
        plt.title(title_label)
        plt.xticks(rotation=45)
        plt.show()

    def plot_bodyfat(self, timeframe = 'daily', window_size=7):
        '''
        Plot body fat over time based on input timeframe and compare with your goal, 
        empirical mean, and a running average of size window_size
        '''
        if self._goalBodyfat == None:
            self._goalBodyfat = self._bodyfat
        mfp_df = self._check_timeframe(timeframe)
        mv_avg = pd.stats.moments.rolling_mean(mfp_df.Bodyfat, window=window_size)
        mv_avg_label = 'Moving Average W = ' + str(window_size)
        calcmean = int(mfp_df['Bodyfat'].mean())
        mean_label = "Mean = " + str(calcmean)
        goal_label = "Goal = " + str(self._goalBodyfat)
        plt.bar(mfp_df.index, mfp_df.Bodyfat, alpha = 0.2, width = 1.0)
        plt.axhline(calcmean, color='r', label=mean_label, linewidth=3)
        plt.axhline(self._goalBodyfat, color='g', label=goal_label, linewidth=3)
        plt.ylim(ymin=(max([3, self._goalBodyfat - 5])))
        plt.plot(mv_avg.index, mv_avg, 'b-', linewidth=3, label=mv_avg_label)
        plt.legend(loc=8)
        plt.xlabel('Date')
        plt.ylabel('Bodyfat')
        title_label = ('Bodyfat from ' + str(mfp_df.index[0].month) + '/' + str(mfp_df.index[0].year)
                       + ' to ' + str(mfp_df.index[-1].month) + '/' + str(mfp_df.index[-1].year))
        plt.title(title_label)
        plt.xticks(rotation=45)
        plt.show()
        
    def plot_macro(self, timeframe='daily', macro='Protein', window_size=7):
        '''
        Plot macronutrient intake for a particular macro over time based on input timeframe 
        and compare with your goal, empirical mean, and a running average of size window_size
        
        Valid macros = 'protein', 'carbs', 'fat'
        '''
        mfp_df = self._check_timeframe(timeframe)
        macro = macro.lower().capitalize()
        try:
            mv_avg = pd.stats.moments.rolling_mean(mfp_df[macro], window=window_size)
            if macro == 'Carbs':
                goalVal = self._carbGoal
            elif macro == 'Fat':
                goalVal = self._fatGoal
            else:
                goalVal = self._proGoal
        except:
            print('Macro not found, defaulting to Protein. Try again.')
            macro = 'Protein'
            goalVal = self._proGoal
            mv_avg = pd.stats.moments.rolling_mean(mfp_df[macro], window=window_size)
        mv_avg_label = 'Moving Average W = ' + str(window_size)
        calcmean = int(mfp_df[macro].mean())
        mean_label = "Mean = " + str(calcmean) + 'g'
        goal_label = "Goal = " + str(goalVal) + 'g'
        plt.bar(mfp_df.index, mfp_df[macro], alpha=0.2, width=1.0)
        plt.axhline(calcmean, color='r', label=mean_label, linewidth=3)
        plt.axhline(goalVal, color='g', label=goal_label, linewidth=3)
        plt.plot(mv_avg.index, mv_avg, 'b-', linewidth=3, label=mv_avg_label)
        plt.legend(loc=8)
        plt.xlabel('Date')
        plt.ylabel(macro + ' (g)')

        title_label = (macro + ' Intake from ' + str(mfp_df.index[0].month) + '/' + str(mfp_df.index[0].year)
                       + ' to ' + str(mfp_df.index[-1].month) + '/' + str(mfp_df.index[-1].year))
        plt.title(title_label)
        plt.xticks(rotation=45)
        plt.show()

    def plot_macro_pie(self, timeframe='monthly', date=None):
        '''
        Plot macronutrient pie showing % of caloric intake over given time period
        Dates must be "first of" i.e. if timeframe = 'monthly', date must be the first of the month
        '''
        mfp_df = self._check_timeframe(timeframe)
        if date == None:
            date = pd.datetime(self._daily.index[-1].year, self._daily.index[-1].month, 1)
        try:
            slices = [mfp_df.PMacro.loc[date], mfp_df.FMacro.loc[date], mfp_df.CMacro.loc[date]]
        except:
            print('No data catalogued under ' + date + '. If using a timeframe larger than daily, the data may be '
                'indexed by the start of the week/month/year.')
            return
        slicecols = ['lightskyblue', 'lightcoral', 'gold']
        plt.figure(figsize=(10,10))
        pie = plt.pie(slices, labels=['Protein', 'Fat', 'Carbs'], autopct='%1.1f%%', colors=slicecols, startangle=90)
        for pie_label in pie[1]:
            pie_label.set_fontsize(15)
        for pie_wedge in pie[0]:
            pie_wedge.set_edgecolor('white')
        datedict = {'daily': '', 'weekly': 'week starting ', 'monthly': 'month starting ', 'yearly': 'year starting '}
        plt.title('Macros on ' + datedict[timeframe.lower()] + date)
        plt.show()

    def plot_weight(self, timeframe='daily', showgoal=False):
        '''
        Plot weight over time based on input timeframe, optionally include and 
        adjust axis limits to show goal weight
        Compares raw data to regressions over multiple time periods
        '''
        mfp_df = self._check_timeframe(timeframe)
        # make copy of df so we don't modify the version outside of the fn
        mfp_df = pd.DataFrame(mfp_df.Weight)

        # attempt to remedy missing weight value... needs testing, especially extradays stuff
        mfp_df.dropna(axis=0, how='any', inplace=True)

        # plot weight vs date and set up axes
        plt.plot(mfp_df.index, mfp_df.Weight, lw=2, label='Raw Weight Data')
        plt.ylabel('Weight')
        plt.xlabel('Date')
        plt.title('Weight over Time')
        plt.xticks(rotation=45)

        # plot goal weight line if desired and set proper ylim
        if showgoal:
            plt.axhline(y=self._goalWeight, label=('Goal Weight: ' + str(self._goalWeight) + ' ' + self._wgtunit))
            if self._goalWeight > mfp_df.Weight[-1]:
                plt.ylim(ymax=self._goalWeight+5)
            else:
                plt.ylim(ymin=self._goalWeight-5)

        # calculations to predict 1/3 of the graph into the future
        extradays = int((mfp_df.index[-1]-mfp_df.index[0]).days/2)

        # pandas doesn't automatically convert time series to integer predictor...
        #mfp_df['DeltaDays'] = range(len(mfp_df.index))
        mfp_df['DeltaDays'] = ((mfp_df.index - mfp_df.index[0]).days).astype(int)
        regress = pd.stats.api.ols(y=mfp_df.Weight, x=mfp_df.DeltaDays)

        # pandas also doesn't allow model predicting with new data... so... do it manually
        numdays = np.arange((mfp_df.index[-1] - mfp_df.index[0]).days + extradays)
        y_pred = numdays*regress.beta[0]+ regress.beta[1]
        y_pred_index = [mfp_df.index[0] + datetime.timedelta(days=x) for x in range(len(numdays))]
        plt.plot(y_pred_index, y_pred, label=('All data: ' + str(round(regress.beta[0]*7.0, 2)) + self._wgtunit + '/week'))

        # repeat regression for last 14 days data
        regress2 = pd.stats.api.ols(y=mfp_df.Weight[-14:], x=mfp_df.DeltaDays[-14:])
        y2_pred = np.arange(mfp_df.DeltaDays[-14], extradays + 14 + mfp_df.DeltaDays[-14])*regress2.beta[0] + regress2.beta[1]
        y2_pred_index = y_pred_index[-(14+extradays):]

        plt.plot(y2_pred_index, y2_pred, label=('Last 14: ' + str(round(regress2.beta[0]*7.0, 2)) + self._wgtunit + '/week'))

        print('Predicted Weight in ' + str(extradays) + ' days (' + str(y_pred_index[-1].date()) + '):')
        print(str(round(y_pred[-1], 1)) + ' based on all data')
        print(str(round(y2_pred[-1], 1)) + ' based on last 14 days')

        print('Total weight lost since ' + str(mfp_df.index[0].date()) + ': ' + str(mfp_df.Weight[-1] - mfp_df.Weight[0])
              + ' ' + self._wgtunit + '.')

        goalwgtdays = int((self._goalWeight - regress2.beta[1]) / regress2.beta[0])
        goalwgtday = mfp_df.index[0] + datetime.timedelta(days=goalwgtdays)

        print('If your weight change proceeds consistent with the last 14 days, you will reach your goal weight of',
              str(self._goalWeight), self._wgtunit, 'in approximately', str((goalwgtday - mfp_df.index[-1]).days), 'days ('
              + str(goalwgtday.date()) + ').')

        plt.legend()
        plt.show()

    def plot_macro_weight(self, timeframe='daily', macro='carbs'):
        '''
        Plots the immediate effect of a particular macronutrient intake on the next day's weight
        timeframe: str, 'daily', 'weekly', 'monthly', 'yearly'
        macro: str, 'carbs', 'protein', 'fat', 'sodium', ...
        '''
        mfp_df = self._check_timeframe(timeframe)
        macro = macro.lower().capitalize()

        mfp_df = mfp_df[['Weight', macro]]
        mfp_df['WeightDiff'] = mfp_df.Weight.diff(1)

        # shift weight results up one day so calories of a day/period corresponds to weight of the morning after
        mfp_df[['Weight', 'WeightDiff']] = mfp_df[['Weight', 'WeightDiff']].shift(-1)

        # attempt to remedy missing weight value... needs testing
        mfp_df.dropna(axis=0, how='any', subset=['WeightDiff', macro], inplace=True)

        fig = plt.figure()
        ax1 = fig.add_subplot(111)
        ax1.scatter(mfp_df.index, mfp_df.WeightDiff, color='black', alpha = 1)
        ax1.plot(mfp_df.index, mfp_df.WeightDiff, color='red', lw=5, alpha = 0.5, label = "Change in Weight")
        ax1.set_xlabel('Date')
        ax1.set_ylabel('Weight')
        plt.xticks(rotation=45)
        plt.xlim(xmin=mfp_df.index[0], xmax=mfp_df.index[-1])
        ax2 = ax1.twinx()
        
        # aligning twin axes
        nticks = 11
        ax1.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))
        ax2.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))
        
        # checking if macro input a legitimate input
        try:
            ax2.bar(mfp_df.index, mfp_df[macro], alpha=0.4, label = "Macro Intake")
        except:
            print('Macro not found, defaulting to Protein. Try again.')
            macro = 'Protein'
            ax2.bar(mfp_df.index, mfp_df[macro], alpha=0.4)
        ax2.set_ylabel(macro + ' (g)')
        ax2.set_xlim([mfp_df.index[0], mfp_df.index[-1]])
        
        corr = str(round(mfp_df[macro].corr(mfp_df['WeightDiff']), 2))
        
        plt.title('Effect of ' + macro + ' on Immediate Weight, Correlation = ' + corr)
        plt.legend()
        plt.show()

    def plot_normalized_macros(self, timeframe='weekly'):
        '''
        Compare your macronutrient intake to your goals based on weekly timeframe.
        Delivers the ratio of actual intake/goal intake
        '''
        mfp_df = self._check_timeframe(timeframe)
        # make copy so we don't modify df outside fn
        mfp_df = mfp_df.copy()

        # normalize macros actual/goal values
        mfp_df['ProNorm'] = mfp_df['Protein']/self._proGoal
        mfp_df['FatNorm'] = mfp_df['Fat']/self._fatGoal
        mfp_df['CarbsNorm'] = mfp_df['Carbs']/self._carbGoal

        # to arrange bars side by side
        bar_width = 0.2
        x_axis = np.arange(len(mfp_df.index))

        # aligning x labels with middle bar placement
        if len(mfp_df) > 40:
            plt.xticks((x_axis+2*bar_width)*5, mfp_df.index.date[::5], rotation=90)
        else:
             plt.xticks((x_axis+2*bar_width), mfp_df.index.date, rotation=45)

        plt.bar(x_axis, mfp_df.ProNorm, width=bar_width, color='lightskyblue', label='Protein')
        plt.bar(x_axis + bar_width, mfp_df.FatNorm, width=bar_width, color='lightcoral', label='Fat')
        plt.bar(x_axis + 2*bar_width, mfp_df.CarbsNorm, width=bar_width, color='gold', label='Carbs')
        plt.axhline(y=1, color='g', lw=4, alpha = 0.5)
        plt.xlabel('Date')
        plt.ylabel('Normalized Intake of Macro')
        plt.title('Ratio Intake/Goal Intake, ideal = 1')
        plt.legend(loc=9)
        plt.show()

    def plot_otdee(self, window_size=7): #timeframe
        '''
        Plots observed TDEE over time and compares with the mean over several time periods
        as well as a smoothed line by window_size
        '''
        #mfp_df = self._check_timeframe(timeframe)
        otdeelabel = 'oTDEE, ' + str(window_size) + ' day window'
        plt.plot(self._otdeeDF.index, self._otdeeDF.oTDEE, label=otdeelabel, color = 'c')
        plt.xticks(rotation=45)
        meanotdee = int(self._otdeeDF.oTDEE.mean())
        meanotdee14 = int(self._otdeeDF.oTDEE[-14:].mean())
        plt.plot(self._otdeeDF.index, self._otdeeFit.predict(), color='k', lw=3, label=(str(int(self._otdeeFit.predict()[-1])) + ': Regression, all data'))
        plt.plot(self._otdeeDF.index[-14:], self._otdeeFit2.predict(), color='r', lw=3, label=(str(int(self._otdeeFit2.predict()[-1])) + ': Regression, last 14'))
        plt.axhline(y=meanotdee, label=(str(meanotdee) + ': Mean oTDEE, all data'), color='m', lw=2)
        plt.axhline(y=meanotdee14, label=(str(meanotdee14) + ': Mean oTDEE, last 14 days'), color='y', lw=2)
        plt.xlabel('Date')
        plt.ylabel('Observed TDEE')
        plt.title('Observed TDEE using ' + str(window_size) + ' day window')
        plt.legend()
        plt.show()

    def plot_pratio(self):
        '''
        Plot P-ratio curves, includes defalut female, male, constant, and user input custom model
        '''
        def func(B, a, b):
            return 1/(a+b*B/100)

        # male default function
        xm = np.linspace((self._pratio_curve_def[:, 0]*100)[0],(self._pratio_curve_def[:, 0]*100)[-1], 100)
        ym = func(xm, *self._pratio_male_def)

        # female default function
        xf = np.linspace(((self._pratio_curve_def[:, 0] + 0.07)*100)[0], ((self._pratio_curve_def[:, 0] + 0.07)*100)[-1], 100)
        yf = func(xf, *self._pratio_female_def)

        # user input function (using set_pratio_curve
        xcustom = np.linspace((self._pratio_curve[:, 0]*100)[0], (self._pratio_curve[:, 0]*100)[-1], 100)
        ycustom = func(xcustom, *self._pratio_const)

        xcustompts = (self._pratio_curve[:, 0]*100)
        ycustompts = func(xcustompts, *self._pratio_const)

        # plotting all 3 to compare
        plt.plot(xm, ym, lw=3, color='black', label="Default Male Model")
        plt.plot(xf, yf, lw=3, color='red', label="Default Female Model")
        plt.plot(xcustom, ycustom, lw=3, color='green', label="Current User Model")
        plt.scatter(xcustompts, ycustompts, s=150, marker='+', color='green')
        plt.axhline(y=self._const_loss/100, color='blue', lw=3, label='Constant ' + str(self._const_loss) + '% Loss')
        plt.xlim(1, 45)
        plt.ylim(ymax=1.05, ymin=0)
        plt.xlabel('Bodyfat %')
        plt.ylabel('P-Ratio aka % of weight loss that will be LBM')
        plt.title("P-Ratio LBM Loss Curves")
        plt.legend()

        plt.show()

    ####### JEFIT SPECIFIC #######
    
    def get_exercise_list(self):
        print(self._lifts.ExerciseName.unique())

    def _check_timeframe_jefit(self, timeframe, body=False):
        timeframe = timeframe.lower()
        if body:
            timeframedict = {'daily': self._lifts_daily, 'weekly': self._lifts_weekly, 'monthly': self._lifts_monthly, 'yearly': self._lifts_yearly}
        else:
            timeframedict = {'daily': self._lifts_daily_no_body, 'weekly': self._lifts_weekly_no_body, 'monthly': self._lifts_monthly_no_body, 'yearly': self._lifts_yearly_no_body}
        if timeframe not in timeframedict.keys():
            print('Timeframe not recognized as daily/weekly/monthly/yearly. Defaulting to daily, please try again.')
            return timeframedict['daily']
        return timeframedict[timeframe].copy()

    def _create_historical_performance(self):
        historical_performance = self._lifts.reset_index().groupby(['Date', 'ExerciseName']).apply(max).drop(['Date',
            'Weight', 'Reps', 'Sets', 'ExerciseName', 'Bodypart', 'Set1Time', 'Set2Time', 'CardioCalsBurned', 'CardioDistance',
            'CardioSpeed', 'CardioTime'], axis=1)
        
        return historical_performance

    def _calc_max_prs(self):
        # has all time max for each exercise and date achieved
        max_prs = self._lifts.reset_index().groupby(['ExerciseName']).agg({'1RM': 'max'}).rename(columns={'1RM':'1RMRecord'})
        max_prs = pd.merge(self._lifts.reset_index()[['Date','ExerciseName','1RM']], max_prs.reset_index(), how='left', on=['ExerciseName'])
        max_prs = max_prs[max_prs['1RM'] == max_prs['1RMRecord']].drop_duplicates('ExerciseName')
        max_prs = max_prs.drop(['1RMRecord'], axis=1).set_index(['Date']).sort('ExerciseName')

        return max_prs

    def _calc_lifts_daily(self):
        lifts_daily = self._lifts.reset_index().groupby(['Date', 'Bodypart']).aggregate({'Volume': 'sum', 'Sets': 'sum', 'Reps': 'sum'})
        lifts_daily_no_body = self._lifts.reset_index().groupby(['Date']).aggregate({'Volume': 'sum', 'Sets': 'sum', 'Reps': 'sum'})
        
        return lifts_daily, lifts_daily_no_body

    def _calc_lifts_weekly(self):
        lifts_weekly = self._lifts.copy().reset_index()
        lifts_weekly['Date'] = pd.to_datetime(lifts_weekly['Date'])

        # for future index/groupby purposes
        lifts_weekly['Date2'] = lifts_weekly['Date']

        # index by start-date of week i.e Sunday 6/2 would include 6/2 to Saturday 6/8
        lifts_weekly = lifts_weekly.set_index(['Date']).groupby(['Date2', 'Bodypart']).resample('W', how='sum', label='left')

        # cleaning up some columns
        
        lifts_weekly = lifts_weekly.reset_index().set_index(['Date']).drop(['Date2', 'Weight', '1RM'], axis=1)
        
        lifts_weekly = lifts_weekly.reset_index().groupby(['Date', 'Bodypart']).aggregate(sum)

        lifts_weekly_no_body = lifts_weekly.reset_index().groupby(['Date']).aggregate(sum)
        
        return lifts_weekly, lifts_weekly_no_body

    def _calc_lifts_monthly(self):
        lifts_monthly = self._lifts.copy().reset_index()
        lifts_monthly['Date'] = pd.to_datetime(lifts_monthly['Date'])

        # for future index/groupby purposes
        lifts_monthly['Date2'] = lifts_monthly['Date']

        # index by start-date of month i.e. 6/1 includes 6/1-6/30 data
        lifts_monthly = lifts_monthly.set_index(['Date']).groupby(['Date2', 'Bodypart']).resample('MS', how='sum', label='left')

        # cleaning up some columns
        lifts_monthly = lifts_monthly.reset_index().set_index(['Date']).drop(['Date2', 'Weight', '1RM'], axis=1)

        lifts_monthly = lifts_monthly.reset_index().groupby(['Date', 'Bodypart']).aggregate(sum)

        lifts_monthly_no_body = lifts_monthly.reset_index().groupby(['Date']).aggregate(sum)

        return lifts_monthly, lifts_monthly_no_body

    def _calc_lifts_yearly(self):
        lifts_yearly = self._lifts.copy().reset_index()
        lifts_yearly['Date'] = pd.to_datetime(lifts_yearly['Date'])

        # for future index/groupby purposes
        lifts_yearly['Date2'] = lifts_yearly['Date']

        # index by start-date of year i.e. 2014 includes 1/1/14 - 12/31/14
        lifts_yearly = lifts_yearly.set_index(['Date']).groupby(['Date2', 'Bodypart']).resample('AS', how='sum', label='left')

        # cleaning up some columns
        lifts_yearly = lifts_yearly.reset_index().set_index(['Date']).drop(['Date2', 'Weight', '1RM'], axis=1)

        lifts_yearly = lifts_yearly.reset_index().groupby(['Date', 'Bodypart']).aggregate(sum)

        lifts_yearly_no_body = lifts_yearly.reset_index().groupby(['Date']).aggregate(sum)

        return lifts_yearly, lifts_yearly_no_body

    def plot_jefit_historical_performance(self, exercisename, show_weight=True):
        # has maxes for each exercise on each day
        historical_df = self._historical_performance
        exercise_history = pd.DataFrame(historical_df.loc[historical_df.index.get_level_values('ExerciseName') == exercisename])
        date_index = exercise_history.index.get_level_values('Date')
        all_time_pr, all_time_date = round(exercise_history['1RM'].max(),2), exercise_history['1RM'].idxmax()[0]

        fig = plt.figure()
        ax1 = fig.add_subplot(111)
        ax1.scatter(date_index, exercise_history['1RM'], color='red', lw=3, label='1RM Performance')
        plt.xticks(rotation=45)
        ax1.set_xlim(xmax=date_index[-1], xmin=date_index[0])
        ax1.set_ylim(ymax=all_time_pr + 5, ymin=exercise_history['1RM'].min() - 5)
        ax1.set_ylabel('Calculated 1 RM')
        ax1.set_xlabel('Date')
        
        if show_weight:
            ax2 = ax1.twinx()
            ax2.plot(self._daily.index, self._daily.Weight, color='black', label='Weight', lw=2)
            ax2.set_ylabel('Weight')
            ax2.set_xlim(xmax=date_index[-1], xmin=date_index[0])
            ax2.set_ylim(ymax=self._daily.Weight.max(), ymin=self._daily.Weight.min())
            
            # aligning twin axes
            nticks = 11
            ax1.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))
            ax2.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))
        ax1.axhline(y=all_time_pr, lw=3, color='green', label=('1RM ' + str(all_time_pr) + ' set on ' + str(all_time_date.date())))
        plt.title('Historical Performance on ' + exercisename + ' (1RM)')
        ax1.legend(loc=8)
        ax2.legend(loc=9)
        plt.show()

    def plot_jefit_bodypart_pie(self, timeframe='monthly', date=None):
        '''
        Plots a pie chart distribution of exercises done by body part. Requires a date 
        corresponding to Sunday if weekly timeframe, first of the month if monthly, 
        and Jan 1st if annually
        '''
        if date == None:
            date = pd.datetime(self._daily.index[-1].year, self._daily.index[-1].month, 1)
        def make_autopct(values):
            # displays % (sets) for each slice in pie
            def my_autopct(pct):
                total = sum(values)
                val = int((pct*total/100.0) + 0.5)
                return '{p:.2f}%  ({v:d})'.format(p=pct,v=val)
            return my_autopct

        lifts_df = self._check_timeframe_jefit(timeframe, body=True)
        day = lifts_df.loc[(lifts_df.index.get_level_values('Date') == date)]
        if day.empty:
            print('Body part pie chart failed. No exercise data on this date. Please try again.')
            return
        day = day.reset_index()
        slices = day.Sets
        plt.figure(figsize=(10,10))
        cmap = plt.cm.Pastel1_r
        colors = cmap(np.linspace(0., 1., len(slices)))
        labels = day.Bodypart
        pie = plt.pie(slices, colors=colors, labels=labels, autopct=make_autopct(slices))
        for pie_label in pie[1]:
            pie_label.set_fontsize(15)
        for pie_wedge in pie[0]:
            pie_wedge.set_edgecolor('white')
        datedict = {'daily': '', 'weekly': 'week starting ', 'monthly': 'month starting ', 'yearly': 'year starting '}
        plt.title('Sets per body part on ' + datedict[timeframe.lower()] + date)
        plt.show()

    def plot_jefit_volume_sets(self, timeframe='weekly'):
        '''
        Compares volume and sets over time.
        '''
        lifts = self._check_timeframe_jefit(timeframe)
        width = 0.33
        idx = list(range(len(lifts.index)))
        idx2 = [num+width for num in idx]
        
        fig, ax1 = plt.subplots()

        rects1 = ax1.bar(idx, lifts.Volume, width, label='Total Volume')
        ax2 = plt.twinx()
        rects2 = ax2.bar([num+width for num in idx], lifts.Sets, width, color='red', label = 'Total Sets')

        ax1.set_ylabel("Total Volume")
        ax1.set_title('Volume/Sets vs Time over ' + timeframe.capitalize() + ' Timeframe')

        ax2.set_ylabel('Total Sets')
        
        # aligning twin axes
        nticks = 11
        ax1.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))
        ax2.yaxis.set_major_locator(matplotlib.ticker.LinearLocator(nticks))

        plt.legend((rects1[0], rects2[0]), ('Volume', 'Sets'), loc=0)
        ax1.set_xticklabels(lifts.index.date)

        fig.autofmt_xdate()
        plt.show()

List of the variables contained within the class, methods exist to access and modify those necessary

In [6]:
variables = pd.Series(list(user.__dict__.keys())).sort_values(inplace=False)
print(variables)
22                  _activity
38                       _age
20                   _bodyfat
42                   _calGoal
35                  _carbGoal
12                 _carbRatio
37                _const_loss
1                      _daily
5                    _fatGoal
14                  _fatRatio
17                    _fatlbs
26                    _gender
6                _goalBodyfat
29                _goalWeight
46                    _height
11                   _hgtunit
18    _historical_performance
24                  _imperial
23                     _lifts
32               _lifts_daily
40       _lifts_daily_no_body
4              _lifts_monthly
0      _lifts_monthly_no_body
3               _lifts_weekly
31      _lifts_weekly_no_body
45              _lifts_yearly
41      _lifts_yearly_no_body
44                   _max_prs
9                    _monthly
34                     _otdee
21                   _otdeeDF
33                  _otdeeFit
39                 _otdeeFit2
27              _pratio_const
13              _pratio_curve
36          _pratio_curve_def
8          _pratio_female_def
2            _pratio_male_def
15                   _proGoal
25                  _proRatio
28                   _profile
43                      _tdee
10                    _weekly
30              _weeklyChange
16                    _weight
7                    _wgtunit
19                    _yearly
dtype: object

List of internal methods calculated upon initialization or upon using set methods

In [7]:
fns = pd.Series(list(vars(FitnessLogs))).sort(inplace=False)
print(fns[5:20])
38              _calc_activity_level
18                    _calc_calories
9                  _calc_lifts_daily
31               _calc_lifts_monthly
12                _calc_lifts_weekly
25                _calc_lifts_yearly
20                     _calc_max_prs
21                       _calc_otdee
29             _calc_pratio_constant
41               _calc_pratio_weight
0                         _calc_tdee
14                  _check_timeframe
40            _check_timeframe_jefit
15    _create_historical_performance
22                     _create_means
dtype: object

Exploratory plot methods available

In [8]:
print(fns[21:33])
6                          plot_bodyfat
13                        plot_calories
24              plot_jefit_bodypart_pie
16    plot_jefit_historical_performance
7                plot_jefit_volume_sets
1                            plot_macro
4                        plot_macro_pie
36                    plot_macro_weight
3                plot_normalized_macros
32                           plot_otdee
19                          plot_pratio
39                          plot_weight
dtype: object

Methods to set or change values imported from Jefit or MyFitnessPal

In [9]:
print(fns[33:])
5     set_activitylevel
27          set_bodyfat
23         set_deadline
42     set_goal_bodyfat
17    set_goal_calories
28      set_goal_weight
34           set_macros
43            set_otdee
37     set_pratio_curve
26     set_weeklychange
2            set_weight
dtype: object

And any methods to get relevant lists of potential inputs

In [10]:
print(fns.iloc[20])
get_exercise_list

Fitness class will initialize with a brief summary regarding your goals and current stats

In [11]:
user = FitnessLogs(jefit=True, start_date='2013-01-01', end_date='2015-08-18')
There are 388 days missing from your data spanning 823 days (47%).
This greatly affects the value of the predicted result which is based on all history; missing weight or caloric logs cannot be accurately estimated (as missing days are often a result of going off diet). It is recommended to use start and end dates without such large gaps.
Data will be imputed based on current limits in the meantime

Predicted TDEE for a male weighing 160.4 lbs and measuring 68.0 inches at age 30.0 with an activity level described as sedentary is 2261.

Your caloric goal is currently set to 1500.
This value is reasonably close to our recommendation of 1369 daily calories for a weekly loss of 2.0 lbs.

With a theoretical TDEE of 2261 you will lose 1.522 lbs per week.
With the empirically calculated oTDEE of 2369 you will lose 1.738 lbs per week.

The maximal daily caloric deficit for fat loss is approximately 31.4 cals per lb of fat. (See http://www.ncbi.nlm.nih.gov/pubmed/15615615)
With 26.15 lbs of fat, you can afford a maximum deficit of 821 cals/day, or 1.642 lbs/week.

Caloric deficit required to lose 2.0 lbs per week is greater than what would reasonably come from fat stores.
As such, you will likely be losing lean body mass (muscle), hindering your efforts to lower bodyfat aka "tone".

It is recommended to lower the weekly weight loss goal to a maximum of 1.642 lbs.
Current macronutrient goals are 168/41/112 P/F/C in grams, with proportions 45.0/25.0/30.0.
Your protein goal is at least 0.82g/lb of body weight, appropriate according to modern research. (See http://www.ncbi.nlm.nih.gov/pubmed/22150425)

Plot Caloric intake over time by chosen timeframe, apply optional smoothing window, in current date range there is a lot of missing data that had to be imputed, better to try with a smaller range

In [12]:
user.plot_calories('daily', window_size = 7)

Smaller date range with less missing data

In [13]:
user = FitnessLogs(jefit=True, start_date='2015-06-18', end_date='2015-08-18')
Predicted TDEE for a male weighing 160.4 lbs and measuring 68.0 inches at age 30.0 with an activity level described as sedentary is 2261.

Your caloric goal is currently set to 1500.
This value is reasonably close to our recommendation of 1369 daily calories for a weekly loss of 2.0 lbs.

With a theoretical TDEE of 2261 you will lose 1.522 lbs per week.
With the empirically calculated oTDEE of 2369 you will lose 1.738 lbs per week.

The maximal daily caloric deficit for fat loss is approximately 31.4 cals per lb of fat. (See http://www.ncbi.nlm.nih.gov/pubmed/15615615)
With 26.15 lbs of fat, you can afford a maximum deficit of 821 cals/day, or 1.642 lbs/week.

Caloric deficit required to lose 2.0 lbs per week is greater than what would reasonably come from fat stores.
As such, you will likely be losing lean body mass (muscle), hindering your efforts to lower bodyfat aka "tone".

It is recommended to lower the weekly weight loss goal to a maximum of 1.642 lbs.
Current macronutrient goals are 168/41/112 P/F/C in grams, with proportions 45.0/25.0/30.0.
Your protein goal is at least 0.82g/lb of body weight, appropriate according to modern research. (See http://www.ncbi.nlm.nih.gov/pubmed/22150425)

Supposing imported body fat or weight was erroneous, or you wanted to change your goal stats:

In [14]:
user.set_weight(175)
user.set_bodyfat(16)
Weight has been modified.

Bodyfat has been modified.

If you had a particular deadline to meet (say you want to hit your college weight by a wedding date) you can use set_deadline. Note that noise is artificially added based on standard deviation of your empirical weight logs.

In [15]:
college_wgt = 145
wedding_date = '2018-12-31'
user.set_deadline(date = wedding_date, goalweight=college_wgt)
# you can get the same plot through the set_goal_weight or  plot_weight methods
------------------------------------------------------------
Goal: Lose 30 lbs by 2018-12-31.

The following calculations assume you begin the caloric deficit tomorrow, and wake up on deadline morning weighing your goal weight.
Please keep in mind that there can be major fluctuations in weight on any particular day up to and including the goal date due to water retention and other cyclical reasons. If "weight" and not "weight loss" is of prime importance, begin eating lower carbohydrates in the final week.

You require an initial daily deficit of 157 calories (this is NOT how much to eat).
Using your theoretical TDEE of 2261, you should start with a daily goal of under 2104 calories, tapering down to 1920 by the final day.
Using your observed TDEE of 2369, you should start with a daily goal of under 2212 calories, tapering down to 2028 by the final day.
------------------------------------------------------------

If you wanted to calculate based off body fat at a given weight using P-Ratio, you could try out a custom model (More info on P-Ratios in https://josetorres.us/data-science/using-p-ratio-to-plan-a-diet-with-python-excel/)

In [16]:
user.set_pratio_curve(np.array([[.60, .5],
                                [.12, 1]]))
user.set_goal_bodyfat(10)
# you can get the same plot through the plot_pratio method
P-Ratio values have been modified.

Goal: To drop from 175 lbs and 16% bodyfat to 10% bodyfat

Using the P-Ratio curve to calculate LBM loss during your diet:
Total Weight Lost: 95.98
Final Weight: 79.0234375
Final LBM: 71.12
Change in LBM: -75.88
Effective Rate of LBM Loss (%): 79.06

Using a flat 20% loss rate to calculate LBM loss during your diet:
Total Weight Lost: 15.0
Final Weight: 160.0
Final LBM: 144.0
Change in LBM: -3.0
Effective Rate of LBM Loss (%): 20

If you can manage to lose 100% fat, congratulations! You will reach your goal in only 11.669999999999987 lbs at a final weight of 163.33 lbs.

To get an estimate of your actual TDEE (total daily energy expenditure) aka maintenance calories using empirical data

In [17]:
user.plot_otdee(window_size = 7)

To examine body fat over time

In [18]:
user.plot_bodyfat(timeframe='daily', window_size=7)

For some more detailed nutritional analysis, you can examine a particular macronutrient over time

In [19]:
user.plot_macro(timeframe='daily', macro='carbs', window_size=7)

Or how well you’ve followed your goal macronutrient ratios (IIFYM, anyone?)

In [20]:
user.plot_macro_pie(timeframe='monthly', date='2015-07-01')
user.plot_normalized_macros(timeframe='weekly')

If you’re trying to optimize weight on a particular day, you can check what immediate effect a particular macro has on your weight (consider water retention from extra carbs, etc). In this case we find very little correlation, a lot of interesting reasons why that may be the case, whether cyclical (hormones), digestive tract retention, sodium, overall water intake, etc.

In [21]:
user.plot_macro_weight(timeframe='daily', macro='carbs')