''' Created on Aug 12, 2018 @author: Brett Paufler Copyright Brett Paufler A continuation of the calorie_counter_script: Better Code! Different Graphs! Logical Structure: CalorieCounter Day Meal Meal Day Meal Meal Or if you don't like that explaination: CalorieCounter -> List of Days -> List of Meals This code was faster to write (no comments) and to me is easier to read (no comments), but that is just a personal opinion. I may come back and add a few more graphs, but that will wait until the webpage write-up. ''' #ALL imports at the top import numpy as np import matplotlib.pyplot as plt #This is still a one shot script. #But I could use it for another data set, easily. file_in = './/input//calorie-counter-second.txt' class Meal(object): '''The lowest level data construct: an individual eating record).''' def __init__(self, time, cal, pro): '''Not much to it.''' self.time = time self.cal = cal self.pro = pro def __repr__(self): '''The output if we print an instance.''' text = 'meal (t:%d, c:%d, p:%d)' % ( self.time, self.cal, self.pro) return text class Day(object): '''Meals are contained within days, so this is the second level of abstraction.''' def __init__(self, date): '''Days contain a date and a list of meals. Note: meals are passed to an existing day object, not during creation.''' #I like listing all attributes out. self.date = date self.meals = [] #Even ones that do not start with values. self.num_meals = None self.cal_total = None self.pro_total = None #Then again, values are added here. #Many would be happy to bury the attributes #in the function, I would not.s self.initialize_values() def __repr__(self): '''Text for the print command.''' header = 'Day: %d (n:%d, c:%d, p:%d)\n\t' % ( self.date, self.num_meals, self.cal_total, self.pro_total) meals_list = [meal.__repr__() for meal in self.meals] meals_text = ('\n\t').join(meals_list) text = header + meals_text return text def initialize_values(self): '''I made fewer graphs than I was expecting. Thus, I thought I would be using these values over and over. I did not. If I had, there may have been some computation cost savings. Also, done once, I know they are right.''' self.num_meals = len(self.meals) self.cal_total = sum([ meal.cal for meal in self.meals]) self.pro_total = sum([ meal.pro for meal in self.meals]) class CalorieCounter(object): '''The main wrapping class. None of these classes are required, but this one even less than the others.''' def __init__(self): self.days = [] self.num_days = 0 self.num_meals = 0 self.cal_list_all =[] self.cal_total = 0 self.pro_list_all = [] self.pro_total = 0 #Rountine called upon initiation, which reads #file_in and converts to a useable series of #objects self.initialize_text() #Once I have the starter objects, I replace #the 0's & []'s with terminal values. #For a live object (one that is updated), #this would likely be far from ideal. self.initialize_values() def initialize_text(self): '''Initialize Day -> Meal construct. Although this is the same 'munging' as used in the script, it has been improved. After all, I've now been down this road before. ''' with open(file_in, 'r') as f: raw_text = f.read() #Here's the real insight text_slashless = raw_text.replace('/', '-') text_list_raw = text_slashless.split('\n\n') text_list_rev = list(reversed(text_list_raw)) text_days = [day for day in text_list_rev if not ('' == day or 'Calorie' in day or 'Brett' in day)] for text_day in text_days: lines_raw = text_day.split('\n') lines = [i for i in lines_raw if i != ''] date = int(lines[0]) meals = lines[1:] day = Day(date=date) for meal in meals: time, cal, pro = meal.split('-') meal = Meal( int(time), int(cal), int(pro)) day.meals.append(meal) day.meals = sorted( day.meals, key=lambda x: x.time) day.initialize_values() self.days.append(day) #print day #One day complete #All days complete #Exit function def initialize_values(self): '''Calculate starting values. The expected values (given my data) are as follows: num_days = 31 num_meals = 209 cal_list_all = [... cal_total = 102226 pro_list_all = [... pro_total = 4523 ''' self.num_days = len(self.days) self.num_meals = len([ meal for day in self.days for meal in day.meals]) self.cal_list_all = [ meal.cal for day in self.days for meal in day.meals] self.cal_total = sum(self.cal_list_all) self.pro_list_all = [ meal.pro for day in self.days for meal in day.meals] self.pro_total = sum( self.pro_list_all) #Um, to check the expected values, #this is where/when I wrote all those #__repr__. def __repr__(self): '''I've done this before, so the main use is as a sanity check. Am I getting the results I expect? Because, this is what I expect: REPR: SANITY CHECK days: 31, meals=209 cal_total: 102226 pro_total: 4523 cal/pro: 22 I essentially got to here (whatever here means) on my first sprint (+/- one hour). During which time I utilized neither numpy nor matplotlib even though I imported both, first thing. ''' #Notice that text +=, below? #That's like the polar opposite of #Functional Programming. text = 'REPR: SANITY CHECK\n' text += 'days: %d, meals=%d\n' % ( self.num_days, self.num_meals) text += 'cal_total: %d\n' % self.cal_total text += 'pro_total: %d\n' % self.pro_total text += '\tcal/pro: %d' % ( self.cal_total / self.pro_total) return text def graph_scatterplot_cal_pro(self): '''The graphs that follow, are for the most cut and paste from the script. Very little is of any meaningful interest. They do a job.''' plt.figure(figsize=(10,5)) #A scatterplot plt.scatter( self.cal_list_all, self.pro_list_all, marker='o', color='blue') #Plot the Trendline: Deep Magic plt.plot( self.cal_list_all, np.poly1d( np.polyfit( self.cal_list_all, self.pro_list_all, 1) )(self.cal_list_all ), color='red') #This confirms the slope is essentially 22 #plt.plot( # range(0, 2200, 22), # range(0, 100, 1), # color='yellow') plt.ylim(0, 100) plt.xlim(0, 2000) plt.title('Calories vs Protein') plt.ylabel('Protein (grams)') plt.xlabel('Serving Size (calories)') save_name = './output/calories_cal_pro.png' print save_name plt.savefig(save_name) #plt.show() plt.close() def graph_scatterplot_cal_pro_normal(self): plt.figure(figsize=(10,5)) pro_cal_ratio = [ float(p)/float(c) for c,p in zip( self.cal_list_all, self.pro_list_all)] #A scatterplot, again plt.scatter( self.cal_list_all, pro_cal_ratio, marker='o', color='blue') #Plot the Trendline plt.plot( self.cal_list_all, np.poly1d( np.polyfit( self.cal_list_all, pro_cal_ratio, 1) )(self.cal_list_all ), color='red') plt.ylim(0, 0.16) plt.xlim(0, 2000) plt.title('Ratio: Protein to Calories') plt.ylabel('gram protein / calorie') plt.xlabel('Serving Size (calories)') save_name = './output/calories_cal_pro_ratio.png' print save_name plt.savefig(save_name) #plt.show() plt.close() def graph_protein_per_day(self): '''Similiar to the calorie. It slumps in the middle. And as such, I am not expecting much difference in protein graphs from the previous calorie graphs.''' pro_per_day = [ day.pro_total for day in self.days] #print len(pro_per_day) #31 plt.figure(figsize=(10,5)) #Daily, Actual plt.plot( range(1, 32, 1), pro_per_day, color='blue', label='daily') #Average plt.plot( range(1, 32, 1), 31 * [sum(pro_per_day) / 31.0], color='red', label='average') plt.title('Protein Consumed per Day') plt.ylabel('Grams Protein') plt.xlabel('December, 2017') plt.ylim(50, 250) plt.xlim(1, 31) plt.legend(loc='lower right') save_name = './output/calories_pro_per_day.png' print save_name plt.savefig(save_name) #plt.show() plt.close() if __name__ == '__main__': cc = CalorieCounter() print cc cc.graph_scatterplot_cal_pro() cc.graph_scatterplot_cal_pro_normal() cc.graph_protein_per_day() ''' Print Output (as of this writing, 3 graphs, no debugs): REPR: SANITY CHECK days: 31, meals=209 cal_total: 102226 pro_total: 4523 cal/pro: 22 ./output/calories_cal_pro.png ./output/calories_cal_pro_ratio.png ./output/calories_pro_per_day.png And I think I have what I am going to use to write-up the webpage. 2018-08-13 (c) Brett Paufler '''