Skip to content

Calculate percentage spent in each HR zone#29

Merged
vkurup merged 4 commits intovkurup:masterfrom
khink:percentage-in-zones
Dec 20, 2020
Merged

Calculate percentage spent in each HR zone#29
vkurup merged 4 commits intovkurup:masterfrom
khink:percentage-in-zones

Conversation

@khink
Copy link
Contributor

@khink khink commented Dec 1, 2020

I was looking for a simple way to extract the percentage of workout spent in each HR zone from .tcx files, and this module seems the best basis.

This calculates the percentage based on number of measurements (TrackPoints), it assumes the time between these measurements is roughly equal. This is not the most precise way, to do it, but for my purpose (and, i would say, arguably for most) it suffices, as usually the measurements are taken in regular intervals.

I hope you find it useful as well.

Copy link
Owner

@vkurup vkurup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking the time to contribute this! I don't personally use this library anymore, but this looks really useful. I made a few comments, which I'll try to get to at some point if you don't beat me to it.

return min(self.hr_values())

def hr_percent_in_zones(self, zones):
"""Percentage of workout spent in eacht heart rate zone.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Percentage of workout spent in eacht heart rate zone.
"""Percentage of workout spent in each heart rate zone.

Comment on lines +104 to +111
Given these user's heart rate zones:
{
"Z0": (0, 119),
"Z1": (120, 199),
"Z2": (200, 240),
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Given these user's heart rate zones:
{
"Z0": (0, 119),
"Z1": (120, 199),
"Z2": (200, 240),
}
Given these user's heart rate zones:
zones = {
"Z0": (0, 119),
"Z1": (120, 199),
"Z2": (200, 240),
}

Comment on lines +111 to +118
We return something like:
{
"Z0": 5,
"Z1": 95,
"Z2": 0,
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
We return something like:
{
"Z0": 5,
"Z1": 95,
"Z2": 0,
}
Then `self.hr_percent_in_zones(zones)` would return something like:
{
"Z0": 5,
"Z1": 95,
"Z2": 0,
}

Correct calculation relies on evenly spaced measurement times.
"""
# count number of HR measurements per zone
per_zone = dict(zip(zones.keys(), [0 for x in zones.keys()]))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now use Python's cool defaultdict for this. It's a dictionary that acts like a normal dictionary unless you try to assign values to an unknown key, in which case, it will create an entry for that key, defaulting to whatever you'd like it to default to. So this line could be replaced with:

from collections import defaultdict
per_zone = defaultdict(int)  # <- int class defaults to zero

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code could indeed be more elegant. I found defaultdict by itself does not do the main thing that i want to happen here, which is create a dictionary with same keys as zones. I propose to change it to this:

        # Initialize a dictionary with one item for each zone
        per_zone = dict.fromkeys(zones.keys(), 0)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh good point. This looks great.

# convert counts to percentages
nr_hr_values = len(hr_values)
for name, count in per_zone.items():
per_zone[name] = round(100 * count / nr_hr_values)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure, but I think it would be possible for nr_hr_values to be zero so we'd get a division by zero error here (like if a user called this method without having any valid HR data parsed in their data file). So we should probably just make sure that nr_hr_values is not zero. Better safe than sorry :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. As errors should not go unnoticed, i decided to raise a NoHeartRateDataError in this case.

@khink
Copy link
Contributor Author

khink commented Dec 19, 2020 via email

@khink
Copy link
Contributor Author

khink commented Dec 20, 2020

I see now that the tests failed for Python2.7, because of the super().__init__(self.message) construct.

Do you see any reason to keep supporting Python 2? It has been officially EOL since the beginning of 2020. In my PR, i've "solved the problem" by removing Python 2.7 from the test matrix.

Rather than fail with DivisionByZeroError, we raise a custom
exception that shows what the cause of the problem is.
@vkurup
Copy link
Owner

vkurup commented Dec 20, 2020

Agree that there's no need to support 2.7 any longer. Thank you for this contribution.

@vkurup vkurup merged commit f11f196 into vkurup:master Dec 20, 2020
@khink
Copy link
Contributor Author

khink commented Dec 21, 2020

Thanks for the review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants