# Weekly Dose tracker with time and dose mean

## Function:

Outputs daily and weekly dose totals with means for time between doses and dose amount.

### Code:


from decimal import Decimal
from dataclasses import dataclass
from typing import List, Dict, Iterable, Collection

UNIT = 'u'
SUBSTANCE = 'sub'

@dataclass
class DayDoseMean:
day: str
time_dose: Dict[float, float]

@property
def doses(self) -> List[Decimal]:
return [
round(
Decimal(i), 1
) for i in self.time_dose.values()
]

@property
def times(self) -> List[Decimal]:
return [
round(
Decimal(i), 2
) for i in self.time_dose.keys()
]

@property
def daily_dose(self) -> Decimal:
return sum(i for i in self.doses)

@property
def mean(self) -> Decimal:
return self.daily_dose / len(self.doses)

@property
def diff(self) -> Iterable[Decimal]:
return (
abs(
self.times[i] - self.times[i+1]
) for i in range(len(self.times) - 1)
)

@property
def time_mean(self) -> Decimal:
return sum(self.diff) / len(self.times)

@property
def prnt(self):
return (
f'{self.day}{":":4}'
f'{self.daily_dose}{UNIT:4}'
f'{self.mean}{self.time_mean:7}'
)

@dataclass
class WeekDoseMean:
week_dose_mean: Collection[DayDoseMean]

@property
def weekly_dose(self) -> Decimal:
return sum(
i.daily_dose
for i in self.week_dose_mean
)

@property
def weekly_mean(self) -> Decimal:
return round(
sum(
i.mean
for i in self.week_dose_mean
) / 7, 1
)

@property
def weekly_time_mean(self) -> Decimal:
return round(
sum(
i.time_mean
for i in self.week_dose_mean
) / 7, 2
)

@property
def echo(self):
print(
f'\n--------------------------\n'
f'{"Day":7}{SUBSTANCE:7}'
f'{"dM":6}tM\n'
f'--------------------------'
)

print(
'\n'
.join(
i.prnt
for i in self.week_dose_mean
)
)

print(
f'--------------------------\n'
f'Weekly {"dM":4} -> mean: '
f'{self.weekly_mean}\n'
f'Weekly {"tM":4} -> mean: '
f'{self.weekly_time_mean}\n'
f'--------------------------\n'
f'Weekly dose -> {SUBSTANCE}:'
f'{self.weekly_dose}{UNIT}\n'
f'--------------------------'
)

def main():
week_date = WeekDoseMean(
week_dose_mean=(
DayDoseMean(
day='Mon',
time_dose={
12: 1,
13: 1
}
),
DayDoseMean(
day='Tue',
time_dose={
12: 1,
13: 1
}
),
DayDoseMean(
day='Wed',
time_dose={
12: 1,
13: 1
}
),
DayDoseMean(
day='Thu',
time_dose={
12: 1,
13: 1
}
),
DayDoseMean(
day='Fri',
time_dose={
12: 1,
13: 1
}
),
DayDoseMean(
day='Sat',
time_dose={
12: 1,
13: 1
}
),
DayDoseMean(
day='Sun',
time_dose={
12: 2,
14: 2
}
)
)
)
week_date.echo

if __name__ == '__main__':
main()


### Output:


--------------------------
Day    phen   dM    tM
--------------------------
Mon:   2.0u   1.0   0.50
Tue:   2.0u   1.0   0.50
Wed:   2.0u   1.0   0.50
Thu:   2.0u   1.0   0.50
Fri:   2.0u   1.0   0.50
Sat:   2.0u   1.0   0.50
Sun:   4.0u   2.0   1.00
--------------------------
Weekly dM   -> mean: 1.1
Weekly tM   -> mean: 0.57
--------------------------
Weekly dose -> sub:16.0u
--------------------------



### Help:

I don’t know if I understand classes and OOP yet.

• Am I correctly using classes?
• Is this OOP?

#### Background:

I wrote this script a long time ago after learning many things about classes and data structures from a very kind member on here.

I was trying to learn OOP and classes.

• What is the "real" way that data are going to be entered? Surely they won't be hard-coded. Will you have datetimes, rather than strictly days-of-the-week? Aug 15 at 14:04

Am I correctly using classes?

Very vaguely yes, though there's always room for improvement.

Is this OOP?

Yes!

time_dose has as its key type float, which is not a good representation of a time in Python (generally speaking). datetime.time should be used instead. Since you're running these values through a mean, maybe a float would be justified, but

1. making it clear that this is used as a time with a NewType type alias, and
2. using the standard seconds-since-1970-epoch "Unix" timestamp format rather than what seems to be fractional-hours-after-midnight.

DayDoseMean is not a great name, since the class itself doesn't represent a mean, only one of its members; so perhaps call it DayDoses. Likewise with WeekDoses.

Don't abbreviate print to prnt.

This is a little advanced, but in CPython a function like this:

@property
def diff(self) -> Iterable[Decimal]:
return (
abs(
self.times[i] - self.times[i+1]
) for i in range(len(self.times) - 1)
)


will, in bytecode, produce a second hidden generator function, whereas a for/yield will not. I only learned this recently, and I encourage you to use the disassembly tool to see for yourself the difference.

{":":4} and {UNIT:4} are odd. You're putting a delimiter in a fixed-width field. That's probably not what you want. Instead, put your self.day, daily_dose etc. in fixed-width fields, since they're the strings whose length is subject to variance.

sum(i for i in self.doses) can just be sum(self.doses).

You're scattering round calls throughout your analysis code. This does not seem like a good idea. If it's for the purposes of formatting, don't do it where it is now, and just add a fixed-precision specifier in your interpolated strings, like

f'{self.weekly_mean:.1f}\n'


DayDoseMean.prnt and WeekDoseMean.echo are both properties; the first correctly returns a value and the second doesn't. To fix the second, either:

• Keep it as a property but return a string; or
• Remove the property decorator and represent it as a normal method.

Instead of passing days-of-week as strings, use integers. When it comes time to print use https://docs.python.org/3/library/calendar.html#calendar.day_name .

## Pandas

For something very different: Pandas is a better fit for this kind of data-munging and analysis. Advantages are that it's a little more concise, probably runs faster for large datasets, and more expressive in terms of grouping semantics.

• Treat datetimes as first-class and weekday names as second-class
• Are you sure that / len(self.times) is what you want to be doing? Given n times in a day, there are n-1 differential values.
• What do you want to happen for a series of datetimes that spans over one week? Do you still want to group by weekday, or do you simply want to group by day? I've shown the latter, but your example data do not make this clear. In your sample data, using a dictionary of weekday names to doses is not particularly representative of reality. What if your data span eight days? Probably this should just be a sequence of datetimes.
• Don't round anywhere except at the output formatting

## Suggested

import numpy as np
from datetime import datetime

import pandas as pd

SUBSTANCE = 'phen'

def group_by_day(doses: pd.DataFrame) -> pd.DataFrame:
# Fractional hours after midnight
doses['hour'] = (
doses.index - doses.index.date.astype('datetime64[ns]')
) / np.timedelta64(1, 'h')

# Get dose sum, dose mean, and time-differential grouped by day (not weekday)
by_day = (
doses.groupby(by=doses.index.date)
.agg({
SUBSTANCE: ('sum', 'mean'),
'hour': lambda hour: hour.diff().mean(),
})
)
by_day.columns = (SUBSTANCE, 'dM', 'tM')
by_day.insert(
loc=0, column='weekday',
value=by_day.index.astype('datetime64[ns]').strftime('%a'),
)
return by_day

def summarize(by_day: pd.DataFrame) -> None:
print(
f'\nWeekly dM mean: {by_day.dM.mean():.1f}'
f'\nWeekly tM mean: {by_day.tM.mean():.2f}'
f'\nWeekly dose: {SUBSTANCE}: {by_day[SUBSTANCE].sum():.1f}'
)

def main():
# All data, entered flat (not grouped by weekday), plain datetimes
times = (
datetime(2021, 8,  9, 12, 0), datetime(2021, 8,  9, 13, 0),
datetime(2021, 8, 10, 12, 0), datetime(2021, 8, 10, 13, 0),
datetime(2021, 8, 11, 12, 0), datetime(2021, 8, 11, 13, 0),
datetime(2021, 8, 12, 12, 0), datetime(2021, 8, 12, 13, 0),
datetime(2021, 8, 13, 12, 0), datetime(2021, 8, 13, 13, 0),
datetime(2021, 8, 14, 12, 0), datetime(2021, 8, 14, 13, 0),
datetime(2021, 8, 15, 12, 0), datetime(2021, 8, 15, 14, 0),
)
doses = pd.DataFrame(
(
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 2.0, 2.0,
),
columns=(SUBSTANCE,), index=times,
)

by_day = group_by_day(doses)
print(by_day)
summarize(by_day)

if __name__ == '__main__':
main()


## Output

           weekday  phen   dM   tM
2021-08-09     Mon   2.0  1.0  1.0
2021-08-10     Tue   2.0  1.0  1.0
2021-08-11     Wed   2.0  1.0  1.0
2021-08-12     Thu   2.0  1.0  1.0
2021-08-13     Fri   2.0  1.0  1.0
2021-08-14     Sat   2.0  1.0  1.0
2021-08-15     Sun   4.0  2.0  2.0

Weekly dM mean: 1.1
Weekly tM mean: 1.14
Weekly dose: phen: 16.0

• Sorry for the late response, I’ve been away for a while, thanks for the knowledge will use this Sep 15 at 11:31