Personal blog written from scratch using Node.js, Bootstrap, and MySQL. https://jrtechs.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

584 lines
18 KiB

  1. Health trackers are the current craze. After I bought a Fitbit, I
  2. wanted to determine what exactly could I do with my Fitbit data. Can we
  3. learn something from this data that we did not know before?
  4. Most people don't need a watch to tell them that they walked a lot
  5. today or that they got a ton of sleep. We typically have a pretty good
  6. gauge of our basic physical health. I am interested in figuring out
  7. how we can use data science to look at our health over a longer
  8. period of time and learn something.
  9. Lets first look at a few things that people typically use Fitbit data for
  10. before we jump into the weeds.
  11. - Setting Goals
  12. - Motivation
  13. - Tracking Progress
  14. Ever since I bought a Fitbit, I found that I went to the gym a lot
  15. more frequently. Having something which keeps track of your progress
  16. is a great motivator. Not only is your daily steps recorded for your
  17. own viewing, you can share that data with your friends as a
  18. competition. Although I only have one friend on Fitbit, I found that was
  19. a good motivator to hit ten thousand steps every day.
  20. Goals which are not concrete never get accomplished. Simply
  21. saying that "I will get in shape" is a terrible goal. In order for you
  22. to actually accomplish your goals, they need to be quantifiable, reasonable, and
  23. measurable. Rather than saying "I will improve my health this year",
  24. you can say "I will loose ten pounds this year by increasing my daily
  25. step count to twelve thousand and go to the gym twice a week". One
  26. goal is wishy washy where the other is concrete and measurable. Having
  27. concrete data from Fitbit allows you to quantify your goals and set
  28. milestones for you to accomplish. Along the way to achieving your
  29. goal, you can easily track your progress.
  30. Simply knowing your Fitbit data can help you make more informed
  31. decisions about your fitness. You can tweak your life style after comparing your data against what is
  32. considered healthy. For example: if you notice that
  33. you are only getting 6 hours of sleep per night, you can look up the
  34. recommended amount of sleep and tweak your sleep routine until you hit
  35. that target.
  36. Alright, lets do some data science!
  37. ![Tom and Jerry Data Science Meme](media/fitbit/dataScience.jpg)
  38. # Getting The Data
  39. There are two options that we can use to fetch data from Fitbit.
  40. ## Using Fitbit's API
  41. Fitbit has an [OAuth 2.0 web
  42. API](https://dev.fitbit.com/build/reference/web-api/) that you can
  43. use. You have to register your application on Fitbit's website
  44. to receive a client ID and a client secret to connect to the API.
  45. I decided to fetch the Fitbit data using an Express app with node.
  46. Fetching the data this way will make it really easy to use on a
  47. live website. Node has tons of NPM modules which makes connecting to
  48. Fitbit's API relatively easy. I'm using [Passport](http://www.passportjs.org/) which is a common
  49. authentication middleware for Express.
  50. ```javascript
  51. /** express app */
  52. const express = require("express");
  53. /** Manages oauth 2.0 w/ fitbit */
  54. const passport = require('passport');
  55. /** Used to make API calls */
  56. const unirest = require('unirest');
  57. /** express app */
  58. const app = express();
  59. app.use(passport.initialize());
  60. app.use(passport.session({
  61. resave: false,
  62. saveUninitialized: true
  63. }));
  64. var FitbitStrategy = require( 'passport-fitbit-oauth2' ).FitbitOAuth2Strategy;
  65. var accessTokenTemp = null;
  66. passport.use(new FitbitStrategy({
  67. clientID: config.clientID,
  68. clientSecret: config.clientSecret,
  69. callbackURL: config.callbackURL
  70. },
  71. function(accessToken, refreshToken, profile, done)
  72. {
  73. console.log(accessToken);
  74. accessTokenTemp = accessToken;
  75. done(null, {
  76. accessToken: accessToken,
  77. refreshToken: refreshToken,
  78. profile: profile
  79. });
  80. }
  81. ));
  82. passport.serializeUser(function(user, done) {
  83. done(null, user);
  84. });
  85. passport.deserializeUser(function(obj, done) {
  86. done(null, obj);
  87. });
  88. passport.authenticate('fitbit', { scope:
  89. ['activity','heartrate','location','profile']
  90. });
  91. ```
  92. Since our authentication middlware is all set up, we just need to add
  93. the express routes which are required when authenticating.
  94. ```javascript
  95. app.get('/auth/fitbit',
  96. passport.authenticate('fitbit', { scope:
  97. ['activity','heartrate','location','profile'] }
  98. ));
  99. app.get( '/auth/fitbit/callback', passport.authenticate( 'fitbit', {
  100. successRedirect: '/',
  101. failureRedirect: '/error'
  102. }));
  103. app.get('/error', (request, result) =>
  104. {
  105. result.write("Error authenticating with Fitbit API");
  106. result.end();
  107. });
  108. ```
  109. Now that we are authenticated with Fitbit, we can finally make
  110. queries to Fitbit's Server. I created a helper function called "queryAPI" which attempts
  111. to authenticate if it is not already authenticated and then fetches
  112. the API result from a provided URL.
  113. ```javascript
  114. const queryAPI = function(result, path)
  115. {
  116. return new Promise((resolve, reject)=>
  117. {
  118. if(accessTokenTemp == null)
  119. {
  120. result.redirect('/auth/fitbit');
  121. resolve(false);
  122. }
  123. unirest.get(path)
  124. .headers({'Accept': 'application/json', 'Content-Type': 'application/json',
  125. Authorization: "Bearer " + accessTokenTemp})
  126. .end(function (response)
  127. {
  128. if(response.hasOwnProperty("success") && response.success == false)
  129. {
  130. result.redirect('/auth/fitbit');
  131. resolve(false);
  132. }
  133. resolve(response.body);
  134. });
  135. });
  136. };
  137. app.get('/steps', (request, result)=>
  138. {
  139. queryAPI(result, 'https://api.fitbit.com/1/user/-/activities/tracker/steps/date/today/1m.json').then((data)=>
  140. {
  141. if(data != false)
  142. {
  143. result.writeHead(200, {'Content-Type': 'text/html'});
  144. result.write(JSON.stringify(data));
  145. result.end();
  146. }
  147. else
  148. {
  149. console.log("Validating with API");
  150. }
  151. });
  152. });
  153. ```
  154. ## Exporting Data from Website
  155. On [Fitbit's website](https://www.fitbit.com/settings/data/export)
  156. there is a nice page where you can export your data.
  157. ![Fitbit Website Data Export](media/fitbit/fitbitDataExport.png)
  158. The on demand export is pretty useless because it can only go back a
  159. month. On top of that, you don't get to download any detailed heart rate data.
  160. The data that you get is aggregated by day. This might be fine
  161. for some use cases; however, this will not suffice for any interesting
  162. analysis.
  163. I decided to try the account archive option out of curiosity.
  164. ![Fitbit Archive Data](media/fitbit/fitbitArchiveData.png)
  165. The Fitbit data archive was very organized and kept meticulous records
  166. of everything. All of the data was organized
  167. in separate JSON files labeled by date. Fitbit keeps around 1MB
  168. of data on you per day; most of this data is from the heart rate
  169. sensors. Although 1MB of data may sound like a ton of data, it is probably a
  170. lot less if you store it in formats other than JSON. Since Fitbit
  171. hires a lot of people for hadoop and SQL development, they are most
  172. likely using [Apache Hive](https://hive.apache.org/) to store user
  173. information on the backend. Distributing the data to users as JSON is
  174. really convenient since it makes learning the data schema very easy.
  175. # Visualizing The Data
  176. Since the Data Archive is far easier, I'm going to start visualizing the
  177. data retrieved from the JSON archive. In the future I may
  178. use the Fitbit API if I decide to make this a live website.
  179. Using R to visualize this would be convenient, however; I want to use some
  180. pretty javascript graphs so I can host this as a demo on my website.
  181. ## Heart Rate
  182. My biggest complaint with Fitbit's website is that they only display your continuous
  183. heart rate in one day intervals. If you zoom out to the week or month view, it aggregates it
  184. as the number of minutes you are in each heart rate zone.
  185. This is fine for the fitbit app where you have limited screen space and no good ways of zooming in
  186. and out of the graphs.
  187. ![Fitbit Daily Heart Rate Graph](media/fitbit/fitbitDaily.png)
  188. ![Fitbit Monthly Heart Rate Graph](media/fitbit/fitBitMonthly.png)
  189. I really want to be able to view my heart rate over the course of a
  190. few days. To visualize the continuous heart rate I'm going to
  191. use [VisJS](http://visjs.org/docs/graph2d/) because
  192. it works really well with time series data.
  193. To start, I wrote some Javascript which reads the local JSON files
  194. from the Fitbit data export.
  195. ```html
  196. <div class="col-4 shadow-lg p-3 bg-white rounded">
  197. <label>Heart Rate JSON Files</label>
  198. <input type="file" id="files" name="files[]" multiple />
  199. <output id="list"></output>
  200. </div>
  201. ...
  202. <script>
  203. function handleFileSelect(evt)
  204. {
  205. fetchFilesAsJSONArray(evt).then((data)=>
  206. {
  207. generateHeartRateGraph(data);
  208. })
  209. }
  210. document.getElementById('files').addEventListener('change', handleFileSelect, false);
  211. function fetchFilesAsJSONArray(evt)
  212. {
  213. return new Promise((res, rej)=>
  214. {
  215. var files = evt.target.files; // FileList object
  216. var promises = [];
  217. for (var i = 0, f; f = files[i]; i++)
  218. {
  219. promises.push(new Promise((resolve, reject)=>
  220. {
  221. var reader = new FileReader();
  222. reader.onload = function(e)
  223. {
  224. resolve(JSON.parse(reader.result));
  225. };
  226. reader.onerror= function(e)
  227. {
  228. reject(e);
  229. };
  230. reader.readAsBinaryString(files[i]);
  231. }));
  232. }
  233. Promise.all(promises).then((data)=>
  234. {
  235. res(data);
  236. }).catch((error)=>
  237. {
  238. console.log(error);
  239. console.log("Unable to Load Data");
  240. rej(error);
  241. })
  242. });
  243. }
  244. </script>
  245. ```
  246. The heart rate JSON files look like this:
  247. ```json
  248. [{
  249. "dateTime" : "04/22/19 04:00:05",
  250. "value" : {
  251. "bpm" : 69,
  252. "confidence" : 2
  253. }
  254. },{
  255. "dateTime" : "04/22/19 04:00:10",
  256. "value" : {
  257. "bpm" : 70,
  258. "confidence" : 2
  259. }
  260. }
  261. ...
  262. ]
  263. ```
  264. I found it interesting that each point had a confidence value associated with it. I wonder
  265. how Fitbit is using that confidence information. Since it does not directly appear anywhere in the app,
  266. Fitbit may just be using it to exclude inaccurate points from the heart rate graphs and calculations.
  267. A really annoying thing about this data is that the time stamps don't contain any information on the
  268. timezone. When graphing this data, I shifted the times by 4 hours so that it aligns
  269. with eastern standard time.
  270. After we read the data from the user selected heart rate files, we can treat that object as an array
  271. of arrays. Each array represents a file containing an entire days worth of heart rate measurements. Each day is an
  272. array of time stamped points with heart rate information. Using the code from the
  273. [VisJS examples](http://visjs.org/docs/graph2d/), it is relatively straightforward to plot this data.
  274. ```javascript
  275. function generateHeartRateGraph(jsonFiles)
  276. {
  277. var items = [];
  278. for(var i = 0; i < jsonFiles.length; i++)
  279. {
  280. console.log(jsonFiles[i].length);
  281. for(var j = 0; j < jsonFiles[i].length; j++)
  282. {
  283. var localTime = new Date(jsonFiles[i][j].dateTime);
  284. items.push({y:jsonFiles[i][j].value.bpm,
  285. x:localTime.setHours(localTime.getHours() - 4)});
  286. }
  287. }
  288. var dataset = new vis.DataSet(items);
  289. var options = {
  290. dataAxis: {
  291. showMinorLabels: true,
  292. left: {
  293. title: {
  294. text: "Heart Rate"
  295. }
  296. }
  297. }
  298. };
  299. var container = document.getElementById("heartRateGraph");
  300. var graph2d = new vis.Graph2d(container, dataset, options);
  301. graph2d.on('rangechanged', graphMoved);
  302. graphsOnPage.push(graph2d);
  303. }
  304. ```
  305. It works! As an example, this is what my heart rate looks like over a week.
  306. ![Heart Rate for One Week](media/fitbit/oneWeekHeartRateGraph.png)
  307. ## Time Line
  308. Fitbit does a pretty good job of detecting and recording health related activities.
  309. The two major things that Fitbit detects is sleep and workout activities.
  310. Although the app does a good job at informing you about these activities, the app is lacking
  311. a comprehensive timeline. Rather than provide a timeline for these activities,
  312. the app only displays a simple list.
  313. ![Fitbit Activity History Log](media/fitbit/activityHistory.png)
  314. The JSON filea for sleep store a ton of data! For the sake of the time line, I am only interested
  315. in the start and finish times. Unlike the heart rate data, this actually stores the time zone.
  316. ```json
  317. [{
  318. "logId" : 22128553286,
  319. "dateOfSleep" : "2019-04-28",
  320. "startTime" : "2019-04-27T23:09:00.000",
  321. "endTime" : "2019-04-28T07:33:30.000",
  322. "duration" : 30240000,
  323. "minutesToFallAsleep" : 0,
  324. "minutesAsleep" : 438,
  325. "minutesAwake" : 66,
  326. "minutesAfterWakeup" : 1,
  327. "timeInBed" : 504,
  328. "efficiency" : 86,
  329. "type" : "stages",
  330. "infoCode" : 0,
  331. "levels" : {
  332. "summary" : {
  333. "deep" : {
  334. "count" : 4,
  335. "minutes" : 103,
  336. "thirtyDayAvgMinutes" : 89
  337. },
  338. "wake" : {
  339. "count" : 33,
  340. "minutes" : 66,
  341. "thirtyDayAvgMinutes" : 65
  342. },
  343. "light" : {
  344. "count" : 24,
  345. "minutes" : 214,
  346. "thirtyDayAvgMinutes" : 221
  347. },
  348. "rem" : {
  349. "count" : 16,
  350. "minutes" : 121,
  351. "thirtyDayAvgMinutes" : 93
  352. }
  353. },
  354. "data" : [{
  355. "dateTime" : "2019-04-27T23:09:00.000",
  356. "level" : "wake",
  357. "seconds" : 30
  358. },{
  359. "dateTime" : "2019-04-27T23:09:30.000",
  360. "level" : "light",
  361. "seconds" : 900
  362. },
  363. ```
  364. The JSON file for each activity stores a lot of information on heart rate during the exercise.
  365. Similar to the heart rate file, this date format does not take into account time zones. Grr!
  366. Rather than storing the finish time like the sleep JSON file, this keeps track of the total duration
  367. of the event in milliseconds.
  368. ```json
  369. [{
  370. "logId" : 21092332392,
  371. "activityName" : "Run",
  372. "activityTypeId" : 90009,
  373. "activityLevel" : [{
  374. "minutes" : 0,
  375. "name" : "sedentary"
  376. },{
  377. "minutes" : 0,
  378. "name" : "lightly"
  379. },{
  380. "minutes" : 1,
  381. "name" : "fairly"
  382. },{
  383. "minutes" : 30,
  384. "name" : "very"
  385. }],
  386. "averageHeartRate" : 149,
  387. "calories" : 306,
  388. "duration" : 1843000,
  389. "activeDuration" : 1843000,
  390. "steps" : 4510,
  391. "logType" : "auto_detected",
  392. "manualValuesSpecified" : {
  393. "calories" : false,
  394. "distance" : false,
  395. "steps" : false
  396. },
  397. "heartRateZones" : [{
  398. "name" : "Out of Range",
  399. "min" : 30,
  400. "max" : 100,
  401. "minutes" : 0
  402. },{
  403. "name" : "Fat Burn",
  404. "min" : 100,
  405. "max" : 140,
  406. "minutes" : 6
  407. },{
  408. "name" : "Cardio",
  409. "min" : 140,
  410. "max" : 170,
  411. "minutes" : 24
  412. },{
  413. "name" : "Peak",
  414. "min" : 170,
  415. "max" : 220,
  416. "minutes" : 1
  417. }],
  418. "lastModified" : "04/06/19 17:51:30",
  419. "startTime" : "04/06/19 17:11:48",
  420. "originalStartTime" : "04/06/19 17:11:48",
  421. "originalDuration" : 1843000,
  422. "hasGps" : false,
  423. "shouldFetchDetails" : false
  424. }
  425. ```
  426. After we import both the sleep files and activity files, we can use the VisJS library
  427. to construct a timeline.
  428. ```javascript
  429. function generateTimeline(jsonFiles)
  430. {
  431. var items = [];
  432. for(var i = 0; i < jsonFiles.length; i++)
  433. {
  434. for(var j = 0; j < jsonFiles[i].length; j++)
  435. {
  436. if(jsonFiles[i][j].hasOwnProperty("dateOfSleep"))
  437. {
  438. var startT = new Date(jsonFiles[i][j].startTime);
  439. var finishT = new Date(jsonFiles[i][j].endTime);
  440. items.push({content: "Sleep",
  441. start:startT, end:finishT, group:0});
  442. }
  443. else
  444. {
  445. var localTime = new Date(jsonFiles[i][j].startTime);
  446. var timeAdjusted = localTime.setHours(localTime.getHours() - 4);
  447. var timeFinish = localTime.setMilliseconds(
  448. localTime.getMilliseconds() + jsonFiles[i][j].activeDuration);
  449. items.push({content: jsonFiles[i][j].activityName,
  450. start:timeAdjusted, end:timeFinish, group:0});
  451. }
  452. }
  453. }
  454. console.log("Finished Loading Heart Rate Data Into Graph");
  455. var dataset = new vis.DataSet(items);
  456. var options =
  457. {
  458. margin:
  459. {
  460. item:20,
  461. axis:40
  462. },
  463. showCurrentTime: false
  464. };
  465. var grpups = new vis.DataSet([
  466. {id: 0, content:"Activity", value:0}
  467. ]);
  468. var container = document.getElementById("heartRateGraph");
  469. var graph2d = new vis.Timeline(container, dataset, options);
  470. graph2d.setGroups(grpups);
  471. graph2d.on('rangechanged', graphMoved);
  472. graphsOnPage.push(graph2d);
  473. }
  474. ```
  475. To make both the heart rate graph and the activity timeline focused on the same region at the
  476. same time, I used the 'rangechanged' event to move the other graphs's window of view.
  477. ```javascript
  478. function graphMoved(moveEvent)
  479. {
  480. graphsOnPage.forEach((g)=>
  481. {
  482. g.setWindow(moveEvent.start, moveEvent.end);
  483. })
  484. }
  485. ```
  486. I am pretty pleased with how these two graphs turned out. When you zoom too far out of the graph, the
  487. events get really small, but, it does a pretty good job at visualizing a few days worth of data at a time.
  488. ![Fitbit Activity TimeLine With Heart Rate](media/fitbit/fitbitDailyActivities.png)
  489. ![Fitbit Activity TimeLine](media/fitbit/morningRoutine.png)
  490. # Future Analysis
  491. There is a ton of data to look at here and thousands of different angles which I can take.
  492. Since this blog post is getting rather long, I'm going to split this up and write a few more articles on Fitbit data.
  493. I currently have three ideas for future blog posts on this topic.
  494. - What factors affect sleep quality the most.
  495. - Using fuzzy logic with Fitbit data to plan, train, and assess fitness goals.
  496. - Third party data to use with Fitbit.
  497. The full source code to the web page that I created today can be found on my [Github](https://github.com/jrtechs/HomePage/blob/master/fitbitVisualizer.html).