3
\$\begingroup\$

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?
  • Please suggest improvements

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.

\$\endgroup\$
1
  • \$\begingroup\$ 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? \$\endgroup\$
    – Reinderien
    Aug 15 at 14:04
2
\$\begingroup\$

Am I correctly using classes?

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

Is this OOP?

Yes!

Please suggest improvements

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
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Sorry for the late response, I’ve been away for a while, thanks for the knowledge will use this \$\endgroup\$
    – Nickotine
    Sep 15 at 11:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.