Disclaimer: This is not medical advice. And please don't be like me. Fasting can be dangerous to some people and if you ever want to try it - please consult your doctor before doing so.
I've come across my biology professor at my school, who was currently on his 13-th day of fast. At first, I didn't believe him. He looked full of energy, had a sharp mind, and wasn't at all hungry (according to him). Everything was opposite of what you expect someone to be after skipping 13 days in a row on every form of calorie intake. I've researched the biology behind fasting and how our bodies respond to it. Many practitioners report increased well-being and mental focus during prolonged fasting. Long story short - I wanted to give it a try. I wanted to collect as much data as possible during my fast. I had my apple watch on me all day, except for bedtime when it needed to be recharged. I bought affordable smart weights from Renpho. They measure many different biological metrics. However, not all of them are within acceptable accuracy (>95%) compared to industrial solutions that can be found in hospitals. Further, I should have measured blood glucose levels as well as blood ketone levels. Since I didn't want to deal with blood then, I decided to pass on that. The best thing about Renpho smart weights is that it synchronizes with Apple Health. Thus I could combine data from both Apple Watch and Repho weights conveniently.
Importing libraries. xmltodict is one of the most powerful libraries to parse XML files.
import xmltodict
import pandas as pd
with open('./export.xml', 'r') as xml_file:
input_data = xmltodict.parse(xml_file.read())
records_list = input_data['HealthData']['Record']
df = pd.DataFrame(records_list)
df.columns
Index(['@type', '@sourceName', '@sourceVersion', '@unit', '@creationDate', '@startDate', '@endDate', '@value', '@device', 'MetadataEntry', 'HeartRateVariabilityMetadataList'], dtype='object')
df['@type'].unique()
array(['HKQuantityTypeIdentifierBodyMassIndex', 'HKQuantityTypeIdentifierHeight', 'HKQuantityTypeIdentifierBodyMass', 'HKQuantityTypeIdentifierHeartRate', 'HKQuantityTypeIdentifierBodyFatPercentage', 'HKQuantityTypeIdentifierLeanBodyMass', 'HKQuantityTypeIdentifierStepCount', 'HKQuantityTypeIdentifierDistanceWalkingRunning', 'HKQuantityTypeIdentifierBasalEnergyBurned', 'HKQuantityTypeIdentifierActiveEnergyBurned', 'HKQuantityTypeIdentifierFlightsClimbed', 'HKQuantityTypeIdentifierAppleExerciseTime', 'HKQuantityTypeIdentifierRestingHeartRate', 'HKQuantityTypeIdentifierVO2Max', 'HKQuantityTypeIdentifierWalkingHeartRateAverage', 'HKQuantityTypeIdentifierEnvironmentalAudioExposure', 'HKQuantityTypeIdentifierHeadphoneAudioExposure', 'HKQuantityTypeIdentifierWalkingDoubleSupportPercentage', 'HKQuantityTypeIdentifierSixMinuteWalkTestDistance', 'HKQuantityTypeIdentifierAppleStandTime', 'HKQuantityTypeIdentifierWalkingSpeed', 'HKQuantityTypeIdentifierWalkingStepLength', 'HKQuantityTypeIdentifierWalkingAsymmetryPercentage', 'HKQuantityTypeIdentifierStairAscentSpeed', 'HKQuantityTypeIdentifierStairDescentSpeed', 'HKDataTypeSleepDurationGoal', 'HKQuantityTypeIdentifierAppleWalkingSteadiness', 'HKCategoryTypeIdentifierSleepAnalysis', 'HKCategoryTypeIdentifierAppleStandHour', 'HKCategoryTypeIdentifierMindfulSession', 'HKCategoryTypeIdentifierLowHeartRateEvent', 'HKCategoryTypeIdentifierAudioExposureEvent', 'HKCategoryTypeIdentifierHeadphoneAudioExposureEvent', 'HKCategoryTypeIdentifierRapidPoundingOrFlutteringHeartbeat', 'HKCategoryTypeIdentifierDizziness', 'HKQuantityTypeIdentifierHeartRateVariabilitySDNN'], dtype=object)
As all of our fields are of type object (string) we need to change our dates to datetime64.
format = '%Y-%m-%d %H:%M:%S %z'
df['@creationDate'] = pd.to_datetime(df['@creationDate'],
format=format)
df['@startDate'] = pd.to_datetime(df['@startDate'],
format=format)
df['@endDate'] = pd.to_datetime(df['@endDate'],
format=format)
We will need to pull up Health data separately, so let's create couple of functions. val_to_numeric - changes string object type to numeric getFastData - gets all the data within time I've been fasting transformFastDataAvg - will combine previous functions, group it by creation day and return resampled mean values. We can use mean values on weight, heart-rate and etc, while values like calories spent over the day will need a separate function - transformFastDataSum
def val_to_numeric(data_frame):
data_frame.loc[:, '@value'] = pd.to_numeric (data_frame.loc[:, '@value'])
return data_frame
def getFastData(dataFrame):
fast_dataFrame = dataFrame[dataFrame['@startDate'] > '2022-04-08']
fast_dataFrame = fast_dataFrame[fast_dataFrame['@endDate'] < '2022-04-17']
return fast_dataFrame
def transformFastDataAvg(dataFrame):
dataFrame = val_to_numeric(dataFrame)
fastDataFrame = getFastData(dataFrame)
fastDataFrameSorted = fastDataFrame.groupby('@creationDate').mean()
return fastDataFrameSorted['@value'].resample('D').mean()
def transformFastDataSum(dataFrame):
dataFrame = val_to_numeric(dataFrame)
fastDataFrame = getFastData(dataFrame)
fastDataFrameSorted = fastDataFrame.groupby('@creationDate').sum()
return fastDataFrameSorted['@value'].resample('D').sum()
body_mass = df[df['@type'] == 'HKQuantityTypeIdentifierBodyMass']
body_mass.head()
@type | @sourceName | @sourceVersion | @unit | @creationDate | @startDate | @endDate | @value | @device | MetadataEntry | HeartRateVariabilityMetadataList | |
---|---|---|---|---|---|---|---|---|---|---|---|
73 | HKQuantityTypeIdentifierBodyMass | Renpho | 2 | lb | 2022-04-08 18:05:18-04:00 | 2022-04-08 10:04:35-04:00 | 2022-04-08 10:04:35-04:00 | 205.2 | NaN | NaN | NaN |
74 | HKQuantityTypeIdentifierBodyMass | Renpho | 2 | lb | 2022-04-08 18:05:18-04:00 | 2022-04-08 10:04:23-04:00 | 2022-04-08 10:04:23-04:00 | 205.2 | NaN | NaN | NaN |
75 | HKQuantityTypeIdentifierBodyMass | Renpho | 2 | lb | 2022-04-08 18:05:18-04:00 | 2022-03-23 08:04:33-04:00 | 2022-03-23 08:04:33-04:00 | 204.8 | NaN | NaN | NaN |
76 | HKQuantityTypeIdentifierBodyMass | Renpho | 2 | lb | 2022-04-08 18:05:18-04:00 | 2022-03-18 09:02:58-04:00 | 2022-03-18 09:02:58-04:00 | 204.4 | NaN | NaN | NaN |
77 | HKQuantityTypeIdentifierBodyMass | Renpho | 2 | lb | 2022-04-08 18:05:18-04:00 | 2022-03-11 10:55:45-04:00 | 2022-03-11 10:55:45-04:00 | 206.2 | NaN | NaN | NaN |
body_mass.dtypes
@type object @sourceName object @sourceVersion object @unit object @creationDate datetime64[ns, pytz.FixedOffset(-240)] @startDate datetime64[ns, pytz.FixedOffset(-240)] @endDate datetime64[ns, pytz.FixedOffset(-240)] @value object @device object MetadataEntry object HeartRateVariabilityMetadataList object dtype: object
As we see, value type is object, thus we need to use our transformFastDataAvg and add it to newly created groupedData series.
groupedData = transformFastDataAvg(body_mass)
Now we need to change Panda's series to dataframe so that we can use awesome df functions
groupedData = groupedData.to_frame()
groupedData.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 9 entries, 2022-04-08 00:00:00-04:00 to 2022-04-16 00:00:00-04:00 Freq: D Data columns (total 1 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 @value 9 non-null float64 dtypes: float64(1) memory usage: 144.0 bytes
We can see that our "value" field has average weight values for 9 days of fast. Value doesn't look informative, let's rename the column
groupedData = groupedData.rename(columns = {'@value':'body_mass'})
rest_heart_rate = df[df['@type'] == 'HKQuantityTypeIdentifierRestingHeartRate']
rest_heart_rate.head()
@type | @sourceName | @sourceVersion | @unit | @creationDate | @startDate | @endDate | @value | @device | MetadataEntry | HeartRateVariabilityMetadataList | |
---|---|---|---|---|---|---|---|---|---|---|---|
1155719 | HKQuantityTypeIdentifierRestingHeartRate | Maksym’s Apple Watch | 8.5.1 | count/min | 2022-04-28 21:38:52-04:00 | 2022-04-28 07:45:04-04:00 | 2022-04-28 21:37:50-04:00 | 55 | NaN | NaN | NaN |
1155720 | HKQuantityTypeIdentifierRestingHeartRate | Maksym’s Apple Watch | 8.0 | count/min | 2021-10-16 13:54:16-04:00 | 2021-10-15 08:40:48-04:00 | 2021-10-15 21:23:08-04:00 | 52 | NaN | NaN | NaN |
1155721 | HKQuantityTypeIdentifierRestingHeartRate | Maksym’s Apple Watch | 8.0 | count/min | 2021-10-16 17:40:27-04:00 | 2021-10-16 14:03:49-04:00 | 2021-10-16 17:38:04-04:00 | 57 | NaN | NaN | NaN |
1155722 | HKQuantityTypeIdentifierRestingHeartRate | Maksym’s Apple Watch | 8.0 | count/min | 2021-10-17 04:50:35-04:00 | 2021-10-17 00:04:40-04:00 | 2021-10-17 04:45:10-04:00 | 49 | NaN | NaN | NaN |
1155723 | HKQuantityTypeIdentifierRestingHeartRate | Maksym’s Apple Watch | 8.0 | count/min | 2021-10-21 18:45:42-04:00 | 2021-10-21 09:21:51-04:00 | 2021-10-21 18:43:28-04:00 | 50 | NaN | NaN | NaN |
Not let's add other important metrics to groupedData
groupedData['rest_heart_rate'] = transformFastDataAvg(rest_heart_rate)
groupedData
body_mass | rest_heart_rate | |
---|---|---|
@creationDate | ||
2022-04-08 00:00:00-04:00 | 205.600000 | 44.0 |
2022-04-09 00:00:00-04:00 | 203.000000 | NaN |
2022-04-10 00:00:00-04:00 | 202.333333 | 48.5 |
2022-04-11 00:00:00-04:00 | 201.800000 | 50.0 |
2022-04-12 00:00:00-04:00 | 199.200000 | 61.0 |
2022-04-13 00:00:00-04:00 | 198.850000 | 59.0 |
2022-04-14 00:00:00-04:00 | 198.200000 | 54.0 |
2022-04-15 00:00:00-04:00 | 196.000000 | 47.0 |
2022-04-16 00:00:00-04:00 | 196.000000 | 53.0 |
We can observe one null value for April 9-th, I just forgot to wear my Apple Watch that day =(
walk_heart_rate = df[df['@type'] == 'HKQuantityTypeIdentifierWalkingHeartRateAverage']
groupedData['walk_heart_rate'] = transformFastDataAvg(walk_heart_rate)
active_energy_burned = df[df['@type'] == 'HKQuantityTypeIdentifierActiveEnergyBurned']
lean_body_mass = df[df['@type'] == 'HKQuantityTypeIdentifierLeanBodyMass']
groupedData['active_energy_burned'] = transformFastDataSum(active_energy_burned)
groupedData['lean_body_mass'] = transformFastDataAvg(lean_body_mass)
I was also collecting very unscientific happiness index of my wellbeing during 9 days of fast, let's add it to our groupedData.
groupedData.insert(5, "happiness_index",[8.5,7,6,7.5,8,7.5,8.5,7.5,5.5])
As creationDate is the index in our dataframe, let's create a separate date column and trim it to just month and day
groupedData['date'] = groupedData.index.to_series().apply(lambda x: x.strftime('%m-%d'))
groupedData
body_mass | rest_heart_rate | walk_heart_rate | active_energy_burned | lean_body_mass | happiness_index | date | |
---|---|---|---|---|---|---|---|
@creationDate | |||||||
2022-04-08 00:00:00-04:00 | 205.600000 | 44.0 | 102.0 | 536.953 | 164.200000 | 8.5 | 04-08 |
2022-04-09 00:00:00-04:00 | 203.000000 | NaN | NaN | 1199.690 | 163.533333 | 7.0 | 04-09 |
2022-04-10 00:00:00-04:00 | 202.333333 | 48.5 | 108.0 | 632.038 | 163.266667 | 6.0 | 04-10 |
2022-04-11 00:00:00-04:00 | 201.800000 | 50.0 | 112.0 | 654.804 | 163.100000 | 7.5 | 04-11 |
2022-04-12 00:00:00-04:00 | 199.200000 | 61.0 | 123.0 | 301.394 | 161.800000 | 8.0 | 04-12 |
2022-04-13 00:00:00-04:00 | 198.850000 | 59.0 | 116.0 | 1605.741 | 161.800000 | 7.5 | 04-13 |
2022-04-14 00:00:00-04:00 | 198.200000 | 54.0 | 104.0 | 1140.253 | 161.400000 | 8.5 | 04-14 |
2022-04-15 00:00:00-04:00 | 196.000000 | 47.0 | 105.0 | 994.718 | 160.400000 | 7.5 | 04-15 |
2022-04-16 00:00:00-04:00 | 196.000000 | 53.0 | 110.5 | 946.773 | 160.400000 | 5.5 | 04-16 |
Once all data is in place we can plot couple of graphs to see results.
groupedData.plot(x = 'date', y='body_mass', kind = 'line')
<AxesSubplot:xlabel='date'>
We can see that weight was going down, however, it did so with a different pace. It's mostly due to water retention. It's normal as with no nutritial supply the balance of sodium and potassium be corrupted and will lead to water retention in blood cells.
groupedData.plot(x = 'date', y='lean_body_mass', kind = 'line')
<AxesSubplot:xlabel='date'>
Lean bodymass decreased as well, thus some muscles were burned as well. So let's add a column that will only have amount of fat burned.
groupedData['fat_mass'] = groupedData['body_mass'] - groupedData['lean_body_mass']
groupedData.plot(x = 'date', y='fat_mass', kind = 'line')
<AxesSubplot:xlabel='date'>
In this graph that we can see that rate of burned fat is more gradual compared to weight loss.
groupedData.plot(x = 'date', y='rest_heart_rate', kind = 'scatter')
<AxesSubplot:xlabel='date', ylabel='rest_heart_rate'>
From resting heart rate graph we can see that it was increasing all the way until deep ketosis (process of changing main energy fuel from glucose to ketones) was reached. After that it stabilized.
groupedData.plot(x = 'date', y='walk_heart_rate', kind = 'scatter')
<AxesSubplot:xlabel='date', ylabel='walk_heart_rate'>
groupedData.plot(x = 'date', y='happiness_index', kind = 'line')
<AxesSubplot:xlabel='date'>
Happiness index is more questionable. First 3 days I was gradually feeling worse. That includes mental and physical wellbeing, energy levels and etc. With reaching ketosis state I started feeling much better. However, after 7 days I started feeling worse. I attribute it to electrolytes imbalance, which effects blood preassure the most. Thus, any movements led to headaches.
To sum everything up it was an interesting experience with some pros and cons. Pros were: training will power, having some great mental experiences. Cons: low energy for first couple of days, feeling pretty bad at the end. However, should I have more free time and extensive preparation I could have controlled by electrolytes balance better, thus delaying processes I've experienced last 2 days of my fast.